@smithers-orchestrator/cli 0.16.9 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env bun
2
+ import { setJsonMode } from "./util/logger.ts";
2
3
  import { resolve, dirname, basename } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
- import { readFileSync, existsSync, openSync } from "node:fs";
5
+ import { readFileSync, existsSync, openSync, statSync, mkdirSync, writeFileSync } from "node:fs";
5
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
7
  import { Effect, Fiber } from "effect";
7
8
  import { Cli, Mcp as IncurMcp, z } from "incur";
8
9
  import { isRunHeartbeatFresh, runWorkflow, renderFrame, resolveSchema } from "@smithers-orchestrator/engine";
9
- import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";
10
+ import { mdxPlugin } from "./mdx-plugin.js";
10
11
  import { approveNode, denyNode } from "@smithers-orchestrator/engine/approvals";
11
12
  import { signalRun } from "@smithers-orchestrator/engine/signals";
12
13
  import { loadInput, loadOutputs } from "@smithers-orchestrator/db/snapshot";
@@ -34,6 +35,9 @@ import { EVENT_CATEGORY_VALUES, eventTypesForCategory, normalizeEventCategory, }
34
35
  import { aggregateNodeDetailEffect, renderNodeDetailHuman, } from "./node-detail.js";
35
36
  import { diagnoseRunEffect, diagnosisCtaCommands, renderWhyDiagnosisHuman, } from "./why-diagnosis.js";
36
37
  import { detectAvailableAgents } from "./agent-detection.js";
38
+ import { listAccounts, removeAccount } from "@smithers-orchestrator/accounts";
39
+ import { runAgentAdd, pingAccount } from "./agent-commands/runAgentAdd.js";
40
+ import { agentAddWizard } from "./agent-commands/agentAddWizard.js";
37
41
  import { initWorkflowPack, getWorkflowFollowUpCtas } from "./workflow-pack.js";
38
42
  import { discoverWorkflows, resolveWorkflow, createWorkflowFile } from "./workflows.js";
39
43
  import { ask } from "./ask.js";
@@ -45,6 +49,7 @@ import { WATCH_MIN_INTERVAL_MS, runWatchLoop, watchIntervalSecondsToMs, } from "
45
49
  import { createSemanticMcpServer } from "./mcp/semantic-server.js";
46
50
  import pc from "picocolors";
47
51
  import crypto from "node:crypto";
52
+ import React from "react";
48
53
  // ---------------------------------------------------------------------------
49
54
  // Helpers
50
55
  // ---------------------------------------------------------------------------
@@ -100,6 +105,39 @@ function readPackageVersion() {
100
105
  return "unknown";
101
106
  }
102
107
  }
