@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/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 +526 -23
- 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/agent-detection.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { SmithersError } from "@smithers-orchestrator/errors";
|
|
6
|
+
import { listAccounts } from "@smithers-orchestrator/accounts";
|
|
6
7
|
/** @typedef {import("./AgentAvailability.ts").AgentAvailability} AgentAvailability */
|
|
7
8
|
/** @typedef {import("./AgentAvailabilityStatus.ts").AgentAvailabilityStatus} AgentAvailabilityStatus */
|
|
8
9
|
|
|
@@ -63,10 +64,15 @@ const AGENT_VARIANTS = [
|
|
|
63
64
|
variantId: "claudeSonnet",
|
|
64
65
|
constructor: {
|
|
65
66
|
importName: "ClaudeCodeAgent",
|
|
66
|
-
expr: 'new
|
|
67
|
+
expr: 'new SmithersClaudeCodeAgent({ model: "claude-sonnet-4-6", cwd: process.cwd() })',
|
|
67
68
|
},
|
|
68
69
|
},
|
|
69
70
|
];
|
|
71
|
+
const SCAFFOLDED_PROVIDERS = {
|
|
72
|
+
claude: "ClaudeCodeAgent",
|
|
73
|
+
codex: "CodexAgent",
|
|
74
|
+
gemini: "GeminiAgent",
|
|
75
|
+
};
|
|
70
76
|
const TIER_PREFERENCES = {
|
|
71
77
|
cheapFast: { order: ["kimi", "claudeSonnet", "gemini", "pi"], maxSize: 2 },
|
|
72
78
|
smart: { order: ["codex", "claude", "kimi", "gemini", "amp"], maxSize: 3 },
|
|
@@ -75,27 +81,27 @@ const TIER_PREFERENCES = {
|
|
|
75
81
|
const CONSTRUCTORS = {
|
|
76
82
|
claude: {
|
|
77
83
|
importName: "ClaudeCodeAgent",
|
|
78
|
-
expr: 'new
|
|
84
|
+
expr: 'new SmithersClaudeCodeAgent({ model: "claude-opus-4-6", cwd: process.cwd() })',
|
|
79
85
|
},
|
|
80
86
|
codex: {
|
|
81
87
|
importName: "CodexAgent",
|
|
82
|
-
expr: 'new
|
|
88
|
+
expr: 'new SmithersCodexAgent({ model: "gpt-5.3-codex", cwd: process.cwd(), skipGitRepoCheck: true })',
|
|
83
89
|
},
|
|
84
90
|
gemini: {
|
|
85
91
|
importName: "GeminiAgent",
|
|
86
|
-
expr: 'new
|
|
92
|
+
expr: 'new SmithersGeminiAgent({ model: "gemini-3.1-pro-preview", cwd: process.cwd() })',
|
|
87
93
|
},
|
|
88
94
|
pi: {
|
|
89
95
|
importName: "PiAgent",
|
|
90
|
-
expr: 'new
|
|
96
|
+
expr: 'new SmithersPiAgent({ provider: "openai", model: "gpt-5.3-codex" })',
|
|
91
97
|
},
|
|
92
98
|
kimi: {
|
|
93
99
|
importName: "KimiAgent",
|
|
94
|
-
expr: 'new
|
|
100
|
+
expr: 'new SmithersKimiAgent({ model: "kimi-latest" })',
|
|
95
101
|
},
|
|
96
102
|
amp: {
|
|
97
103
|
importName: "AmpAgent",
|
|
98
|
-
expr: "new
|
|
104
|
+
expr: "new SmithersAmpAgent()",
|
|
99
105
|
},
|
|
100
106
|
};
|
|
101
107
|
/**
|
|
@@ -196,14 +202,175 @@ function resolveRoleAgents(role, available) {
|
|
|
196
202
|
return filtered;
|
|
197
203
|
return fallbackAgents(available);
|
|
198
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Maps an account provider id to the SDK class name that constructs it.
|
|
207
|
+
* @type {Record<string, string>}
|
|
208
|
+
*/
|
|
209
|
+
const ACCOUNT_PROVIDER_CLASSES = {
|
|
210
|
+
"claude-code": "ClaudeCodeAgent",
|
|
211
|
+
"codex": "CodexAgent",
|
|
212
|
+
"gemini": "GeminiAgent",
|
|
213
|
+
"kimi": "KimiAgent",
|
|
214
|
+
"anthropic-api": "ClaudeCodeAgent",
|
|
215
|
+
"openai-api": "CodexAgent",
|
|
216
|
+
"gemini-api": "GeminiAgent",
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Family the account belongs to for pool grouping (e.g. anthropic-api and
|
|
221
|
+
* claude-code both go in the `claude` pool).
|
|
222
|
+
* @type {Record<string, string>}
|
|
223
|
+
*/
|
|
224
|
+
const ACCOUNT_PROVIDER_POOL = {
|
|
225
|
+
"claude-code": "claude",
|
|
226
|
+
"anthropic-api": "claude",
|
|
227
|
+
"codex": "codex",
|
|
228
|
+
"openai-api": "codex",
|
|
229
|
+
"gemini": "gemini",
|
|
230
|
+
"gemini-api": "gemini",
|
|
231
|
+
"kimi": "kimi",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Default model per provider when an account doesn't specify one.
|
|
236
|
+
* @type {Record<string, string>}
|
|
237
|
+
*/
|
|
238
|
+
const ACCOUNT_PROVIDER_DEFAULT_MODEL = {
|
|
239
|
+
"claude-code": "claude-opus-4-7",
|
|
240
|
+
"anthropic-api": "claude-opus-4-7",
|
|
241
|
+
"codex": "gpt-5.4-codex",
|
|
242
|
+
"openai-api": "gpt-5.4-codex",
|
|
243
|
+
"gemini": "gemini-3.1-pro-preview",
|
|
244
|
+
"gemini-api": "gemini-3.1-pro-preview",
|
|
245
|
+
"kimi": "kimi-latest",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {string} label
|
|
250
|
+
* @returns {string}
|
|
251
|
+
*/
|
|
252
|
+
function labelToCamel(label) {
|
|
253
|
+
return label
|
|
254
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map((part, i) => (i === 0 ? part : part[0].toUpperCase() + part.slice(1)))
|
|
257
|
+
.join("");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Renders an absolute path as a JS expression. Paths under $HOME are rewritten
|
|
262
|
+
* to `path.join(homedir(), ...)` so the generated agents.ts is portable across
|
|
263
|
+
* machines (the registry stores absolute paths, but a checked-in agents.ts
|
|
264
|
+
* shouldn't bake in /Users/<name>).
|
|
265
|
+
*
|
|
266
|
+
* @param {string} absPath
|
|
267
|
+
* @param {string} homeDir
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
function pathLiteral(absPath, homeDir) {
|
|
271
|
+
if (homeDir && absPath.startsWith(homeDir + "/")) {
|
|
272
|
+
const rel = absPath.slice(homeDir.length + 1);
|
|
273
|
+
return `path.join(homedir(), ${JSON.stringify(rel)})`;
|
|
274
|
+
}
|
|
275
|
+
if (absPath === homeDir) {
|
|
276
|
+
return "homedir()";
|
|
277
|
+
}
|
|
278
|
+
return JSON.stringify(absPath);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generates an agents.ts file driven by ~/.smithers/accounts.json. One
|
|
283
|
+
* `providers.<labelCamel>` entry is emitted per registered account; pools
|
|
284
|
+
* group accounts by engine family.
|
|
285
|
+
*
|
|
286
|
+
* @param {import("@smithers-orchestrator/accounts").Account[]} accounts
|
|
287
|
+
* @param {NodeJS.ProcessEnv} env
|
|
288
|
+
* @returns {string}
|
|
289
|
+
*/
|
|
290
|
+
function generateAccountsAgentsTs(accounts, env) {
|
|
291
|
+
const homeDir = env.HOME ?? homedir();
|
|
292
|
+
/** @type {Set<string>} */
|
|
293
|
+
const importNames = new Set();
|
|
294
|
+
for (const account of accounts) {
|
|
295
|
+
const cls = ACCOUNT_PROVIDER_CLASSES[account.provider];
|
|
296
|
+
if (cls) importNames.add(cls);
|
|
297
|
+
}
|
|
298
|
+
const smithersImportSpecifiers = [
|
|
299
|
+
"type AgentLike",
|
|
300
|
+
...[...importNames].map((n) => `${n} as Smithers${n}`),
|
|
301
|
+
];
|
|
302
|
+
const providerLines = accounts.map((account) => renderAccountProviderLine(account, homeDir));
|
|
303
|
+
/** @type {Map<string, string[]>} */
|
|
304
|
+
const poolMembers = new Map();
|
|
305
|
+
for (const account of accounts) {
|
|
306
|
+
const family = ACCOUNT_PROVIDER_POOL[account.provider];
|
|
307
|
+
if (!family) continue;
|
|
308
|
+
const arr = poolMembers.get(family) ?? [];
|
|
309
|
+
arr.push(labelToCamel(account.label));
|
|
310
|
+
poolMembers.set(family, arr);
|
|
311
|
+
}
|
|
312
|
+
const poolLines = [...poolMembers.entries()].map(([family, members]) =>
|
|
313
|
+
` ${family}: [${members.map((m) => `providers.${m}`).join(", ")}],`,
|
|
314
|
+
);
|
|
315
|
+
const allLabels = accounts.map((a) => labelToCamel(a.label));
|
|
316
|
+
poolLines.push(` smart: [${allLabels.map((m) => `providers.${m}`).join(", ")}],`);
|
|
317
|
+
return [
|
|
318
|
+
"// smithers-source: generated",
|
|
319
|
+
"// Source of truth: ~/.smithers/accounts.json (managed via `smithers agent add|list|remove`)",
|
|
320
|
+
'import { homedir } from "node:os";',
|
|
321
|
+
'import path from "node:path";',
|
|
322
|
+
`import { ${smithersImportSpecifiers.join(", ")} } from "smithers-orchestrator";`,
|
|
323
|
+
"",
|
|
324
|
+
"export const providers = {",
|
|
325
|
+
...providerLines,
|
|
326
|
+
"} as const;",
|
|
327
|
+
"",
|
|
328
|
+
"export const agents = {",
|
|
329
|
+
...poolLines,
|
|
330
|
+
"} as const satisfies Record<string, AgentLike[]>;",
|
|
331
|
+
"",
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Renders an account as `<labelCamel>: new SmithersFooAgent({ ... })` for
|
|
337
|
+
* inclusion in the providers map.
|
|
338
|
+
*
|
|
339
|
+
* @param {import("@smithers-orchestrator/accounts").Account} account
|
|
340
|
+
* @param {string} homeDir
|
|
341
|
+
* @returns {string}
|
|
342
|
+
*/
|
|
343
|
+
function renderAccountProviderLine(account, homeDir) {
|
|
344
|
+
const cls = ACCOUNT_PROVIDER_CLASSES[account.provider];
|
|
345
|
+
const camel = labelToCamel(account.label);
|
|
346
|
+
const model = account.model ?? ACCOUNT_PROVIDER_DEFAULT_MODEL[account.provider];
|
|
347
|
+
/** @type {string[]} */
|
|
348
|
+
const opts = [];
|
|
349
|
+
if (model) opts.push(`model: ${JSON.stringify(model)}`);
|
|
350
|
+
if (account.configDir) opts.push(`configDir: ${pathLiteral(account.configDir, homeDir)}`);
|
|
351
|
+
if (account.apiKey) opts.push(`apiKey: ${JSON.stringify(account.apiKey)}`);
|
|
352
|
+
if (account.provider === "codex" || account.provider === "openai-api") {
|
|
353
|
+
opts.push("skipGitRepoCheck: true");
|
|
354
|
+
}
|
|
355
|
+
opts.push("cwd: process.cwd()");
|
|
356
|
+
return ` ${camel}: new Smithers${cls}({ ${opts.join(", ")} }),`;
|
|
357
|
+
}
|
|
358
|
+
|
|
199
359
|
/**
|
|
200
360
|
* @param {NodeJS.ProcessEnv} [env]
|
|
201
361
|
*/
|
|
202
362
|
export function generateAgentsTs(env = process.env) {
|
|
363
|
+
const registeredAccounts = listAccounts(env);
|
|
203
364
|
const detections = detectAvailableAgents(env);
|
|
204
365
|
const available = detections.filter((entry) => entry.usable);
|
|
366
|
+
if (available.length === 0 && registeredAccounts.length === 0) {
|
|
367
|
+
throw new SmithersError("NO_USABLE_AGENTS", `No usable agents detected and no accounts registered. Checked: ${detections.flatMap((entry) => entry.checks).join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
// When no agents are detected (e.g. fresh machine with only API keys
|
|
370
|
+
// registered via `smithers agent add`), emit the accounts-only shape with
|
|
371
|
+
// engine-family pools — there's no detection-derived base to merge into.
|
|
205
372
|
if (available.length === 0) {
|
|
206
|
-
|
|
373
|
+
return generateAccountsAgentsTs(registeredAccounts, env);
|
|
207
374
|
}
|
|
208
375
|
// Base providers in detection order
|
|
209
376
|
const orderedProviders = DETECTORS
|
|
@@ -212,16 +379,32 @@ export function generateAgentsTs(env = process.env) {
|
|
|
212
379
|
// Derive variants (e.g. claudeSonnet from claude)
|
|
213
380
|
const availableIds = new Set(orderedProviders.map((p) => p.id));
|
|
214
381
|
const activeVariants = AGENT_VARIANTS.filter((v) => availableIds.has(v.derivedFrom));
|
|
215
|
-
//
|
|
382
|
+
// Smithers SDK class imports needed: detection variants + non-scaffolded
|
|
383
|
+
// detection providers + every account class.
|
|
216
384
|
const importNames = new Set();
|
|
217
|
-
for (const provider of orderedProviders)
|
|
218
|
-
|
|
385
|
+
for (const provider of orderedProviders) {
|
|
386
|
+
if (!(provider.id in SCAFFOLDED_PROVIDERS)) {
|
|
387
|
+
importNames.add(CONSTRUCTORS[provider.id].importName);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
219
390
|
for (const variant of activeVariants)
|
|
220
391
|
importNames.add(variant.constructor.importName);
|
|
221
|
-
|
|
392
|
+
for (const account of registeredAccounts) {
|
|
393
|
+
const cls = ACCOUNT_PROVIDER_CLASSES[account.provider];
|
|
394
|
+
if (cls) importNames.add(cls);
|
|
395
|
+
}
|
|
396
|
+
const smithersImportSpecifiers = [
|
|
397
|
+
"type AgentLike",
|
|
398
|
+
...[...importNames].map((importName) => `${importName} as Smithers${importName}`),
|
|
399
|
+
];
|
|
400
|
+
const homeDir = env.HOME ?? homedir();
|
|
401
|
+
const hasAccounts = registeredAccounts.length > 0;
|
|
402
|
+
// Provider lines: detection base + variants + accounts (additive — `agent
|
|
403
|
+
// add` must never silently delete a previously-emitted provider).
|
|
222
404
|
const providerLines = [
|
|
223
|
-
...orderedProviders.map((provider) => ` ${provider.id}: ${CONSTRUCTORS[provider.id].expr},`),
|
|
405
|
+
...orderedProviders.map((provider) => ` ${provider.id}: ${SCAFFOLDED_PROVIDERS[provider.id] ?? CONSTRUCTORS[provider.id].expr},`),
|
|
224
406
|
...activeVariants.map((variant) => ` ${variant.variantId}: ${variant.constructor.expr},`),
|
|
407
|
+
...registeredAccounts.map((account) => renderAccountProviderLine(account, homeDir)),
|
|
225
408
|
];
|
|
226
409
|
// All known provider/variant IDs for tier resolution
|
|
227
410
|
const allProviderIds = new Set([
|
|
@@ -230,20 +413,34 @@ export function generateAgentsTs(env = process.env) {
|
|
|
230
413
|
]);
|
|
231
414
|
// Fallback: all base provider IDs sorted by score (for tiers with no preferred match)
|
|
232
415
|
const fallbackIds = orderedProviders.map((p) => p.id);
|
|
233
|
-
// Tier lines
|
|
416
|
+
// Tier lines: detection-resolved members, then accounts whose engine
|
|
417
|
+
// family is in the tier's preference order get appended.
|
|
234
418
|
const tierLines = Object.entries(TIER_PREFERENCES).map(([tier, { order, maxSize }]) => {
|
|
235
419
|
let resolved = order
|
|
236
420
|
.filter((id) => allProviderIds.has(id))
|
|
237
421
|
.slice(0, maxSize);
|
|
238
|
-
// Fallback to any available base providers if no preferred agents matched
|
|
239
422
|
if (resolved.length === 0) {
|
|
240
423
|
resolved = fallbackIds.slice(0, maxSize);
|
|
241
424
|
}
|
|
242
|
-
|
|
425
|
+
const tierFamilies = new Set(order);
|
|
426
|
+
const tierAccounts = registeredAccounts
|
|
427
|
+
.filter((account) => tierFamilies.has(ACCOUNT_PROVIDER_POOL[account.provider]))
|
|
428
|
+
.map((account) => labelToCamel(account.label));
|
|
429
|
+
const merged = [...resolved, ...tierAccounts];
|
|
430
|
+
return ` ${tier}: [${merged.map((id) => `providers.${id}`).join(", ")}],`;
|
|
243
431
|
});
|
|
244
432
|
return [
|
|
245
433
|
"// smithers-source: generated",
|
|
246
|
-
|
|
434
|
+
...(hasAccounts ? ["// Account providers (camelCase labels) come from ~/.smithers/accounts.json — managed via `smithers agent add|list|remove`."] : []),
|
|
435
|
+
...(hasAccounts ? ['import { homedir } from "node:os";', 'import path from "node:path";'] : []),
|
|
436
|
+
`import { ${smithersImportSpecifiers.join(", ")} } from "smithers-orchestrator";`,
|
|
437
|
+
'import { ClaudeCodeAgent } from "./agents/claude-code";',
|
|
438
|
+
'import { CodexAgent } from "./agents/codex";',
|
|
439
|
+
'import { GeminiAgent } from "./agents/gemini";',
|
|
440
|
+
"",
|
|
441
|
+
'export { ClaudeCodeAgent } from "./agents/claude-code";',
|
|
442
|
+
'export { CodexAgent } from "./agents/codex";',
|
|
443
|
+
'export { GeminiAgent } from "./agents/gemini";',
|
|
247
444
|
"",
|
|
248
445
|
"export const providers = {",
|
|
249
446
|
...providerLines,
|
package/src/hijack-session.js
CHANGED
|
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { SmithersCtx } from "@smithers-orchestrator/driver";
|
|
6
6
|
import { loadInput, loadOutputs } from "@smithers-orchestrator/db/snapshot";
|
|
7
7
|
import { renderFrame, resolveSchema } from "@smithers-orchestrator/engine";
|
|
8
|
-
import { mdxPlugin } from "
|
|
8
|
+
import { mdxPlugin } from "./mdx-plugin.js";
|
|
9
9
|
import { SmithersError } from "@smithers-orchestrator/errors";
|
|
10
10
|
import { Effect } from "effect";
|
|
11
11
|
/** @typedef {import("./HijackCandidate.ts").HijackCandidate} HijackCandidate */
|