@smithers-orchestrator/cli 0.19.0 → 0.20.1
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/dist/agent-detection.d.ts +20 -3
- package/package.json +16 -16
- package/src/AgentAvailability.ts +3 -0
- package/src/agent-detection.js +226 -14
- package/src/ask.js +4 -6
- package/src/index.js +37 -1
- package/src/workflow-pack.js +33 -4
|
@@ -2,26 +2,43 @@ type AgentAvailabilityStatus$1 = "likely-subscription" | "api-key" | "binary-onl
|
|
|
2
2
|
|
|
3
3
|
type AgentAvailability$1 = {
|
|
4
4
|
id: "claude" | "codex" | "gemini" | "pi" | "kimi" | "amp";
|
|
5
|
+
displayName: string;
|
|
5
6
|
binary: string;
|
|
6
7
|
hasBinary: boolean;
|
|
7
8
|
hasAuthSignal: boolean;
|
|
8
9
|
hasApiKeySignal: boolean;
|
|
10
|
+
hasProjectTrustSignal: boolean;
|
|
9
11
|
status: AgentAvailabilityStatus$1;
|
|
10
12
|
score: number;
|
|
11
13
|
usable: boolean;
|
|
12
14
|
checks: string[];
|
|
15
|
+
unusableReasons: string[];
|
|
13
16
|
};
|
|
14
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {AgentAvailability} agent
|
|
20
|
+
*/
|
|
21
|
+
declare function describeUnavailableAgent(agent: AgentAvailability): string;
|
|
22
|
+
/**
|
|
23
|
+
* @param {AgentAvailability[]} detections
|
|
24
|
+
*/
|
|
25
|
+
declare function formatNoUsableAgentsMessage(detections: AgentAvailability[]): string;
|
|
15
26
|
/**
|
|
16
27
|
* @param {NodeJS.ProcessEnv} [env]
|
|
28
|
+
* @param {{ cwd?: string }} [options]
|
|
17
29
|
* @returns {AgentAvailability[]}
|
|
18
30
|
*/
|
|
19
|
-
declare function detectAvailableAgents(env?: NodeJS.ProcessEnv
|
|
31
|
+
declare function detectAvailableAgents(env?: NodeJS.ProcessEnv, options?: {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
}): AgentAvailability[];
|
|
20
34
|
/**
|
|
21
35
|
* @param {NodeJS.ProcessEnv} [env]
|
|
36
|
+
* @param {{ cwd?: string }} [options]
|
|
22
37
|
*/
|
|
23
|
-
declare function generateAgentsTs(env?: NodeJS.ProcessEnv
|
|
38
|
+
declare function generateAgentsTs(env?: NodeJS.ProcessEnv, options?: {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
}): string;
|
|
24
41
|
type AgentAvailability = AgentAvailability$1;
|
|
25
42
|
type AgentAvailabilityStatus = AgentAvailabilityStatus$1;
|
|
26
43
|
|
|
27
|
-
export { type AgentAvailability, type AgentAvailabilityStatus, detectAvailableAgents, generateAgentsTs };
|
|
44
|
+
export { type AgentAvailability, type AgentAvailabilityStatus, describeUnavailableAgent, detectAvailableAgents, formatNoUsableAgentsMessage, generateAgentsTs };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Smithers command-line interface, TUI, MCP server, and local workflow tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,21 +34,21 @@
|
|
|
34
34
|
"picocolors": "^1.1.1",
|
|
35
35
|
"react": "^19.2.5",
|
|
36
36
|
"zod": "^4.3.6",
|
|
37
|
-
"@smithers-orchestrator/
|
|
38
|
-
"@smithers-orchestrator/agents": "0.
|
|
39
|
-
"@smithers-orchestrator/
|
|
40
|
-
"@smithers-orchestrator/
|
|
41
|
-
"@smithers-orchestrator/
|
|
42
|
-
"@smithers-orchestrator/
|
|
43
|
-
"@smithers-orchestrator/
|
|
44
|
-
"@smithers-orchestrator/errors": "0.
|
|
45
|
-
"@smithers-orchestrator/memory": "0.
|
|
46
|
-
"@smithers-orchestrator/observability": "0.
|
|
47
|
-
"@smithers-orchestrator/openapi": "0.
|
|
48
|
-
"@smithers-orchestrator/protocol": "0.
|
|
49
|
-
"@smithers-orchestrator/scheduler": "0.
|
|
50
|
-
"@smithers-orchestrator/server": "0.
|
|
51
|
-
"@smithers-orchestrator/time-travel": "0.
|
|
37
|
+
"@smithers-orchestrator/accounts": "0.20.1",
|
|
38
|
+
"@smithers-orchestrator/agents": "0.20.1",
|
|
39
|
+
"@smithers-orchestrator/components": "0.20.1",
|
|
40
|
+
"@smithers-orchestrator/db": "0.20.1",
|
|
41
|
+
"@smithers-orchestrator/devtools": "0.20.1",
|
|
42
|
+
"@smithers-orchestrator/driver": "0.20.1",
|
|
43
|
+
"@smithers-orchestrator/engine": "0.20.1",
|
|
44
|
+
"@smithers-orchestrator/errors": "0.20.1",
|
|
45
|
+
"@smithers-orchestrator/memory": "0.20.1",
|
|
46
|
+
"@smithers-orchestrator/observability": "0.20.1",
|
|
47
|
+
"@smithers-orchestrator/openapi": "0.20.1",
|
|
48
|
+
"@smithers-orchestrator/protocol": "0.20.1",
|
|
49
|
+
"@smithers-orchestrator/scheduler": "0.20.1",
|
|
50
|
+
"@smithers-orchestrator/server": "0.20.1",
|
|
51
|
+
"@smithers-orchestrator/time-travel": "0.20.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/bun": "latest",
|
package/src/AgentAvailability.ts
CHANGED
|
@@ -2,12 +2,15 @@ import type { AgentAvailabilityStatus } from "./AgentAvailabilityStatus.ts";
|
|
|
2
2
|
|
|
3
3
|
export type AgentAvailability = {
|
|
4
4
|
id: "claude" | "codex" | "gemini" | "pi" | "kimi" | "amp";
|
|
5
|
+
displayName: string;
|
|
5
6
|
binary: string;
|
|
6
7
|
hasBinary: boolean;
|
|
7
8
|
hasAuthSignal: boolean;
|
|
8
9
|
hasApiKeySignal: boolean;
|
|
10
|
+
hasProjectTrustSignal: boolean;
|
|
9
11
|
status: AgentAvailabilityStatus;
|
|
10
12
|
score: number;
|
|
11
13
|
usable: boolean;
|
|
12
14
|
checks: string[];
|
|
15
|
+
unusableReasons: string[];
|
|
13
16
|
};
|
package/src/agent-detection.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { SmithersError } from "@smithers-orchestrator/errors";
|
|
6
6
|
import { listAccounts } from "@smithers-orchestrator/accounts";
|
|
@@ -10,30 +10,53 @@ import { listAccounts } from "@smithers-orchestrator/accounts";
|
|
|
10
10
|
const DETECTORS = [
|
|
11
11
|
{
|
|
12
12
|
id: "claude",
|
|
13
|
+
displayName: "Claude Code",
|
|
13
14
|
binary: "claude",
|
|
14
|
-
authSignals: (homeDir) => [
|
|
15
|
+
authSignals: (homeDir) => [
|
|
16
|
+
join(homeDir, ".claude", ".credentials.json"),
|
|
17
|
+
join(homeDir, ".claude.json"),
|
|
18
|
+
],
|
|
15
19
|
apiKeys: ["ANTHROPIC_API_KEY"],
|
|
20
|
+
setupHint: "Install the Claude Code CLI and run `claude` then `/login`, or set `ANTHROPIC_API_KEY`.",
|
|
16
21
|
},
|
|
17
22
|
{
|
|
18
23
|
id: "codex",
|
|
24
|
+
displayName: "Codex",
|
|
19
25
|
binary: "codex",
|
|
20
|
-
authSignals: (homeDir) => [join(homeDir, ".codex")],
|
|
26
|
+
authSignals: (homeDir) => [join(homeDir, ".codex", "auth.json")],
|
|
21
27
|
apiKeys: ["OPENAI_API_KEY"],
|
|
28
|
+
setupHint: "Install the Codex CLI and run `codex login`, or set `OPENAI_API_KEY`.",
|
|
22
29
|
},
|
|
23
30
|
{
|
|
24
31
|
id: "gemini",
|
|
32
|
+
displayName: "Gemini",
|
|
25
33
|
binary: "gemini",
|
|
26
|
-
authSignals: (homeDir) =>
|
|
34
|
+
authSignals: (homeDir, env) => {
|
|
35
|
+
const configDir = env.GEMINI_DIR ? resolve(env.GEMINI_DIR) : join(homeDir, ".gemini");
|
|
36
|
+
return [
|
|
37
|
+
join(configDir, "oauth_creds.json"),
|
|
38
|
+
join(configDir, "google_accounts.json"),
|
|
39
|
+
];
|
|
40
|
+
},
|
|
27
41
|
apiKeys: ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
42
|
+
projectTrust: (homeDir, env, cwd) => {
|
|
43
|
+
const configDir = env.GEMINI_DIR ? resolve(env.GEMINI_DIR) : join(homeDir, ".gemini");
|
|
44
|
+
const trustFile = join(configDir, "trustedFolders.json");
|
|
45
|
+
return readGeminiProjectTrust(trustFile, cwd);
|
|
46
|
+
},
|
|
47
|
+
setupHint: "Install the Gemini CLI, authenticate it, and trust this project with Gemini, or set `GEMINI_API_KEY` after installing the CLI.",
|
|
28
48
|
},
|
|
29
49
|
{
|
|
30
50
|
id: "pi",
|
|
51
|
+
displayName: "Pi",
|
|
31
52
|
binary: "pi",
|
|
32
53
|
authSignals: (homeDir) => [join(homeDir, ".pi", "agent", "auth.json")],
|
|
33
54
|
apiKeys: [],
|
|
55
|
+
setupHint: "Install and authenticate the `pi` CLI.",
|
|
34
56
|
},
|
|
35
57
|
{
|
|
36
58
|
id: "kimi",
|
|
59
|
+
displayName: "Kimi",
|
|
37
60
|
binary: "kimi",
|
|
38
61
|
authSignals: (homeDir, env) => {
|
|
39
62
|
const signals = [join(homeDir, ".kimi")];
|
|
@@ -42,12 +65,15 @@ const DETECTORS = [
|
|
|
42
65
|
return signals;
|
|
43
66
|
},
|
|
44
67
|
apiKeys: [],
|
|
68
|
+
setupHint: "Install the Kimi CLI and run `kimi login`.",
|
|
45
69
|
},
|
|
46
70
|
{
|
|
47
71
|
id: "amp",
|
|
72
|
+
displayName: "Amp",
|
|
48
73
|
binary: "amp",
|
|
49
74
|
authSignals: (homeDir) => [join(homeDir, ".amp")],
|
|
50
75
|
apiKeys: [],
|
|
76
|
+
setupHint: "Install and authenticate the `amp` CLI.",
|
|
51
77
|
},
|
|
52
78
|
];
|
|
53
79
|
const ROLE_PREFERENCES = {
|
|
@@ -62,6 +88,7 @@ const AGENT_VARIANTS = [
|
|
|
62
88
|
{
|
|
63
89
|
derivedFrom: "claude",
|
|
64
90
|
variantId: "claudeSonnet",
|
|
91
|
+
displayName: "Claude Sonnet",
|
|
65
92
|
constructor: {
|
|
66
93
|
importName: "ClaudeCodeAgent",
|
|
67
94
|
expr: 'new SmithersClaudeCodeAgent({ model: "claude-sonnet-4-6", cwd: process.cwd() })',
|
|
@@ -104,6 +131,82 @@ const CONSTRUCTORS = {
|
|
|
104
131
|
expr: "new SmithersAmpAgent()",
|
|
105
132
|
},
|
|
106
133
|
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} id
|
|
137
|
+
*/
|
|
138
|
+
function detectorForId(id) {
|
|
139
|
+
return DETECTORS.find((detector) => detector.id === id);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} id
|
|
144
|
+
*/
|
|
145
|
+
function variantForId(id) {
|
|
146
|
+
return AGENT_VARIANTS.find((variant) => variant.variantId === id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} id
|
|
151
|
+
*/
|
|
152
|
+
function baseAgentIdForProviderId(id) {
|
|
153
|
+
return variantForId(id)?.derivedFrom ?? id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} id
|
|
158
|
+
*/
|
|
159
|
+
function displayNameForProviderId(id) {
|
|
160
|
+
return variantForId(id)?.displayName ?? detectorForId(id)?.displayName ?? id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {unknown} value
|
|
165
|
+
* @returns {string[]}
|
|
166
|
+
*/
|
|
167
|
+
function extractTrustedFolderPaths(value) {
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
return value.filter((entry) => typeof entry === "string");
|
|
170
|
+
}
|
|
171
|
+
if (!value || typeof value !== "object") {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return Object.entries(/** @type {Record<string, unknown>} */ (value))
|
|
175
|
+
.filter(([, trustValue]) => trustValue === true || trustValue === "TRUST_FOLDER")
|
|
176
|
+
.map(([path]) => path);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} trustedPath
|
|
181
|
+
* @param {string} cwd
|
|
182
|
+
*/
|
|
183
|
+
function trustedPathMatchesCwd(trustedPath, cwd) {
|
|
184
|
+
const trusted = resolve(trustedPath);
|
|
185
|
+
const current = resolve(cwd);
|
|
186
|
+
return current === trusted || current.startsWith(trusted.endsWith(sep) ? trusted : `${trusted}${sep}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} trustFile
|
|
191
|
+
* @param {string} cwd
|
|
192
|
+
* @returns {{ trusted: boolean; checks: string[] }}
|
|
193
|
+
*/
|
|
194
|
+
function readGeminiProjectTrust(trustFile, cwd) {
|
|
195
|
+
let trusted = false;
|
|
196
|
+
if (existsSync(trustFile)) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(readFileSync(trustFile, "utf8"));
|
|
199
|
+
trusted = extractTrustedFolderPaths(parsed).some((path) => trustedPathMatchesCwd(path, cwd));
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
trusted = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
trusted,
|
|
207
|
+
checks: [`project-trust:${trustFile}:${resolve(cwd)}:${trusted ? "yes" : "no"}`],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
107
210
|
/**
|
|
108
211
|
* @param {string} binary
|
|
109
212
|
* @param {NodeJS.ProcessEnv} env
|
|
@@ -149,32 +252,103 @@ function scoreStatus(status) {
|
|
|
149
252
|
return 0;
|
|
150
253
|
}
|
|
151
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {{ authSignals: (homeDir: string, env: NodeJS.ProcessEnv) => string[]; apiKeys: string[] }} detector
|
|
258
|
+
* @param {string} homeDir
|
|
259
|
+
* @param {NodeJS.ProcessEnv} env
|
|
260
|
+
*/
|
|
261
|
+
function credentialRequirementLabel(detector, homeDir, env) {
|
|
262
|
+
const authSignals = detector.authSignals(homeDir, env);
|
|
263
|
+
const pieces = [
|
|
264
|
+
...authSignals.map((signal) => signal.replace(homeDir, "~")),
|
|
265
|
+
...detector.apiKeys.map((name) => `$${name}`),
|
|
266
|
+
];
|
|
267
|
+
return pieces.length > 0 ? pieces.join(" or ") : "agent credentials";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {AgentAvailability} agent
|
|
272
|
+
*/
|
|
273
|
+
function formatUnusableReasons(agent) {
|
|
274
|
+
return agent.unusableReasons.length > 0
|
|
275
|
+
? agent.unusableReasons.join("; ")
|
|
276
|
+
: "not enough availability signals";
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {AgentAvailability} agent
|
|
281
|
+
*/
|
|
282
|
+
export function describeUnavailableAgent(agent) {
|
|
283
|
+
return `${agent.displayName} is unavailable: ${formatUnusableReasons(agent)}. ${agent.displayName === "Codex"
|
|
284
|
+
? "Recommended setup: install the Codex CLI, run `codex login`, then rerun `smithers init`."
|
|
285
|
+
: "Smithers will use another available agent for this role."}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {AgentAvailability[]} detections
|
|
290
|
+
*/
|
|
291
|
+
export function formatNoUsableAgentsMessage(detections) {
|
|
292
|
+
const summaries = detections
|
|
293
|
+
.map((entry) => `${entry.displayName}: ${entry.usable ? "usable" : formatUnusableReasons(entry)}`)
|
|
294
|
+
.join(" | ");
|
|
295
|
+
return [
|
|
296
|
+
`No usable agents detected. ${summaries}.`,
|
|
297
|
+
`Checked: ${detections.flatMap((entry) => entry.checks).join(", ")}`,
|
|
298
|
+
"Recommended setup: install the Codex CLI, run `codex login`, then rerun `smithers init`.",
|
|
299
|
+
"If you use API billing, make sure `codex` is installed and set `OPENAI_API_KEY`.",
|
|
300
|
+
].join(" ");
|
|
301
|
+
}
|
|
302
|
+
|
|
152
303
|
/**
|
|
153
304
|
* @param {NodeJS.ProcessEnv} [env]
|
|
305
|
+
* @param {{ cwd?: string }} [options]
|
|
154
306
|
* @returns {AgentAvailability[]}
|
|
155
307
|
*/
|
|
156
|
-
export function detectAvailableAgents(env = process.env) {
|
|
308
|
+
export function detectAvailableAgents(env = process.env, options = {}) {
|
|
157
309
|
const homeDir = env.HOME ?? homedir();
|
|
310
|
+
const cwd = options.cwd ?? process.cwd();
|
|
158
311
|
return DETECTORS.map((detector) => {
|
|
159
312
|
const authSignals = detector.authSignals(homeDir, env);
|
|
160
313
|
const hasBinary = commandExists(detector.binary, env);
|
|
161
|
-
const
|
|
314
|
+
const authSignalChecks = authSignals.map((signal) => ({
|
|
315
|
+
signal,
|
|
316
|
+
exists: existsSync(signal),
|
|
317
|
+
}));
|
|
318
|
+
const hasAuthSignal = authSignalChecks.some((check) => check.exists);
|
|
162
319
|
const hasApiKeySignal = detector.apiKeys.some((name) => Boolean(env[name]));
|
|
320
|
+
const projectTrust = detector.projectTrust?.(homeDir, env, cwd) ?? { trusted: true, checks: [] };
|
|
321
|
+
const hasProjectTrustSignal = projectTrust.trusted;
|
|
163
322
|
const status = computeStatus(hasBinary, hasAuthSignal, hasApiKeySignal);
|
|
323
|
+
const hasCredentialSignal = hasAuthSignal || hasApiKeySignal;
|
|
324
|
+
const unusableReasons = [];
|
|
325
|
+
if (!hasBinary) {
|
|
326
|
+
unusableReasons.push(`missing \`${detector.binary}\` on PATH`);
|
|
327
|
+
}
|
|
328
|
+
if (!hasCredentialSignal) {
|
|
329
|
+
unusableReasons.push(`missing credentials (${credentialRequirementLabel(detector, homeDir, env)})`);
|
|
330
|
+
}
|
|
331
|
+
if (!hasProjectTrustSignal) {
|
|
332
|
+
unusableReasons.push("current project is not trusted by Gemini");
|
|
333
|
+
}
|
|
164
334
|
return {
|
|
165
335
|
id: detector.id,
|
|
336
|
+
displayName: detector.displayName,
|
|
166
337
|
binary: detector.binary,
|
|
167
338
|
hasBinary,
|
|
168
339
|
hasAuthSignal,
|
|
169
340
|
hasApiKeySignal,
|
|
341
|
+
hasProjectTrustSignal,
|
|
170
342
|
status,
|
|
171
343
|
score: scoreStatus(status),
|
|
172
|
-
usable:
|
|
344
|
+
usable: unusableReasons.length === 0,
|
|
173
345
|
checks: [
|
|
174
346
|
`binary:${detector.binary}:${hasBinary ? "yes" : "no"}`,
|
|
175
|
-
...
|
|
347
|
+
...authSignalChecks.map((check) => `auth:${check.signal}:${check.exists ? "yes" : "no"}`),
|
|
176
348
|
...detector.apiKeys.map((name) => `env:${name}:${env[name] ? "yes" : "no"}`),
|
|
349
|
+
...projectTrust.checks,
|
|
177
350
|
],
|
|
351
|
+
unusableReasons,
|
|
178
352
|
};
|
|
179
353
|
});
|
|
180
354
|
}
|
|
@@ -356,15 +530,48 @@ function renderAccountProviderLine(account, homeDir) {
|
|
|
356
530
|
return ` ${camel}: new Smithers${cls}({ ${opts.join(", ")} }),`;
|
|
357
531
|
}
|
|
358
532
|
|
|
533
|
+
/**
|
|
534
|
+
* @param {string} tier
|
|
535
|
+
* @param {string[]} order
|
|
536
|
+
* @param {Set<string>} allProviderIds
|
|
537
|
+
* @param {Map<string, AgentAvailability>} detectionsById
|
|
538
|
+
* @returns {string[]}
|
|
539
|
+
*/
|
|
540
|
+
function renderUnavailablePreferenceComments(tier, order, allProviderIds, detectionsById) {
|
|
541
|
+
const firstAvailablePreferredIndex = order.findIndex((id) => allProviderIds.has(id));
|
|
542
|
+
const cutoff = firstAvailablePreferredIndex === -1 ? order.length : firstAvailablePreferredIndex;
|
|
543
|
+
const comments = [];
|
|
544
|
+
for (const providerId of order.slice(0, cutoff)) {
|
|
545
|
+
const baseId = baseAgentIdForProviderId(providerId);
|
|
546
|
+
const detection = detectionsById.get(baseId);
|
|
547
|
+
if (!detection || detection.usable) continue;
|
|
548
|
+
comments.push(` // ${tier}: Smithers would normally suggest ${displayNameForProviderId(providerId)} here, but ${detection.displayName} is not available: ${formatUnusableReasons(detection)}.`);
|
|
549
|
+
}
|
|
550
|
+
return comments;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @param {string} tier
|
|
555
|
+
* @param {string[]} providerIds
|
|
556
|
+
* @param {string[]} comments
|
|
557
|
+
*/
|
|
558
|
+
function renderTierLine(tier, providerIds, comments) {
|
|
559
|
+
return [
|
|
560
|
+
...comments,
|
|
561
|
+
` ${tier}: [${providerIds.map((id) => `providers.${id}`).join(", ")}],`,
|
|
562
|
+
];
|
|
563
|
+
}
|
|
564
|
+
|
|
359
565
|
/**
|
|
360
566
|
* @param {NodeJS.ProcessEnv} [env]
|
|
567
|
+
* @param {{ cwd?: string }} [options]
|
|
361
568
|
*/
|
|
362
|
-
export function generateAgentsTs(env = process.env) {
|
|
569
|
+
export function generateAgentsTs(env = process.env, options = {}) {
|
|
363
570
|
const registeredAccounts = listAccounts(env);
|
|
364
|
-
const detections = detectAvailableAgents(env);
|
|
571
|
+
const detections = detectAvailableAgents(env, options);
|
|
365
572
|
const available = detections.filter((entry) => entry.usable);
|
|
366
573
|
if (available.length === 0 && registeredAccounts.length === 0) {
|
|
367
|
-
throw new SmithersError("NO_USABLE_AGENTS",
|
|
574
|
+
throw new SmithersError("NO_USABLE_AGENTS", formatNoUsableAgentsMessage(detections));
|
|
368
575
|
}
|
|
369
576
|
// When no agents are detected (e.g. fresh machine with only API keys
|
|
370
577
|
// registered via `smithers agent add`), emit the accounts-only shape with
|
|
@@ -411,11 +618,12 @@ export function generateAgentsTs(env = process.env) {
|
|
|
411
618
|
...orderedProviders.map((p) => p.id),
|
|
412
619
|
...activeVariants.map((v) => v.variantId),
|
|
413
620
|
]);
|
|
621
|
+
const detectionsById = new Map(detections.map((entry) => [entry.id, entry]));
|
|
414
622
|
// Fallback: all base provider IDs sorted by score (for tiers with no preferred match)
|
|
415
623
|
const fallbackIds = orderedProviders.map((p) => p.id);
|
|
416
624
|
// Tier lines: detection-resolved members, then accounts whose engine
|
|
417
625
|
// family is in the tier's preference order get appended.
|
|
418
|
-
const tierLines = Object.entries(TIER_PREFERENCES).
|
|
626
|
+
const tierLines = Object.entries(TIER_PREFERENCES).flatMap(([tier, { order, maxSize }]) => {
|
|
419
627
|
let resolved = order
|
|
420
628
|
.filter((id) => allProviderIds.has(id))
|
|
421
629
|
.slice(0, maxSize);
|
|
@@ -427,7 +635,11 @@ export function generateAgentsTs(env = process.env) {
|
|
|
427
635
|
.filter((account) => tierFamilies.has(ACCOUNT_PROVIDER_POOL[account.provider]))
|
|
428
636
|
.map((account) => labelToCamel(account.label));
|
|
429
637
|
const merged = [...resolved, ...tierAccounts];
|
|
430
|
-
return
|
|
638
|
+
return renderTierLine(
|
|
639
|
+
tier,
|
|
640
|
+
merged,
|
|
641
|
+
renderUnavailablePreferenceComments(tier, order, allProviderIds, detectionsById),
|
|
642
|
+
);
|
|
431
643
|
});
|
|
432
644
|
return [
|
|
433
645
|
"// smithers-source: generated",
|
package/src/ask.js
CHANGED
|
@@ -11,7 +11,7 @@ import { KimiAgent } from "@smithers-orchestrator/agents/KimiAgent";
|
|
|
11
11
|
import { PiAgent } from "@smithers-orchestrator/agents/PiAgent";
|
|
12
12
|
import { SmithersError } from "@smithers-orchestrator/errors";
|
|
13
13
|
import { createSmithersAgentContract, renderSmithersAgentPromptGuidance, } from "@smithers-orchestrator/agents/agent-contract";
|
|
14
|
-
import { detectAvailableAgents, } from "./agent-detection.js";
|
|
14
|
+
import { describeUnavailableAgent, detectAvailableAgents, formatNoUsableAgentsMessage, } from "./agent-detection.js";
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {typeof ASK_AGENT_IDS[number]} AskAgentId
|
|
17
17
|
*/
|
|
@@ -193,9 +193,7 @@ function formatAgentChecks(agent) {
|
|
|
193
193
|
* @param {AgentAvailability[]} agents
|
|
194
194
|
*/
|
|
195
195
|
function noUsableAgentError(agents) {
|
|
196
|
-
return new SmithersError("NO_USABLE_AGENTS",
|
|
197
|
-
.map((agent) => `${agent.id} => ${formatAgentChecks(agent)}`)
|
|
198
|
-
.join(" | ")}`);
|
|
196
|
+
return new SmithersError("NO_USABLE_AGENTS", formatNoUsableAgentsMessage(agents));
|
|
199
197
|
}
|
|
200
198
|
/**
|
|
201
199
|
* @param {AgentAvailability[]} agents
|
|
@@ -210,7 +208,7 @@ function selectAgent(agents, options) {
|
|
|
210
208
|
throw new SmithersError("CLI_AGENT_UNSUPPORTED", `Agent "${options.agent}" is not supported for \`smithers ask\`.`, { agentId: options.agent });
|
|
211
209
|
}
|
|
212
210
|
if (!explicit.usable) {
|
|
213
|
-
throw new SmithersError("NO_USABLE_AGENTS",
|
|
211
|
+
throw new SmithersError("NO_USABLE_AGENTS", `${describeUnavailableAgent(explicit)} Checked: ${formatAgentChecks(explicit)}`, { agentId: explicit.id });
|
|
214
212
|
}
|
|
215
213
|
return {
|
|
216
214
|
availability: explicit,
|
|
@@ -404,7 +402,7 @@ function buildAgent(selection, bootstrap, systemPrompt, cwd) {
|
|
|
404
402
|
* @returns {Promise<void>}
|
|
405
403
|
*/
|
|
406
404
|
export async function ask(question, cwd, options = {}) {
|
|
407
|
-
const agents = detectAvailableAgents();
|
|
405
|
+
const agents = detectAvailableAgents(process.env, { cwd });
|
|
408
406
|
if (options.listAgents) {
|
|
409
407
|
let selectedAgentId;
|
|
410
408
|
try {
|
package/src/index.js
CHANGED
|
@@ -259,6 +259,36 @@ function parseJsonInput(raw, label, fail) {
|
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* @param {string | undefined} raw
|
|
264
|
+
* @param {FailFn} fail
|
|
265
|
+
* @returns {Record<string, string | number | boolean> | undefined}
|
|
266
|
+
*/
|
|
267
|
+
function parseAnnotations(raw, fail) {
|
|
268
|
+
const parsed = parseJsonInput(raw, "annotations", fail);
|
|
269
|
+
if (parsed === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
272
|
+
return fail({
|
|
273
|
+
code: "INVALID_ANNOTATIONS",
|
|
274
|
+
message: "Run annotations must be a flat JSON object of string/number/boolean values",
|
|
275
|
+
exitCode: 4,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/** @type {Record<string, string | number | boolean>} */
|
|
279
|
+
const annotations = {};
|
|
280
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
281
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
282
|
+
return fail({
|
|
283
|
+
code: "INVALID_ANNOTATIONS",
|
|
284
|
+
message: `Run annotation ${key} must be a string, number, or boolean`,
|
|
285
|
+
exitCode: 4,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
annotations[key] = /** @type {string | number | boolean} */ (value);
|
|
289
|
+
}
|
|
290
|
+
return annotations;
|
|
291
|
+
}
|
|
262
292
|
/**
|
|
263
293
|
* @param {string | undefined} status
|
|
264
294
|
*/
|
|
@@ -1225,6 +1255,7 @@ const upOptions = z.object({
|
|
|
1225
1255
|
toolTimeoutMs: z.number().int().min(1).optional().describe("Max wall-clock time per tool call in ms"),
|
|
1226
1256
|
hot: z.boolean().default(false).describe("Enable hot module replacement for .tsx workflows"),
|
|
1227
1257
|
input: z.string().optional().describe("Input data as JSON string"),
|
|
1258
|
+
annotations: z.string().optional().describe("Run annotations as a flat JSON object of string/number/boolean values"),
|
|
1228
1259
|
resume: z.union([z.boolean(), z.string()]).default(false).describe("Resume a previous run. Pass true with --run-id, or pass the run ID directly (e.g. --resume <run-id>)"),
|
|
1229
1260
|
force: z.boolean().default(false).describe("Resume even if still marked running"),
|
|
1230
1261
|
resumeClaimOwner: z.string().optional().describe("Internal durable resume claim owner"),
|
|
@@ -1450,6 +1481,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
|
|
|
1450
1481
|
try {
|
|
1451
1482
|
const resolvedWorkflowPath = resolve(process.cwd(), workflowPath);
|
|
1452
1483
|
const input = parseJsonInput(options.input, "input", fail) ?? {};
|
|
1484
|
+
const annotations = parseAnnotations(options.annotations, fail);
|
|
1453
1485
|
const { resume, resumeRunId } = normalizeResumeOption(options.resume);
|
|
1454
1486
|
const runId = options.runId ?? resumeRunId;
|
|
1455
1487
|
// Detached mode: spawn ourselves as a background process
|
|
@@ -1460,6 +1492,8 @@ async function executeUpCommand(c, workflowPath, options, fail) {
|
|
|
1460
1492
|
childArgs.push("--run-id", runId);
|
|
1461
1493
|
if (options.input)
|
|
1462
1494
|
childArgs.push("--input", options.input);
|
|
1495
|
+
if (options.annotations)
|
|
1496
|
+
childArgs.push("--annotations", options.annotations);
|
|
1463
1497
|
if (options.maxConcurrency)
|
|
1464
1498
|
childArgs.push("--max-concurrency", String(options.maxConcurrency));
|
|
1465
1499
|
if (options.root)
|
|
@@ -1647,6 +1681,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
|
|
|
1647
1681
|
maxOutputBytes: options.maxOutputBytes,
|
|
1648
1682
|
toolTimeoutMs: options.toolTimeoutMs,
|
|
1649
1683
|
hot: options.hot,
|
|
1684
|
+
annotations,
|
|
1650
1685
|
onProgress,
|
|
1651
1686
|
signal: abort.signal,
|
|
1652
1687
|
}));
|
|
@@ -1699,6 +1734,7 @@ async function executeUpCommand(c, workflowPath, options, fail) {
|
|
|
1699
1734
|
maxOutputBytes: options.maxOutputBytes,
|
|
1700
1735
|
toolTimeoutMs: options.toolTimeoutMs,
|
|
1701
1736
|
hot: options.hot,
|
|
1737
|
+
annotations,
|
|
1702
1738
|
onProgress,
|
|
1703
1739
|
signal: abort.signal,
|
|
1704
1740
|
}));
|
|
@@ -1819,7 +1855,7 @@ const workflowCli = Cli.create({
|
|
|
1819
1855
|
path: resolve(workflowRoot, "bunfig.toml"),
|
|
1820
1856
|
exists: existsSync(resolve(workflowRoot, "bunfig.toml")),
|
|
1821
1857
|
},
|
|
1822
|
-
agents: detectAvailableAgents(),
|
|
1858
|
+
agents: detectAvailableAgents(process.env, { cwd: process.cwd() }),
|
|
1823
1859
|
});
|
|
1824
1860
|
},
|
|
1825
1861
|
});
|
package/src/workflow-pack.js
CHANGED
|
@@ -3261,13 +3261,42 @@ function renderWorkflows() {
|
|
|
3261
3261
|
/**
|
|
3262
3262
|
* @param {DependencyVersions} versions
|
|
3263
3263
|
* @param {NodeJS.ProcessEnv} env
|
|
3264
|
+
* @param {string} projectRoot
|
|
3264
3265
|
* @returns {TemplateFile[]}
|
|
3265
3266
|
*/
|
|
3266
|
-
function renderTemplateFiles(versions, env) {
|
|
3267
|
+
function renderTemplateFiles(versions, env, projectRoot) {
|
|
3267
3268
|
return [
|
|
3268
3269
|
{
|
|
3269
3270
|
path: ".smithers/.gitignore",
|
|
3270
|
-
contents: [
|
|
3271
|
+
contents: [
|
|
3272
|
+
"# Ephemeral data (never commit)",
|
|
3273
|
+
"node_modules/",
|
|
3274
|
+
"executions/",
|
|
3275
|
+
"runs/",
|
|
3276
|
+
"sandboxes/",
|
|
3277
|
+
"state/",
|
|
3278
|
+
"tmp/",
|
|
3279
|
+
"*.db",
|
|
3280
|
+
"*.sqlite",
|
|
3281
|
+
"*.db-shm",
|
|
3282
|
+
"*.db-wal",
|
|
3283
|
+
"dist/",
|
|
3284
|
+
".DS_Store",
|
|
3285
|
+
"",
|
|
3286
|
+
"# Log files",
|
|
3287
|
+
"*.log",
|
|
3288
|
+
"logs/",
|
|
3289
|
+
""
|
|
3290
|
+
].join("\n"),
|
|
3291
|
+
},
|
|
3292
|
+
{
|
|
3293
|
+
path: ".smithers/workflows/.gitignore",
|
|
3294
|
+
contents: [
|
|
3295
|
+
"# Ignore log files in workflows",
|
|
3296
|
+
"*.log",
|
|
3297
|
+
"run-*.log",
|
|
3298
|
+
""
|
|
3299
|
+
].join("\n"),
|
|
3271
3300
|
},
|
|
3272
3301
|
{
|
|
3273
3302
|
path: ".smithers/package.json",
|
|
@@ -3304,7 +3333,7 @@ function renderTemplateFiles(versions, env) {
|
|
|
3304
3333
|
...renderAgentScaffoldFiles(),
|
|
3305
3334
|
{
|
|
3306
3335
|
path: ".smithers/agents.ts",
|
|
3307
|
-
contents: generateAgentsTs(env),
|
|
3336
|
+
contents: generateAgentsTs(env, { cwd: projectRoot }),
|
|
3308
3337
|
},
|
|
3309
3338
|
{
|
|
3310
3339
|
path: ".smithers/smithers.config.ts",
|
|
@@ -3361,7 +3390,7 @@ export function initWorkflowPack(options = {}) {
|
|
|
3361
3390
|
else {
|
|
3362
3391
|
ensureDir(executionsDir);
|
|
3363
3392
|
}
|
|
3364
|
-
templateFiles = renderTemplateFiles(versions, env);
|
|
3393
|
+
templateFiles = renderTemplateFiles(versions, env, projectRoot);
|
|
3365
3394
|
}
|
|
3366
3395
|
for (const file of templateFiles) {
|
|
3367
3396
|
const absolutePath = resolve(projectRoot, file.path);
|