108
+ function smithersTokenStorePath() {
109
+ return process.env.SMITHERS_TOKEN_STORE ?? resolve(process.env.HOME ?? process.cwd(), ".smithers", "tokens.json");
110
+ }
111
+ function readSmithersTokenStore() {
112
+ const path = smithersTokenStorePath();
113
+ if (!existsSync(path)) {
114
+ return { tokens: {} };
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
118
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
119
+ return { tokens: {} };
120
+ }
121
+ const tokens = parsed.tokens && typeof parsed.tokens === "object" && !Array.isArray(parsed.tokens)
122
+ ? parsed.tokens
123
+ : {};
124
+ return { tokens };
125
+ }
126
+ catch {
127
+ return { tokens: {} };
128
+ }
129
+ }
130
+ function writeSmithersTokenStore(store) {
131
+ const path = smithersTokenStorePath();
132
+ mkdirSync(dirname(path), { recursive: true });
133
+ writeFileSync(path, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
134
+ }
135
+ function parseTokenScopes(raw) {
136
+ return raw
137
+ .split(/[,\s]+/)
138
+ .map((scope) => scope.trim())
139
+ .filter(Boolean);
140
+ }
103
141
  const CLI_ARGUMENT_MAX_LENGTH = 4096;
104
142
  const CLI_IDENTIFIER_MAX_LENGTH = 256;
105
143
  const CLI_TEXT_ARGUMENT_MAX_LENGTH = 64 * 1024;
@@ -1242,6 +1280,10 @@ const chatOptions = z.object({
1242
1280
  tail: z.number().int().min(1).optional().describe("Show only the last N chat blocks"),
1243
1281
  stderr: z.boolean().default(true).describe("Include agent stderr output"),
1244
1282
  });
1283
+ const chatCreateOptions = z.object({
1284
+ agent: z.enum(["claude-code", "codex", "gemini"]).describe("CLI agent engine to launch"),
1285
+ cwd: z.string().optional().describe("Working directory for the chat session (default: current directory)"),
1286
+ });
1245
1287
  const inspectArgs = z.object({
1246
1288
  runId: z.string().describe("Run ID to inspect"),
1247
1289
  });
@@ -1320,7 +1362,9 @@ const revertOptions = z.object({
1320
1362
  });
1321
1363
  const initOptions = z.object({
1322
1364
  force: z.boolean().default(false).describe("Overwrite existing scaffold files"),
1365
+ agentsOnly: z.boolean().default(false).describe("Only create .smithers/agents/ and leave the rest of the workflow pack untouched"),
1323
1366
  install: z.boolean().default(true).describe("Run `bun install` inside .smithers/ after scaffolding (--no-install to skip)"),
1367
+ addAgents: z.boolean().default(false).describe("After scaffolding, launch the interactive `agents add` wizard to register one or more accounts."),
1324
1368
  });
1325
1369
  const workflowPathArgs = z.object({
1326
1370
  name: z.string().describe("Workflow ID"),
@@ -1894,7 +1938,7 @@ const cronCli = Cli.create({
1894
1938
  });
1895
1939
  const agentsCli = Cli.create({
1896
1940
  name: "agents",
1897
- description: "Inspect built-in CLI agent capability registries.",
1941
+ description: "Inspect and register subscriptions and api keys.",
1898
1942
  })
1899
1943
  .command("capabilities", {
1900
1944
  description: "Print a JSON report of the built-in CLI agent capability registries.",
@@ -1911,7 +1955,7 @@ const agentsCli = Cli.create({
1911
1955
  run(c) {
1912
1956
  const report = getCliAgentCapabilityDoctorReport();
1913
1957
  commandExitOverride = report.ok ? 0 : 1;
1914
- if (c.options.json) {
1958
+ if (c.options.json || c.format === "json") {
1915
1959
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
1916
1960
  }
1917
1961
  else {
@@ -1919,6 +1963,128 @@ const agentsCli = Cli.create({
1919
1963
  }
1920
1964
  return c.ok(undefined);
1921
1965
  },
1966
+ })
1967
+ .command("add", {
1968
+ description: "Register a Smithers agent account (interactive wizard, or non-interactive via flags).",
1969
+ options: z.object({
1970
+ provider: z.enum([
1971
+ "claude-code", "codex", "gemini", "kimi",
1972
+ "anthropic-api", "openai-api", "gemini-api",
1973
+ ]).optional().describe("Provider id; omit to launch the interactive wizard"),
1974
+ label: z.string().optional().describe("Unique label, e.g. 'claude-work'"),
1975
+ configDir: z.string().optional().describe("Path to the per-account CLI config dir (subscription providers)"),
1976
+ apiKey: z.string().optional().describe("API key (api-key providers only)"),
1977
+ model: z.string().optional().describe("Default model for this account"),
1978
+ skipLogin: z.boolean().default(false).describe("Skip the 'is the dir populated?' check (advanced)"),
1979
+ force: z.boolean().default(false).describe("Register even if no credentials are present"),
1980
+ replace: z.boolean().default(false).describe("Overwrite an existing account with the same label"),
1981
+ loop: z.boolean().default(false).describe("Wizard mode only: keep adding accounts until you say done"),
1982
+ }),
1983
+ async run(c) {
1984
+ // Flag-driven mode: provider+label given → just register.
1985
+ if (c.options.provider && c.options.label) {
1986
+ try {
1987
+ const result = runAgentAdd({
1988
+ provider: c.options.provider,
1989
+ label: c.options.label,
1990
+ configDir: c.options.configDir,
1991
+ apiKey: c.options.apiKey,
1992
+ model: c.options.model,
1993
+ skipLogin: c.options.skipLogin,
1994
+ force: c.options.force,
1995
+ replace: c.options.replace,
1996
+ });
1997
+ if (!result.ok) {
1998
+ const code = result.reason === "login-required" ? 2 : 1;
1999
+ commandExitOverride = code;
2000
+ return c.error({
2001
+ code: result.reason === "login-required"
2002
+ ? "AGENT_LOGIN_REQUIRED"
2003
+ : "AGENT_ADD_FAILED",
2004
+ message: result.detail ?? result.reason,
2005
+ exitCode: code,
2006
+ });
2007
+ }
2008
+ return c.ok({
2009
+ account: result.account,
2010
+ regen: result.regen,
2011
+ });
2012
+ }
2013
+ catch (err) {
2014
+ commandExitOverride = 1;
2015
+ return c.error({
2016
+ code: err?.code ?? "AGENT_ADD_FAILED",
2017
+ message: err?.message ?? String(err),
2018
+ exitCode: 1,
2019
+ });
2020
+ }
2021
+ }
2022
+ // Interactive wizard mode.
2023
+ const labels = await agentAddWizard({ loop: c.options.loop });
2024
+ return c.ok({ added: labels });
2025
+ },
2026
+ })
2027
+ .command("list", {
2028
+ description: "List all registered Smithers agent accounts. Use --format json for machine output.",
2029
+ run(c) {
2030
+ const accounts = listAccounts();
2031
+ if (accounts.length === 0) {
2032
+ process.stderr.write("No accounts registered. Add one with `smithers agents add`.\n");
2033
+ return c.ok({ accounts });
2034
+ }
2035
+ const rows = accounts.map((a) => {
2036
+ const where = a.configDir ?? (a.apiKey ? "(api key set)" : "");
2037
+ return ` ${a.label.padEnd(24)} ${a.provider.padEnd(14)} ${where}`;
2038
+ });
2039
+ process.stderr.write(`Registered accounts (${accounts.length}):\n${rows.join("\n")}\n`);
2040
+ return c.ok({ accounts });
2041
+ },
2042
+ })
2043
+ .command("remove", {
2044
+ description: "Remove a Smithers agent account by label.",
2045
+ args: z.object({ label: z.string().describe("Account label to remove") }),
2046
+ options: z.object({
2047
+ silent: z.boolean().default(false).describe("Do not error if the label is not registered"),
2048
+ }),
2049
+ async run(c) {
2050
+ try {
2051
+ const removed = removeAccount(c.args.label, { silent: c.options.silent });
2052
+ if (removed) {
2053
+ const { regenerateAgentsTsIfPresent } = await import("./agent-commands/regenerateAgentsTsIfPresent.js");
2054
+ const regen = regenerateAgentsTsIfPresent();
2055
+ process.stdout.write(`Removed ${c.args.label}.\n`);
2056
+ return c.ok({ removed: true, label: c.args.label, regen });
2057
+ }
2058
+ return c.ok({ removed: false, label: c.args.label });
2059
+ }
2060
+ catch (err) {
2061
+ commandExitOverride = 1;
2062
+ return c.error({
2063
+ code: err?.code ?? "AGENT_REMOVE_FAILED",
2064
+ message: err?.message ?? String(err),
2065
+ exitCode: 1,
2066
+ });
2067
+ }
2068
+ },
2069
+ })
2070
+ .command("test", {
2071
+ description: "Spawn the account's underlying CLI with --version to verify it is reachable.",
2072
+ args: z.object({ label: z.string().describe("Account label to ping") }),
2073
+ run(c) {
2074
+ const account = listAccounts().find((a) => a.label === c.args.label);
2075
+ if (!account) {
2076
+ commandExitOverride = 1;
2077
+ return c.error({
2078
+ code: "ACCOUNT_NOT_FOUND",
2079
+ message: `No account with label "${c.args.label}" is registered.`,
2080
+ exitCode: 1,
2081
+ });
2082
+ }
2083
+ const ping = pingAccount(account);
2084
+ process.stdout.write(`Ran: ${ping.cmd}\nExit: ${ping.exitCode ?? "<n/a>"}\n`);
2085
+ if (ping.ran && ping.exitCode !== 0) commandExitOverride = 1;
2086
+ return c.ok({ account, ping });
2087
+ },
1922
2088
  });
1923
2089
  // ---------------------------------------------------------------------------
1924
2090
  // OpenAPI subcommand
@@ -1953,6 +2119,79 @@ const openapiCli = Cli.create({
1953
2119
  }
1954
2120
  },
1955
2121
  });
2122
+ const tokenCli = Cli.create({
2123
+ name: "token",
2124
+ description: "Issue and revoke short-lived Gateway bearer tokens.",
2125
+ })
2126
+ .command("issue", {
2127
+ description: "Issue a local short-lived Gateway bearer token grant.",
2128
+ options: z.object({
2129
+ scopes: z.string().default("run:read").describe("Comma or space separated Gateway scopes"),
2130
+ role: z.string().default("operator").describe("Role recorded on the token grant"),
2131
+ userId: z.string().optional().describe("User id recorded on the token grant"),
2132
+ ttl: z.string().default("1h").describe("Token lifetime, such as 15m or 1h"),
2133
+ }),
2134
+ run(c) {
2135
+ const fail = (opts) => {
2136
+ commandExitOverride = opts.exitCode ?? 1;
2137
+ return c.error(opts);
2138
+ };
2139
+ try {
2140
+ const ttlMs = parseDurationMs(c.options.ttl, "ttl");
2141
+ const now = Date.now();
2142
+ const token = `smithers_${crypto.randomBytes(32).toString("base64url")}`;
2143
+ const tokenId = crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
2144
+ const grant = {
2145
+ tokenId,
2146
+ role: c.options.role,
2147
+ scopes: parseTokenScopes(c.options.scopes),
2148
+ ...(c.options.userId ? { userId: c.options.userId } : {}),
2149
+ issuedAtMs: now,
2150
+ expiresAtMs: now + ttlMs,
2151
+ };
2152
+ const store = readSmithersTokenStore();
2153
+ store.tokens[token] = grant;
2154
+ writeSmithersTokenStore(store);
2155
+ return c.ok({
2156
+ token,
2157
+ grant,
2158
+ storePath: smithersTokenStorePath(),
2159
+ });
2160
+ }
2161
+ catch (err) {
2162
+ return fail({
2163
+ code: err instanceof SmithersError ? err.code : "TOKEN_ISSUE_FAILED",
2164
+ message: err?.message ?? String(err),
2165
+ exitCode: 1,
2166
+ });
2167
+ }
2168
+ },
2169
+ })
2170
+ .command("revoke", {
2171
+ description: "Revoke a locally issued Gateway bearer token.",
2172
+ args: z.object({
2173
+ token: z.string().describe("Bearer token to revoke"),
2174
+ }),
2175
+ run(c) {
2176
+ const store = readSmithersTokenStore();
2177
+ const grant = store.tokens[c.args.token];
2178
+ if (!grant) {
2179
+ commandExitOverride = 1;
2180
+ return c.error({
2181
+ code: "TOKEN_NOT_FOUND",
2182
+ message: "Token was not found in the local Smithers token store",
2183
+ exitCode: 1,
2184
+ });
2185
+ }
2186
+ grant.revokedAtMs = Date.now();
2187
+ writeSmithersTokenStore(store);
2188
+ return c.ok({
2189
+ revoked: true,
2190
+ tokenId: grant.tokenId ?? crypto.createHash("sha256").update(c.args.token).digest("hex").slice(0, 16),
2191
+ storePath: smithersTokenStorePath(),
2192
+ });
2193
+ },
2194
+ });
1956
2195
  // ---------------------------------------------------------------------------
1957
2196
  // DevTools live-run commands (tree / diff / output / rewind)
1958
2197
  // ---------------------------------------------------------------------------
@@ -2243,7 +2482,7 @@ const cli = Cli.create({
2243
2482
  .command("init", {
2244
2483
  description: "Install the local Smithers workflow pack into .smithers/.",
2245
2484
  options: initOptions,
2246
- run(c) {
2485
+ async run(c) {
2247
2486
  const fail = (opts) => {
2248
2487
  commandExitOverride = opts.exitCode ?? 1;
2249
2488
  return c.error(opts);
@@ -2251,22 +2490,36 @@ const cli = Cli.create({
2251
2490
  try {
2252
2491
  const result = initWorkflowPack({
2253
2492
  force: c.options.force,
2254
- skipInstall: !c.options.install,
2255
- });
2256
- return c.ok(result, {
2257
- cta: {
2258
- description: "Next steps:",
2259
- commands: c.agent
2260
- ? [
2261
- { command: "workflow list", description: "View all available workflows" },
2262
- { command: "bun install -g smithers", description: "Install smithers globally" },
2263
- ]
2264
- : [
2265
- { command: "tui", description: "Open the interactive dashboard" },
2266
- { command: "bun install -g smithers", description: "Install smithers globally" },
2267
- ],
2268
- },
2493
+ agentsOnly: c.options.agentsOnly,
2494
+ skipInstall: c.options.agentsOnly || !c.options.install,
2269
2495
  });
2496
+ if (c.options.addAgents) {
2497
+ const added = await agentAddWizard({ loop: true });
2498
+ result.addedAccounts = added;
2499
+ // Regenerate agents.ts now that accounts are in place — the
2500
+ // initial generateAgentsTs() call ran before any accounts
2501
+ // existed, so it produced the detection-based file.
2502
+ if (added.length > 0) {
2503
+ const { regenerateAgentsTsIfPresent } = await import("./agent-commands/regenerateAgentsTsIfPresent.js");
2504
+ result.regen = regenerateAgentsTsIfPresent();
2505
+ }
2506
+ }
2507
+ return c.ok(result, c.options.agentsOnly
2508
+ ? undefined
2509
+ : {
2510
+ cta: {
2511
+ description: "Next steps:",
2512
+ commands: c.agent
2513
+ ? [
2514
+ { command: "workflow list", description: "View all available workflows" },
2515
+ { command: "workflow run implement", description: "Run the implementation workflow" },
2516
+ ]
2517
+ : [
2518
+ { command: "tui", description: "Open the interactive dashboard" },
2519
+ { command: "workflow list", description: "View all available workflows" },
2520
+ ],
2521
+ },
2522
+ });
2270
2523
  }
2271
2524
  catch (err) {
2272
2525
  if (err instanceof SmithersError) {
@@ -2922,6 +3175,80 @@ const cli = Cli.create({
2922
3175
  cleanup?.();
2923
3176
  }
2924
3177
  },
3178
+ })
3179
+ // =========================================================================
3180
+ // smithers chat create
3181
+ // =========================================================================
3182
+ .command("chat-create", {
3183
+ description: "Create and start a one-task auto-hijacked chat run.",
3184
+ options: chatCreateOptions,
3185
+ async run(c) {
3186
+ const fail = (opts) => {
3187
+ commandExitOverride = opts.exitCode ?? 1;
3188
+ return c.error(opts);
3189
+ };
3190
+ const chatCwd = resolve(process.cwd(), c.options.cwd ?? ".");
3191
+ if (!existsSync(chatCwd)) {
3192
+ return fail({
3193
+ code: "PATH_NOT_FOUND",
3194
+ message: `Path does not exist: ${chatCwd}`,
3195
+ exitCode: 4,
3196
+ });
3197
+ }
3198
+ if (!statSync(chatCwd).isDirectory()) {
3199
+ return fail({
3200
+ code: "PATH_NOT_DIRECTORY",
3201
+ message: `Path is not a directory: ${chatCwd}`,
3202
+ exitCode: 4,
3203
+ });
3204
+ }
3205
+ try {
3206
+ const workflow = await buildInlineChatWorkflow(c.options.agent, chatCwd);
3207
+ setupSqliteCleanup(workflow);
3208
+ const result = await Effect.runPromise(runWorkflow(workflow, {
3209
+ input: {},
3210
+ rootDir: chatCwd,
3211
+ }));
3212
+ const adapter = new SmithersDb(workflow.db);
3213
+ const candidate = result.runId
3214
+ ? await resolveHijackCandidate(adapter, result.runId, c.options.agent)
3215
+ : null;
3216
+ if (!candidate) {
3217
+ if (result.status === "failed") {
3218
+ return fail({
3219
+ code: result.error?.code ?? "CHAT_CREATE_FAILED",
3220
+ message: result.error?.message ?? `Chat run ${result.runId} failed.`,
3221
+ exitCode: result.error?.code === "TASK_HIJACK_UNSUPPORTED" ? 4 : 1,
3222
+ });
3223
+ }
3224
+ return fail({
3225
+ code: "CHAT_CREATE_UNAVAILABLE",
3226
+ message: `Chat run ${result.runId} did not produce a hijackable ${c.options.agent} session.`,
3227
+ exitCode: 1,
3228
+ });
3229
+ }
3230
+ return c.ok({
3231
+ runId: result.runId,
3232
+ workflowName: "chat",
3233
+ agent: c.options.agent,
3234
+ }, {
3235
+ cta: {
3236
+ description: "Next steps:",
3237
+ commands: [
3238
+ { command: `hijack ${result.runId}`, description: "Open the chat session" },
3239
+ { command: `inspect ${result.runId}`, description: "Inspect run state" },
3240
+ ],
3241
+ },
3242
+ });
3243
+ }
3244
+ catch (err) {
3245
+ return fail({
3246
+ code: err instanceof SmithersError ? err.code : "CHAT_CREATE_FAILED",
3247
+ message: err?.message ?? String(err),
3248
+ exitCode: err instanceof SmithersError ? 4 : 1,
3249
+ });
3250
+ }
3251
+ },
2925
3252
  })
2926
3253
  // =========================================================================
2927
3254
  // smithers hijack <run_id>
@@ -4608,7 +4935,8 @@ const cli = Cli.create({
4608
4935
  .command(cronCli)
4609
4936
  .command(agentsCli)
4610
4937
  .command(memoryCli)
4611
- .command(openapiCli);
4938
+ .command(openapiCli)
4939
+ .command(tokenCli);
4612
4940
  const cliCommands = Cli.toCommands?.get(cli);
4613
4941
  if (!(cliCommands instanceof Map)) {
4614
4942
  throw new Error("Could not resolve Smithers CLI commands for input bounds.");
@@ -4619,8 +4947,8 @@ wrapCliCommandHandlersWithInputBounds(cliCommands);
4619
4947
  // ---------------------------------------------------------------------------
4620
4948
  const KNOWN_COMMANDS = new Set([
4621
4949
  "init", "up", "supervise", "down", "ps", "logs", "events", "chat", "inspect", "node", "why", "approve", "deny",
4622
- "cancel", "graph", "revert", "scores", "observability", "workflow", "ask", "cron",
4623
- "replay", "diff", "fork", "timeline", "memory", "openapi", "agents", "alerts",
4950
+ "cancel", "graph", "revert", "scores", "observability", "workflow", "ask", "cron", "chat-create",
4951
+ "replay", "diff", "fork", "timeline", "memory", "openapi", "token", "agents", "alerts",
4624
4952
  "tree", "output", "rewind", "gui",
4625
4953
  ]);
4626
4954
  /**
@@ -4760,6 +5088,67 @@ function hasHelpFlag(argv, startIndex = 0) {
4760
5088
  }
4761
5089
  return false;
4762
5090
  }
5091
+ /**
5092
+ * @param {string[]} argv
5093
+ */
5094
+ function hasJsonFormatFlag(argv) {
5095
+ for (let index = 0; index < argv.length; index++) {
5096
+ const arg = argv[index];
5097
+ if (arg === "--format") {
5098
+ const value = argv[index + 1];
5099
+ if (value === "json" || value === "jsonl") {
5100
+ return true;
5101
+ }
5102
+ index++;
5103
+ continue;
5104
+ }
5105
+ if (arg === "--format=json" || arg === "--format=jsonl") {
5106
+ return true;
5107
+ }
5108
+ }
5109
+ return false;
5110
+ }
5111
+ /**
5112
+ * @param {string[]} argv
5113
+ * @param {number} startIndex
5114
+ */
5115
+ function hasJsonFlag(argv, startIndex) {
5116
+ for (let index = startIndex; index < argv.length; index++) {
5117
+ const arg = argv[index];
5118
+ if (arg === "--json" || arg === "-j") {
5119
+ return true;
5120
+ }
5121
+ }
5122
+ return false;
5123
+ }
5124
+ /**
5125
+ * @param {string[]} argv
5126
+ */
5127
+ function argvRequestsJsonMode(argv) {
5128
+ const commandIndex = findFirstPositionalIndex(argv);
5129
+ if (commandIndex < 0) {
5130
+ return hasJsonFormatFlag(argv);
5131
+ }
5132
+ const command = argv[commandIndex];
5133
+ if (hasJsonFormatFlag(argv)) {
5134
+ return true;
5135
+ }
5136
+ if (command === "why" ||
5137
+ command === "events" ||
5138
+ command === "inspect" ||
5139
+ command === "node" ||
5140
+ DEVTOOLS_COMMANDS.has(command)) {
5141
+ return hasJsonFlag(argv, commandIndex + 1);
5142
+ }
5143
+ if (command === "agents") {
5144
+ const subcommandIndex = findFirstPositionalIndex(argv, commandIndex + 1);
5145
+ return subcommandIndex >= 0 && argv[subcommandIndex] === "doctor" && hasJsonFlag(argv, subcommandIndex + 1);
5146
+ }
5147
+ if (command === "doctor") {
5148
+ return hasJsonFlag(argv, commandIndex + 1);
5149
+ }
5150
+ return false;
5151
+ }
4763
5152
  /**
4764
5153
  * @param {string[]} argv
4765
5154
  */
@@ -4840,14 +5229,128 @@ function normalizeResumeOption(value) {
4840
5229
  }
4841
5230
  return { resume: true, resumeRunId: normalized };
4842
5231
  }
5232
+ const CHAT_CREATE_PROMPT = [
5233
+ "Start an interactive chat session with the user and help them directly.",
5234
+ "Stay in this conversation until the user is done.",
5235
+ 'When you are completely finished and want to hand control back to Smithers, end your final response with an empty JSON object in a ```json fence: {}.',
5236
+ ].join("\n\n");
5237
+ /**
5238
+ * @param {"claude-code" | "codex" | "gemini"} agentId
5239
+ * @param {string} cwd
5240
+ */
5241
+ async function createChatAgent(agentId, cwd) {
5242
+ switch (agentId) {
5243
+ case "claude-code": {
5244
+ const { ClaudeCodeAgent } = await import("@smithers-orchestrator/agents/ClaudeCodeAgent");
5245
+ return new ClaudeCodeAgent({
5246
+ cwd,
5247
+ model: "claude-opus-4-6",
5248
+ });
5249
+ }
5250
+ case "codex": {
5251
+ const { CodexAgent } = await import("@smithers-orchestrator/agents/CodexAgent");
5252
+ return new CodexAgent({
5253
+ cwd,
5254
+ model: "gpt-5.3-codex",
5255
+ skipGitRepoCheck: true,
5256
+ });
5257
+ }
5258
+ case "gemini": {
5259
+ const { GeminiAgent } = await import("@smithers-orchestrator/agents/GeminiAgent");
5260
+ return new GeminiAgent({
5261
+ cwd,
5262
+ model: "gemini-3.1-pro-preview",
5263
+ });
5264
+ }
5265
+ }
5266
+ }
5267
+ /**
5268
+ * @param {"claude-code" | "codex" | "gemini"} agentId
5269
+ * @param {string} cwd
5270
+ * @returns {Promise<import("@smithers-orchestrator/components/SmithersWorkflow").SmithersWorkflow<any>>}
5271
+ */
5272
+ async function buildInlineChatWorkflow(agentId, cwd) {
5273
+ const [
5274
+ { Database },
5275
+ { drizzle },
5276
+ { sqliteTable, text },
5277
+ { Workflow, Task },
5278
+ { zodToTable },
5279
+ { syncZodTableSchema },
5280
+ { camelToSnake },
5281
+ { z: zod },
5282
+ ] = await Promise.all([
5283
+ import("bun:sqlite"),
5284
+ import("drizzle-orm/bun-sqlite"),
5285
+ import("drizzle-orm/sqlite-core"),
5286
+ import("@smithers-orchestrator/components"),
5287
+ import("@smithers-orchestrator/db/zodToTable"),
5288
+ import("@smithers-orchestrator/db/zodToCreateTableSQL"),
5289
+ import("@smithers-orchestrator/db/utils/camelToSnake"),
5290
+ import("zod"),
5291
+ ]);
5292
+ const agent = await createChatAgent(agentId, cwd);
5293
+ const chatSchema = zod.object({});
5294
+ const inputTable = sqliteTable("input", {
5295
+ runId: text("run_id").primaryKey(),
5296
+ payload: text("payload", { mode: "json" }).$type(),
5297
+ });
5298
+ const chatTableName = camelToSnake("chat");
5299
+ const chatTable = zodToTable(chatTableName, chatSchema);
5300
+ const sqlite = new Database(resolve(cwd, "smithers.db"));
5301
+ sqlite.run("PRAGMA journal_mode = WAL");
5302
+ sqlite.run("PRAGMA busy_timeout = 30000");
5303
+ sqlite.run("PRAGMA synchronous = NORMAL");
5304
+ sqlite.run("PRAGMA locking_mode = NORMAL");
5305
+ sqlite.run("PRAGMA foreign_keys = ON");
5306
+ sqlite.exec(`CREATE TABLE IF NOT EXISTS "input" (run_id TEXT PRIMARY KEY, payload TEXT)`);
5307
+ syncZodTableSchema(sqlite, chatTableName, chatSchema);
5308
+ const db = drizzle(sqlite, { schema: { input: inputTable, chat: chatTable } });
5309
+ const schemaRegistry = new Map([["chat", { table: chatTable, zodSchema: chatSchema }]]);
5310
+ const zodToKeyName = new Map([[chatSchema, "chat"]]);
5311
+ return {
5312
+ db,
5313
+ build: () => React.createElement(Workflow, { name: "chat" }, React.createElement(Task, {
5314
+ id: "chat",
5315
+ output: chatSchema,
5316
+ agent,
5317
+ hijack: true,
5318
+ }, CHAT_CREATE_PROMPT)),
5319
+ opts: {},
5320
+ schemaRegistry,
5321
+ zodToKeyName,
5322
+ };
5323
+ }
5324
+ /**
5325
+ * @param {string[]} argv
5326
+ */
5327
+ function rewriteChatCreateArgv(argv) {
5328
+ const commandIndex = findFirstPositionalIndex(argv);
5329
+ if (commandIndex < 0 || argv[commandIndex] !== "chat") {
5330
+ return argv;
5331
+ }
5332
+ const subcommandIndex = findFirstPositionalIndex(argv, commandIndex + 1);
5333
+ if (subcommandIndex < 0 || argv[subcommandIndex] !== "create") {
5334
+ return argv;
5335
+ }
5336
+ return [
5337
+ ...argv.slice(0, commandIndex),
5338
+ "chat-create",
5339
+ ...argv.slice(subcommandIndex + 1),
5340
+ ];
5341
+ }
4843
5342
  async function main() {
4844
5343
  const rawArgv = process.argv.slice(2);
4845
5344
  let argv = rawArgv.map((arg) => (arg === "-v" ? "--version" : arg));
4846
5345
  argv = rewriteGuiShortcutArgv(argv);
5346
+ argv = rewriteChatCreateArgv(argv);
4847
5347
  argv = rewriteWorkflowCommandArgv(argv);
4848
5348
  argv = rewriteEventsJsonFlagArgv(argv);
4849
5349
  // Finding #3: route `--json` to command-scoped `-j` for devtools commands.
4850
5350
  argv = rewriteDevtoolsJsonFlagArgv(argv);
5351
+ if (argvRequestsJsonMode(argv)) {
5352
+ setJsonMode(true);
5353
+ }
4851
5354
  // Finding #1: pre-validate argv for devtools commands so missing-args
4852
5355
  // / invalid-flag errors go to stderr with exit 1 (not incur's
4853
5356
  // remap-to-4 VALIDATION_ERROR envelope on stdout).
@@ -10,7 +10,7 @@ import { diagnoseRunEffect, } from "../why-diagnosis.js";
10
10
  import { chatAttemptKey, parseChatAttemptMeta, parseNodeOutputEvent, selectChatAttempts, } from "../chat.js";
11
11
  import { WATCH_MIN_INTERVAL_MS } from "../watch.js";
12
12
  import { discoverWorkflows, resolveWorkflow } from "../workflows.js";
13
- import { mdxPlugin } from "smithers-orchestrator/mdx-plugin";
13
+ import { mdxPlugin } from "../mdx-plugin.js";
14
14
  import { approveNode, denyNode } from "@smithers-orchestrator/engine/approvals";
15
15
  import { runWorkflow } from "@smithers-orchestrator/engine";
16
16
  import { revertToAttempt } from "@smithers-orchestrator/time-travel/revert";