@smithers-orchestrator/cli 0.16.8 → 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/package.json +21 -12
- package/src/InitWorkflowPackOptions.ts +2 -0
- package/src/InitWorkflowPackResult.ts +8 -2
- package/src/agent-commands/agentAddWizard.js +190 -0
- package/src/agent-commands/regenerateAgentsTsIfPresent.js +28 -0
- package/src/agent-commands/runAgentAdd.js +147 -0
- package/src/agent-detection.js +214 -17
- package/src/hijack-session.js +1 -1
- package/src/index.js +527 -24
- package/src/mcp/semantic-tools.js +1 -1
- package/src/mdx-plugin.js +6 -0
- package/src/output.js +52 -0
- package/src/smithersRuntime.js +2 -1
- package/src/util/logger.ts +97 -0
- package/src/workflow-pack.js +151 -16
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 "
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -91,7 +96,7 @@ async function loadWorkflowDb(workflowPath) {
|
|
|
91
96
|
*/
|
|
92
97
|
function readPackageVersion() {
|
|
93
98
|
try {
|
|
94
|
-
const pkgUrl = new URL("
|
|
99
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
95
100
|
const raw = readFileSync(pkgUrl, "utf8");
|
|
96
101
|
const parsed = JSON.parse(raw);
|
|
97
102
|
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
@@ -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
|
|
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
|
-
|
|
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).
|