Merge branch 'main' into commands-list-clean

This commit is contained in:
Luke
2026-01-09 03:12:08 -05:00
committed by GitHub
128 changed files with 5743 additions and 1503 deletions

View File

@@ -27,6 +27,8 @@ describe("commands registry", () => {
expect(detection.regex.test("/status:")).toBe(true);
expect(detection.regex.test("/stop")).toBe(true);
expect(detection.regex.test("/send:")).toBe(true);
expect(detection.regex.test("/models")).toBe(true);
expect(detection.regex.test("/models list")).toBe(true);
expect(detection.regex.test("try /status")).toBe(false);
});

View File

@@ -111,7 +111,7 @@ const CHAT_COMMANDS: ChatCommandDefinition[] = [
key: "model",
nativeName: "model",
description: "Show or set the model.",
textAliases: ["/model"],
textAliases: ["/model", "/models"],
acceptsArgs: true,
},
{

View File

@@ -10,6 +10,13 @@ describe("extractModelDirective", () => {
expect(result.cleaned).toBe("");
});
it("extracts /models with argument", () => {
const result = extractModelDirective("/models gpt-5");
expect(result.hasDirective).toBe(true);
expect(result.rawModel).toBe("gpt-5");
expect(result.cleaned).toBe("");
});
it("extracts /model with provider/model format", () => {
const result = extractModelDirective("/model anthropic/claude-opus-4-5");
expect(result.hasDirective).toBe(true);

View File

@@ -14,7 +14,7 @@ export function extractModelDirective(
if (!body) return { cleaned: "", hasDirective: false };
const modelMatch = body.match(
/(?:^|\s)\/model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i,
/(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)?)?/i,
);
const aliases = (options?.aliases ?? [])

View File

@@ -181,7 +181,7 @@ describe("trigger handling", () => {
});
});
it("restarts even with prefix/whitespace", async () => {
it("rejects /restart by default", async () => {
await withTempHome(async (home) => {
const res = await getReplyFromConfig(
{
@@ -193,6 +193,24 @@ describe("trigger handling", () => {
makeCfg(home),
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("/restart is disabled");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
});
});
it("restarts when enabled", async () => {
await withTempHome(async (home) => {
const cfg = { ...makeCfg(home), commands: { restart: true } };
const res = await getReplyFromConfig(
{
Body: "/restart",
From: "+1001",
To: "+2000",
},
{},
cfg,
);
const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(
text?.startsWith("⚙️ Restarting") ||
text?.startsWith("⚠️ Restart failed"),

View File

@@ -1,8 +1,7 @@
import crypto from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { TemplateContext } from "../templating.js";
import { onAgentEvent } from "../../infra/agent-events.js";
import type { TemplateContext } from "../templating.js";
import type { FollowupRun, QueueSettings } from "./queue.js";
import { createMockTypingController } from "./test-helpers.js";
@@ -105,9 +104,7 @@ function createRun() {
describe("runReplyAgent claude-cli routing", () => {
it("uses claude-cli runner for claude-cli provider", async () => {
const randomSpy = vi
.spyOn(crypto, "randomUUID")
.mockReturnValue("run-1");
const randomSpy = vi.spyOn(crypto, "randomUUID").mockReturnValue("run-1");
const lifecyclePhases: string[] = [];
const unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== "run-1") return;

View File

@@ -18,7 +18,10 @@ import {
} from "../../config/sessions.js";
import type { TypingMode } from "../../config/types.js";
import { logVerbose } from "../../globals.js";
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
import {
emitAgentEvent,
registerAgentRunContext,
} from "../../infra/agent-events.js";
import { defaultRuntime } from "../../runtime.js";
import {
estimateUsageCost,
@@ -352,7 +355,7 @@ export async function runReplyAgent(params: {
runId,
extraSystemPrompt: followupRun.run.extraSystemPrompt,
ownerNumbers: followupRun.run.ownerNumbers,
resumeSessionId:
claudeSessionId:
sessionEntry?.claudeCliSessionId?.trim() || undefined,
})
.then((result) => {

View File

@@ -220,16 +220,13 @@ function resolveModelAuthLabel(
const providerKey = normalizeProviderId(resolved);
const store = ensureAuthProfileStore();
const profileOverride = sessionEntry?.authProfileOverride?.trim();
const lastGood = store.lastGood?.[providerKey] ?? store.lastGood?.[resolved];
const order = resolveAuthProfileOrder({
cfg,
store,
provider: providerKey,
preferredProfile: profileOverride,
});
const candidates = [profileOverride, lastGood, ...order].filter(
Boolean,
) as string[];
const candidates = [profileOverride, ...order].filter(Boolean) as string[];
for (const profileId of candidates) {
const profile = store.profiles[profileId];
@@ -240,6 +237,10 @@ function resolveModelAuthLabel(
if (profile.type === "oauth") {
return `oauth${label ? ` (${label})` : ""}`;
}
if (profile.type === "token") {
const snippet = formatApiKeySnippet(profile.token);
return `token ${snippet}${label ? ` (${label})` : ""}`;
}
const snippet = formatApiKeySnippet(profile.key);
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
}
@@ -508,6 +509,14 @@ export async function handleCommands(params: {
);
return { shouldContinue: false };
}
if (cfg.commands?.restart !== true) {
return {
shouldContinue: false,
reply: {
text: "⚠️ /restart is disabled. Set commands.restart=true to enable.",
},
};
}
const hasSigusr1Listener = process.listenerCount("SIGUSR1") > 0;
if (hasSigusr1Listener) {
scheduleGatewaySigusr1Restart({ reason: "/restart" });

View File

@@ -88,13 +88,18 @@ const resolveAuthLabel = async (
!profile ||
(configProfile?.provider &&
configProfile.provider !== profile.provider) ||
(configProfile?.mode && configProfile.mode !== profile.type)
(configProfile?.mode &&
configProfile.mode !== profile.type &&
!(configProfile.mode === "oauth" && profile.type === "token"))
) {
return `${profileId}=missing`;
}
if (profile.type === "api_key") {
return `${profileId}=${maskApiKey(profile.key)}`;
}
if (profile.type === "token") {
return `${profileId}=token:${maskApiKey(profile.token)}`;
}
const display = resolveAuthProfileDisplayLabel({
cfg,
store,

View File

@@ -330,7 +330,9 @@ export function buildStatusMessage(args: StatusArgs): string {
const usagePair = formatUsagePair(inputTokens, outputTokens);
const costLine = costLabel ? `💵 Cost: ${costLabel}` : null;
const usageCostLine =
usagePair && costLine ? `${usagePair} · ${costLine}` : usagePair ?? costLine;
usagePair && costLine
? `${usagePair} · ${costLine}`
: (usagePair ?? costLine);
return [
versionLine,
@@ -349,7 +351,7 @@ export function buildStatusMessage(args: StatusArgs): string {
export function buildHelpMessage(): string {
return [
" Help",
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
"Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)",
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id> | /cost on|off",
"More: /commands for all slash commands",
].join("\n");