diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index c2ac71824..037ca0d29 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -718,9 +718,7 @@ export async function setAuthProfileOrder(params: { const providerKey = normalizeProviderId(params.provider); const sanitized = params.order && Array.isArray(params.order) - ? params.order - .map((entry) => String(entry).trim()) - .filter(Boolean) + ? params.order.map((entry) => String(entry).trim()).filter(Boolean) : []; const deduped: string[] = []; diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index ce5248966..b7e4eaaf8 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -88,7 +88,9 @@ const resolveAuthLabel = async ( mode: ModelAuthDetailMode = "compact", ): Promise<{ label: string; source: string }> => { const formatPath = (value: string) => shortenHomePath(value); - const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); + const store = ensureAuthProfileStore(agentDir, { + allowKeychainPrompt: false, + }); const order = resolveAuthProfileOrder({ cfg, store, provider }); const providerKey = normalizeProviderId(provider); const lastGood = (() => { @@ -121,7 +123,8 @@ const resolveAuthLabel = async ( const configProfile = cfg.auth?.profiles?.[profileId]; const missing = !profile || - (configProfile?.provider && configProfile.provider !== profile.provider) || + (configProfile?.provider && + configProfile.provider !== profile.provider) || (configProfile?.mode && configProfile.mode !== profile.type && !(configProfile.mode === "oauth" && profile.type === "token")); @@ -170,7 +173,11 @@ const resolveAuthLabel = async ( if (lastGood && profileId === lastGood) flags.push("lastGood"); if (isProfileInCooldown(store, profileId)) { const until = store.usageStats?.[profileId]?.cooldownUntil; - if (typeof until === "number" && Number.isFinite(until) && until > now) { + if ( + typeof until === "number" && + Number.isFinite(until) && + until > now + ) { flags.push(`cooldown ${formatUntil(until)}`); } else { flags.push("cooldown"); @@ -197,7 +204,11 @@ const resolveAuthLabel = async ( Number.isFinite(profile.expires) && profile.expires > 0 ) { - flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`); + flags.push( + profile.expires <= now + ? "expired" + : `exp ${formatUntil(profile.expires)}`, + ); } const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : ""; return `${profileId}=token:${maskApiKey(profile.token)}${suffix}`; @@ -218,7 +229,11 @@ const resolveAuthLabel = async ( Number.isFinite(profile.expires) && profile.expires > 0 ) { - flags.push(profile.expires <= now ? "expired" : `exp ${formatUntil(profile.expires)}`); + flags.push( + profile.expires <= now + ? "expired" + : `exp ${formatUntil(profile.expires)}`, + ); } const suffixLabel = suffix ? ` ${suffix}` : ""; const suffixFlags = flags.length > 0 ? ` (${flags.join(", ")})` : ""; @@ -242,7 +257,8 @@ const resolveAuthLabel = async ( if (customKey) { return { label: maskApiKey(customKey), - source: mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "", + source: + mode === "verbose" ? `models.json: ${formatPath(modelsPath)}` : "", }; } return { label: "missing", source: "missing" }; @@ -803,16 +819,16 @@ export async function handleDirectiveOnly(params: { } modelSelection = resolved.selection; if (modelSelection) { - if (directives.rawModelProfile) { - const profileResolved = resolveProfileOverride({ - rawProfile: directives.rawModelProfile, - provider: modelSelection.provider, - cfg: params.cfg, - agentDir, - }); - if (profileResolved.error) { - return { text: profileResolved.error }; - } + if (directives.rawModelProfile) { + const profileResolved = resolveProfileOverride({ + rawProfile: directives.rawModelProfile, + provider: modelSelection.provider, + cfg: params.cfg, + agentDir, + }); + if (profileResolved.error) { + return { text: profileResolved.error }; + } profileOverride = profileResolved.profileId; } const nextLabel = `${modelSelection.provider}/${modelSelection.model}`; @@ -994,6 +1010,10 @@ export async function persistInlineDirectives(params: { agentCfg, } = params; let { provider, model } = params; + const activeAgentId = sessionKey + ? resolveAgentIdFromSessionKey(sessionKey) + : resolveDefaultAgentId(cfg); + const agentDir = resolveAgentDir(cfg, activeAgentId); if (sessionEntry && sessionStore && sessionKey) { let updated = false; diff --git a/src/cli/gateway-cli.ts b/src/cli/gateway-cli.ts index 11ecd42b9..f478b3293 100644 --- a/src/cli/gateway-cli.ts +++ b/src/cli/gateway-cli.ts @@ -180,7 +180,10 @@ async function ensureDevWorkspace(dir: string) { path.join(resolvedDir, "TOOLS.md"), DEV_TOOLS_TEMPLATE, ); - await writeFileIfMissing(path.join(resolvedDir, "USER.md"), DEV_USER_TEMPLATE); + await writeFileIfMissing( + path.join(resolvedDir, "USER.md"), + DEV_USER_TEMPLATE, + ); await writeFileIfMissing( path.join(resolvedDir, "HEARTBEAT.md"), DEV_HEARTBEAT_TEMPLATE, diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index dd2ca826b..ce897d66d 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -392,7 +392,9 @@ export function registerModelsCli(program: Command) { order .command("set") - .description("Set per-agent auth order override (locks rotation to this list)") + .description( + "Set per-agent auth order override (locks rotation to this list)", + ) .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") .argument("", "Auth profile ids (e.g. anthropic:claude-cli)") @@ -414,7 +416,9 @@ export function registerModelsCli(program: Command) { order .command("clear") - .description("Clear per-agent auth order override (fall back to config/round-robin)") + .description( + "Clear per-agent auth order override (fall back to config/round-robin)", + ) .requiredOption("--provider ", "Provider id (e.g. anthropic)") .option("--agent ", "Agent id (default: configured default agent)") .action(async (opts) => { diff --git a/src/cli/profile.test.ts b/src/cli/profile.test.ts index daf51071d..e67657713 100644 --- a/src/cli/profile.test.ts +++ b/src/cli/profile.test.ts @@ -23,12 +23,7 @@ describe("parseCliProfileArgs", () => { }); it("still accepts global --dev before subcommand", () => { - const res = parseCliProfileArgs([ - "node", - "clawdbot", - "--dev", - "gateway", - ]); + const res = parseCliProfileArgs(["node", "clawdbot", "--dev", "gateway"]); if (!res.ok) throw new Error(res.error); expect(res.profile).toBe("dev"); expect(res.argv).toEqual(["node", "clawdbot", "gateway"]); diff --git a/src/commands/configure.ts b/src/commands/configure.ts index f915f439e..d75a1527e 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -80,6 +80,7 @@ import { DEFAULT_WORKSPACE, ensureWorkspaceAndSessions, guardCancel, + openUrl, printWizardHeader, probeGatewayReachable, randomToken, diff --git a/src/commands/models/auth-order.ts b/src/commands/models/auth-order.ts index e0429a372..4af49e63c 100644 --- a/src/commands/models/auth-order.ts +++ b/src/commands/models/auth-order.ts @@ -1,16 +1,22 @@ -import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { + resolveAgentDir, + resolveDefaultAgentId, +} from "../../agents/agent-scope.js"; +import { + type AuthProfileStore, ensureAuthProfileStore, setAuthProfileOrder, - type AuthProfileStore, } from "../../agents/auth-profiles.js"; import { normalizeProviderId } from "../../agents/model-selection.js"; import { loadConfig } from "../../config/config.js"; +import { normalizeAgentId } from "../../routing/session-key.js"; import type { RuntimeEnv } from "../../runtime.js"; import { shortenHomePath } from "../../utils.js"; -import { normalizeAgentId } from "../../routing/session-key.js"; -function resolveTargetAgent(cfg: ReturnType, raw?: string): { +function resolveTargetAgent( + cfg: ReturnType, + raw?: string, +): { agentId: string; agentDir: string; } { @@ -37,7 +43,9 @@ export async function modelsAuthOrderGetCommand( const cfg = loadConfig(); const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent); - const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); + const store = ensureAuthProfileStore(agentDir, { + allowKeychainPrompt: false, + }); const order = describeOrder(store, provider); if (opts.json) { @@ -59,9 +67,13 @@ export async function modelsAuthOrderGetCommand( runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); - runtime.log(`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`); runtime.log( - order.length > 0 ? `Order override: ${order.join(", ")}` : "Order override: (none)", + `Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`, + ); + runtime.log( + order.length > 0 + ? `Order override: ${order.join(", ")}` + : "Order override: (none)", ); } @@ -75,8 +87,13 @@ export async function modelsAuthOrderClearCommand( const cfg = loadConfig(); const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent); - const updated = await setAuthProfileOrder({ agentDir, provider, order: null }); - if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?)."); + const updated = await setAuthProfileOrder({ + agentDir, + provider, + order: null, + }); + if (!updated) + throw new Error("Failed to update auth-profiles.json (lock busy?)."); runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); @@ -94,7 +111,9 @@ export async function modelsAuthOrderSetCommand( const cfg = loadConfig(); const { agentId, agentDir } = resolveTargetAgent(cfg, opts.agent); - const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); + const store = ensureAuthProfileStore(agentDir, { + allowKeychainPrompt: false, + }); const providerKey = normalizeProviderId(provider); const requested = (opts.order ?? []) .map((entry) => String(entry).trim()) @@ -120,10 +139,10 @@ export async function modelsAuthOrderSetCommand( provider, order: requested, }); - if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?)."); + if (!updated) + throw new Error("Failed to update auth-profiles.json (lock busy?)."); runtime.log(`Agent: ${agentId}`); runtime.log(`Provider: ${provider}`); runtime.log(`Order override: ${describeOrder(updated, provider).join(", ")}`); } - diff --git a/src/gateway/server.sessions-send.test.ts b/src/gateway/server.sessions-send.test.ts index 4e78115ed..acfbbf38a 100644 --- a/src/gateway/server.sessions-send.test.ts +++ b/src/gateway/server.sessions-send.test.ts @@ -103,88 +103,92 @@ describe("sessions_send gateway loopback", () => { }); describe("sessions_send label lookup", () => { - it("finds session by label and sends message", async () => { - const port = await getFreePort(); - const prevPort = process.env.CLAWDBOT_GATEWAY_PORT; - process.env.CLAWDBOT_GATEWAY_PORT = String(port); + it( + "finds session by label and sends message", + { timeout: 15_000 }, + async () => { + const port = await getFreePort(); + const prevPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(port); - const server = await startGatewayServer(port); - const spy = vi.mocked(agentCommand); - spy.mockImplementation(async (opts) => { - const params = opts as { - sessionId?: string; - runId?: string; - extraSystemPrompt?: string; - }; - const sessionId = params.sessionId ?? "test-labeled"; - const runId = params.runId ?? sessionId; - const sessionFile = resolveSessionTranscriptPath(sessionId); - await fs.mkdir(path.dirname(sessionFile), { recursive: true }); + const server = await startGatewayServer(port); + const spy = vi.mocked(agentCommand); + spy.mockImplementation(async (opts) => { + const params = opts as { + sessionId?: string; + runId?: string; + extraSystemPrompt?: string; + }; + const sessionId = params.sessionId ?? "test-labeled"; + const runId = params.runId ?? sessionId; + const sessionFile = resolveSessionTranscriptPath(sessionId); + await fs.mkdir(path.dirname(sessionFile), { recursive: true }); - const startedAt = Date.now(); - emitAgentEvent({ - runId, - stream: "lifecycle", - data: { phase: "start", startedAt }, + const startedAt = Date.now(); + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "start", startedAt }, + }); + + const text = "labeled response"; + const message = { + role: "assistant", + content: [{ type: "text", text }], + }; + await fs.appendFile( + sessionFile, + `${JSON.stringify({ message })}\n`, + "utf8", + ); + + emitAgentEvent({ + runId, + stream: "lifecycle", + data: { phase: "end", startedAt, endedAt: Date.now() }, + }); }); - const text = "labeled response"; - const message = { - role: "assistant", - content: [{ type: "text", text }], - }; - await fs.appendFile( - sessionFile, - `${JSON.stringify({ message })}\n`, - "utf8", - ); + try { + // First, create a session with a label via sessions.patch + const { callGateway } = await import("./call.js"); + await callGateway({ + method: "sessions.patch", + params: { key: "test-labeled-session", label: "my-test-worker" }, + timeoutMs: 5000, + }); - emitAgentEvent({ - runId, - stream: "lifecycle", - data: { phase: "end", startedAt, endedAt: Date.now() }, - }); - }); + const tool = createClawdbotTools().find( + (candidate) => candidate.name === "sessions_send", + ); + if (!tool) throw new Error("missing sessions_send tool"); - try { - // First, create a session with a label via sessions.patch - const { callGateway } = await import("./call.js"); - await callGateway({ - method: "sessions.patch", - params: { key: "test-labeled-session", label: "my-test-worker" }, - timeoutMs: 5000, - }); - - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "sessions_send", - ); - if (!tool) throw new Error("missing sessions_send tool"); - - // Send using label instead of sessionKey - const result = await tool.execute("call-by-label", { - label: "my-test-worker", - message: "hello labeled session", - timeoutSeconds: 5, - }); - const details = result.details as { - status?: string; - reply?: string; - sessionKey?: string; - }; - expect(details.status).toBe("ok"); - expect(details.reply).toBe("labeled response"); - expect(details.sessionKey).toBe("agent:main:test-labeled-session"); - } finally { - if (prevPort === undefined) { - delete process.env.CLAWDBOT_GATEWAY_PORT; - } else { - process.env.CLAWDBOT_GATEWAY_PORT = prevPort; + // Send using label instead of sessionKey + const result = await tool.execute("call-by-label", { + label: "my-test-worker", + message: "hello labeled session", + timeoutSeconds: 5, + }); + const details = result.details as { + status?: string; + reply?: string; + sessionKey?: string; + }; + expect(details.status).toBe("ok"); + expect(details.reply).toBe("labeled response"); + expect(details.sessionKey).toBe("agent:main:test-labeled-session"); + } finally { + if (prevPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevPort; + } + await server.close(); } - await server.close(); - } - }); + }, + ); - it("returns error when label not found", async () => { + it("returns error when label not found", { timeout: 15_000 }, async () => { const port = await getFreePort(); const prevPort = process.env.CLAWDBOT_GATEWAY_PORT; process.env.CLAWDBOT_GATEWAY_PORT = String(port); @@ -215,33 +219,39 @@ describe("sessions_send label lookup", () => { } }); - it("returns error when neither sessionKey nor label provided", async () => { - const port = await getFreePort(); - const prevPort = process.env.CLAWDBOT_GATEWAY_PORT; - process.env.CLAWDBOT_GATEWAY_PORT = String(port); + it( + "returns error when neither sessionKey nor label provided", + { timeout: 15_000 }, + async () => { + const port = await getFreePort(); + const prevPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(port); - const server = await startGatewayServer(port); + const server = await startGatewayServer(port); - try { - const tool = createClawdbotTools().find( - (candidate) => candidate.name === "sessions_send", - ); - if (!tool) throw new Error("missing sessions_send tool"); + try { + const tool = createClawdbotTools().find( + (candidate) => candidate.name === "sessions_send", + ); + if (!tool) throw new Error("missing sessions_send tool"); - const result = await tool.execute("call-no-key", { - message: "hello", - timeoutSeconds: 5, - }); - const details = result.details as { status?: string; error?: string }; - expect(details.status).toBe("error"); - expect(details.error).toContain("Either sessionKey or label is required"); - } finally { - if (prevPort === undefined) { - delete process.env.CLAWDBOT_GATEWAY_PORT; - } else { - process.env.CLAWDBOT_GATEWAY_PORT = prevPort; + const result = await tool.execute("call-no-key", { + message: "hello", + timeoutSeconds: 5, + }); + const details = result.details as { status?: string; error?: string }; + expect(details.status).toBe("error"); + expect(details.error).toContain( + "Either sessionKey or label is required", + ); + } finally { + if (prevPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevPort; + } + await server.close(); } - await server.close(); - } - }); + }, + ); });