@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.
@@ -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): AgentAvailability[];
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): string;
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.19.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/components": "0.19.0",
38
- "@smithers-orchestrator/agents": "0.19.0",
39
- "@smithers-orchestrator/db": "0.19.0",
40
- "@smithers-orchestrator/devtools": "0.19.0",
41
- "@smithers-orchestrator/accounts": "0.19.0",
42
- "@smithers-orchestrator/engine": "0.19.0",
43
- "@smithers-orchestrator/driver": "0.19.0",
44
- "@smithers-orchestrator/errors": "0.19.0",
45
- "@smithers-orchestrator/memory": "0.19.0",
46
- "@smithers-orchestrator/observability": "0.19.0",
47
- "@smithers-orchestrator/openapi": "0.19.0",
48
- "@smithers-orchestrator/protocol": "0.19.0",
49
- "@smithers-orchestrator/scheduler": "0.19.0",
50
- "@smithers-orchestrator/server": "0.19.0",
51
- "@smithers-orchestrator/time-travel": "0.19.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",
@@ -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
  };
@@ -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) => [join(homeDir, ".claude")],
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) => [join(homeDir, ".gemini", "oauth_creds.json")],
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 hasAuthSignal = authSignals.some((signal) => existsSync(signal));
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: scoreStatus(status) > 0,
344
+ usable: unusableReasons.length === 0,
173
345
  checks: [
174
346
  `binary:${detector.binary}:${hasBinary ? "yes" : "no"}`,
175
- ...authSignals.map((signal) => `auth:${signal}:${existsSync(signal) ? "yes" : "no"}`),
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", `No usable agents detected and no accounts registered. Checked: ${detections.flatMap((entry) => entry.checks).join(", ")}`);
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).map(([tier, { order, maxSize }]) => {
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 ` ${tier}: [${merged.map((id) => `providers.${id}`).join(", ")}],`;
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", `No usable agents detected. Checked: ${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", `Agent "${explicit.id}" is not usable. Checked: ${formatAgentChecks(explicit)}`, { agentId: explicit.id });
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
  });
@@ -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: ["node_modules/", "executions/", "runs/", "sandboxes/", "state/", "tmp/", "*.db", "*.sqlite", "dist/", ".DS_Store", ""].join("\n"),
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);