@smithers-orchestrator/cli 0.20.0 → 0.20.3

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.
Files changed (37) hide show
  1. package/dist/agent-detection.d.ts +20 -3
  2. package/package.json +17 -19
  3. package/src/AgentAvailability.ts +3 -0
  4. package/src/agent-detection.js +226 -14
  5. package/src/ask.js +4 -6
  6. package/src/index.js +89 -45
  7. package/src/workflow-pack.js +48 -10
  8. package/src/tui/app.jsx +0 -139
  9. package/src/tui/app.tsx +0 -5
  10. package/src/tui/components/AskModal.jsx +0 -109
  11. package/src/tui/components/AskModal.tsx +0 -3
  12. package/src/tui/components/AttentionPane.jsx +0 -112
  13. package/src/tui/components/AttentionPane.tsx +0 -6
  14. package/src/tui/components/ChatPane.jsx +0 -57
  15. package/src/tui/components/ChatPane.tsx +0 -7
  16. package/src/tui/components/CronList.jsx +0 -87
  17. package/src/tui/components/CronList.tsx +0 -5
  18. package/src/tui/components/DetailsPane.jsx +0 -96
  19. package/src/tui/components/DetailsPane.tsx +0 -7
  20. package/src/tui/components/FramesPane.jsx +0 -147
  21. package/src/tui/components/FramesPane.tsx +0 -8
  22. package/src/tui/components/LogsPane.jsx +0 -46
  23. package/src/tui/components/LogsPane.tsx +0 -6
  24. package/src/tui/components/MetricsPane.jsx +0 -108
  25. package/src/tui/components/MetricsPane.tsx +0 -5
  26. package/src/tui/components/NodeDetailView.jsx +0 -284
  27. package/src/tui/components/NodeDetailView.tsx +0 -7
  28. package/src/tui/components/NodeInspector.jsx +0 -51
  29. package/src/tui/components/NodeInspector.tsx +0 -7
  30. package/src/tui/components/RunDetailView.jsx +0 -190
  31. package/src/tui/components/RunDetailView.tsx +0 -7
  32. package/src/tui/components/RunsList.jsx +0 -184
  33. package/src/tui/components/RunsList.tsx +0 -7
  34. package/src/tui/components/SqliteBrowser.jsx +0 -131
  35. package/src/tui/components/SqliteBrowser.tsx +0 -5
  36. package/src/tui/components/WorkflowLauncher.jsx +0 -63
  37. package/src/tui/components/WorkflowLauncher.tsx +0 -3
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/cli",
3
- "version": "0.20.0",
4
- "description": "Smithers command-line interface, TUI, MCP server, and local workflow tools",
3
+ "version": "0.20.3",
4
+ "description": "Smithers command-line interface, MCP server, and local workflow tools",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "exports": {
@@ -25,8 +25,6 @@
25
25
  "@effect/workflow": "^0.18.0",
26
26
  "@clack/prompts": "^0.10.1",
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "@opentui/core": "^0.1.100",
29
- "@opentui/react": "^0.1.100",
30
28
  "cron-parser": "^5.5.0",
31
29
  "drizzle-orm": "^0.45.2",
32
30
  "effect": "^3.21.1",
@@ -34,21 +32,21 @@
34
32
  "picocolors": "^1.1.1",
35
33
  "react": "^19.2.5",
36
34
  "zod": "^4.3.6",
37
- "@smithers-orchestrator/agents": "0.20.0",
38
- "@smithers-orchestrator/components": "0.20.0",
39
- "@smithers-orchestrator/db": "0.20.0",
40
- "@smithers-orchestrator/driver": "0.20.0",
41
- "@smithers-orchestrator/engine": "0.20.0",
42
- "@smithers-orchestrator/errors": "0.20.0",
43
- "@smithers-orchestrator/accounts": "0.20.0",
44
- "@smithers-orchestrator/devtools": "0.20.0",
45
- "@smithers-orchestrator/protocol": "0.20.0",
46
- "@smithers-orchestrator/scheduler": "0.20.0",
47
- "@smithers-orchestrator/memory": "0.20.0",
48
- "@smithers-orchestrator/observability": "0.20.0",
49
- "@smithers-orchestrator/server": "0.20.0",
50
- "@smithers-orchestrator/openapi": "0.20.0",
51
- "@smithers-orchestrator/time-travel": "0.20.0"
35
+ "@smithers-orchestrator/accounts": "0.20.3",
36
+ "@smithers-orchestrator/agents": "0.20.3",
37
+ "@smithers-orchestrator/components": "0.20.3",
38
+ "@smithers-orchestrator/db": "0.20.3",
39
+ "@smithers-orchestrator/devtools": "0.20.3",
40
+ "@smithers-orchestrator/driver": "0.20.3",
41
+ "@smithers-orchestrator/engine": "0.20.3",
42
+ "@smithers-orchestrator/errors": "0.20.3",
43
+ "@smithers-orchestrator/memory": "0.20.3",
44
+ "@smithers-orchestrator/observability": "0.20.3",
45
+ "@smithers-orchestrator/openapi": "0.20.3",
46
+ "@smithers-orchestrator/protocol": "0.20.3",
47
+ "@smithers-orchestrator/scheduler": "0.20.3",
48
+ "@smithers-orchestrator/server": "0.20.3",
49
+ "@smithers-orchestrator/time-travel": "0.20.3"
52
50
  },
53
51
  "devDependencies": {
54
52
  "@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 {