@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.
@@ -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 ClaudeCodeAgent({ model: "claude-sonnet-4-6" })',
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 ClaudeCodeAgent({ model: "claude-opus-4-6" })',
84
+ expr: 'new SmithersClaudeCodeAgent({ model: "claude-opus-4-6", cwd: process.cwd() })',
79
85
  },
80
86
  codex: {
81
87
  importName: "CodexAgent",
82
- expr: 'new CodexAgent({ model: "gpt-5.3-codex", skipGitRepoCheck: true })',
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 GeminiAgent({ model: "gemini-3.1-pro-preview" })',
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 PiAgent({ provider: "openai", model: "gpt-5.3-codex" })',
96
+ expr: 'new SmithersPiAgent({ provider: "openai", model: "gpt-5.3-codex" })',
91
97
  },
92
98
  kimi: {
93
99
  importName: "KimiAgent",
94
- expr: 'new KimiAgent({ model: "kimi-latest" })',
100
+ expr: 'new SmithersKimiAgent({ model: "kimi-latest" })',
95
101
  },
96
102
  amp: {
97
103
  importName: "AmpAgent",
98
- expr: "new AmpAgent()",
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
- throw new SmithersError("NO_USABLE_AGENTS", `No usable agents detected. Checked: ${detections.flatMap((entry) => entry.checks).join(", ")}`);
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
- // Collect all import names (dedup)
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
- importNames.add(CONSTRUCTORS[provider.id].importName);
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
- // Provider lines: base + variants
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
- return ` ${tier}: [${resolved.map((id) => `providers.${id}`).join(", ")}],`;
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
- `import { ${[...importNames].join(", ")}, type AgentLike } from "smithers-orchestrator";`,
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,
@@ -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 "smithers-orchestrator/mdx-plugin";
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 */