@smithers-orchestrator/agents 0.16.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +65 -0
  3. package/src/AgentLike.ts +28 -0
  4. package/src/AmpAgent.js +232 -0
  5. package/src/AmpAgentOptions.ts +26 -0
  6. package/src/AnthropicAgent.js +54 -0
  7. package/src/AnthropicAgentOptions.ts +8 -0
  8. package/src/BaseCliAgent/AgentCliActionKind.ts +10 -0
  9. package/src/BaseCliAgent/AgentCliEvent.ts +44 -0
  10. package/src/BaseCliAgent/BaseCliAgent.js +874 -0
  11. package/src/BaseCliAgent/BaseCliAgentOptions.ts +13 -0
  12. package/src/BaseCliAgent/CliOutputInterpreter.ts +8 -0
  13. package/src/BaseCliAgent/CliUsageInfo.ts +7 -0
  14. package/src/BaseCliAgent/CodexConfigOverrides.ts +3 -0
  15. package/src/BaseCliAgent/PiExtensionUiRequest.ts +10 -0
  16. package/src/BaseCliAgent/PiExtensionUiResponse.ts +7 -0
  17. package/src/BaseCliAgent/RunCommandResult.ts +5 -0
  18. package/src/BaseCliAgent/buildGenerateResult.js +57 -0
  19. package/src/BaseCliAgent/combineNonEmpty.js +8 -0
  20. package/src/BaseCliAgent/createAgentStdoutTextEmitter.js +198 -0
  21. package/src/BaseCliAgent/extractPrompt.js +88 -0
  22. package/src/BaseCliAgent/extractTextFromJsonValue.js +46 -0
  23. package/src/BaseCliAgent/index.js +32 -0
  24. package/src/BaseCliAgent/normalizeCodexConfig.js +22 -0
  25. package/src/BaseCliAgent/parseHelpers.js +111 -0
  26. package/src/BaseCliAgent/pushFlag.js +18 -0
  27. package/src/BaseCliAgent/pushList.js +10 -0
  28. package/src/BaseCliAgent/resolveTimeouts.js +24 -0
  29. package/src/BaseCliAgent/runCommandEffect.js +32 -0
  30. package/src/BaseCliAgent/runRpcCommandEffect.js +365 -0
  31. package/src/BaseCliAgent/truncateToBytes.js +13 -0
  32. package/src/BaseCliAgent/tryParseJson.js +18 -0
  33. package/src/ClaudeCodeAgent.js +455 -0
  34. package/src/ClaudeCodeAgentOptions.ts +52 -0
  35. package/src/CodexAgent.js +593 -0
  36. package/src/CodexAgentOptions.ts +23 -0
  37. package/src/ForgeAgent.js +128 -0
  38. package/src/ForgeAgentOptions.ts +14 -0
  39. package/src/GeminiAgent.js +273 -0
  40. package/src/GeminiAgentOptions.ts +20 -0
  41. package/src/KimiAgent.js +260 -0
  42. package/src/KimiAgentOptions.ts +21 -0
  43. package/src/OpenAIAgent.js +54 -0
  44. package/src/OpenAIAgentOptions.ts +8 -0
  45. package/src/PiAgent.js +468 -0
  46. package/src/PiAgentOptions.ts +40 -0
  47. package/src/SdkAgentOptions.ts +16 -0
  48. package/src/agent-contract/SmithersAgentContract.ts +10 -0
  49. package/src/agent-contract/SmithersAgentContractTool.ts +8 -0
  50. package/src/agent-contract/SmithersAgentToolCategory.ts +6 -0
  51. package/src/agent-contract/SmithersListedTool.ts +4 -0
  52. package/src/agent-contract/SmithersToolSurface.ts +1 -0
  53. package/src/agent-contract/createSmithersAgentContract.js +188 -0
  54. package/src/agent-contract/index.js +10 -0
  55. package/src/agent-contract/renderSmithersAgentPromptGuidance.js +81 -0
  56. package/src/capability-registry/AgentCapabilityRegistry.ts +22 -0
  57. package/src/capability-registry/AgentToolDescriptor.ts +4 -0
  58. package/src/capability-registry/hashCapabilityRegistry.js +43 -0
  59. package/src/capability-registry/index.js +8 -0
  60. package/src/capability-registry/normalizeCapabilityRegistry.js +52 -0
  61. package/src/capability-registry/normalizeCapabilityStringList.js +9 -0
  62. package/src/cli-capabilities/CliAgentCapabilityAdapterId.ts +6 -0
  63. package/src/cli-capabilities/CliAgentCapabilityDoctorReport.ts +18 -0
  64. package/src/cli-capabilities/CliAgentCapabilityReportEntry.ts +9 -0
  65. package/src/cli-capabilities/formatCliAgentCapabilityDoctorReport.js +24 -0
  66. package/src/cli-capabilities/getCliAgentCapabilityDoctorReport.js +92 -0
  67. package/src/cli-capabilities/getCliAgentCapabilityReport.js +52 -0
  68. package/src/cli-capabilities/index.js +11 -0
  69. package/src/diagnostics/DiagnosticCheck.ts +11 -0
  70. package/src/diagnostics/DiagnosticCheckId.ts +4 -0
  71. package/src/diagnostics/DiagnosticContext.ts +4 -0
  72. package/src/diagnostics/DiagnosticReport.ts +9 -0
  73. package/src/diagnostics/enrichReportWithErrorAnalysis.js +34 -0
  74. package/src/diagnostics/formatDiagnosticSummary.js +17 -0
  75. package/src/diagnostics/getDiagnosticStrategy.js +503 -0
  76. package/src/diagnostics/index.js +13 -0
  77. package/src/diagnostics/launchDiagnostics.js +16 -0
  78. package/src/diagnostics/runDiagnostics.js +52 -0
  79. package/src/index.d.ts +872 -0
  80. package/src/index.js +39 -0
  81. package/src/resolveSdkModel.js +9 -0
  82. package/src/sanitizeForOpenAI.js +47 -0
  83. package/src/streamResultToGenerateResult.js +70 -0
  84. package/src/zodToOpenAISchema.js +16 -0
@@ -0,0 +1,9 @@
1
+ import type { DiagnosticCheck } from "./DiagnosticCheck";
2
+
3
+ export type DiagnosticReport = {
4
+ agentId: string;
5
+ command: string;
6
+ timestamp: string;
7
+ checks: DiagnosticCheck[];
8
+ durationMs: number;
9
+ };
@@ -0,0 +1,34 @@
1
+
2
+ /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
3
+ const RATE_LIMIT_PATTERNS = [
4
+ /rate.?limit/i,
5
+ /\b429\b/,
6
+ /credit balance.*(too low|insufficient|exhausted)/i,
7
+ /overloaded/i,
8
+ /too many requests/i,
9
+ /quota.*(exceeded|exhausted)/i,
10
+ /retry.?after/i,
11
+ ];
12
+ /**
13
+ * @param {DiagnosticReport} report
14
+ * @param {string} errorMessage
15
+ */
16
+ export function enrichReportWithErrorAnalysis(report, errorMessage) {
17
+ if (!errorMessage)
18
+ return;
19
+ const rateLimitCheck = report.checks.find((c) => c.id === "rate_limit_status");
20
+ // Only enrich if the rate limit check was skipped or passed —
21
+ // if it already failed, the pre-flight probe already caught it.
22
+ if (rateLimitCheck && (rateLimitCheck.status === "skip" || rateLimitCheck.status === "pass")) {
23
+ const matched = RATE_LIMIT_PATTERNS.some((p) => p.test(errorMessage));
24
+ if (matched) {
25
+ rateLimitCheck.status = "fail";
26
+ rateLimitCheck.message = `Rate limit detected in error: ${errorMessage.slice(0, 200)}`;
27
+ rateLimitCheck.detail = {
28
+ ...rateLimitCheck.detail,
29
+ detectedPostHoc: true,
30
+ errorExcerpt: errorMessage.slice(0, 500),
31
+ };
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+
2
+ /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
3
+ /**
4
+ * @param {DiagnosticReport} report
5
+ * @returns {string}
6
+ */
7
+ export function formatDiagnosticSummary(report) {
8
+ const failed = report.checks.filter((c) => c.status === "fail");
9
+ const errors = report.checks.filter((c) => c.status === "error");
10
+ if (failed.length === 0 && errors.length === 0) {
11
+ return `[diagnostics] ${report.agentId}: all checks passed (${Math.round(report.durationMs)}ms)`;
12
+ }
13
+ const issues = [...failed, ...errors]
14
+ .map((c) => `${c.id}=${c.status}: ${c.message}`)
15
+ .join("; ");
16
+ return `[diagnostics] ${report.agentId}: ${issues} (${Math.round(report.durationMs)}ms)`;
17
+ }
@@ -0,0 +1,503 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /** @typedef {import("./DiagnosticCheck.ts").DiagnosticCheck} DiagnosticCheck */
3
+ /** @typedef {import("./DiagnosticCheckId.ts").DiagnosticCheckId} DiagnosticCheckId */
4
+ /** @typedef {import("./DiagnosticContext.ts").DiagnosticContext} DiagnosticContext */
5
+
6
+ /**
7
+ * @typedef {{ agentId: string; command: string; checks: DiagnosticCheckDef[]; }} AgentDiagnosticStrategy
8
+ */
9
+ /**
10
+ * @typedef {{ id: DiagnosticCheckId; run: (ctx: DiagnosticContext) => Promise<DiagnosticCheck>; }} DiagnosticCheckDef
11
+ */
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Shared check helpers
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * @param {string} command
18
+ * @param {string} agentId
19
+ * @returns {DiagnosticCheckDef}
20
+ */
21
+ function checkCliInstalled(command, agentId) {
22
+ return {
23
+ id: "cli_installed",
24
+ run: async () => {
25
+ const start = performance.now();
26
+ const result = spawnSync("which", [command], {
27
+ stdio: ["pipe", "pipe", "pipe"],
28
+ });
29
+ const elapsed = performance.now() - start;
30
+ const binaryPath = result.stdout?.toString("utf8").trim();
31
+ if (result.status === 0 && binaryPath) {
32
+ return {
33
+ id: "cli_installed",
34
+ status: "pass",
35
+ message: `${agentId} found at ${binaryPath}`,
36
+ detail: { binaryPath },
37
+ durationMs: elapsed,
38
+ };
39
+ }
40
+ return {
41
+ id: "cli_installed",
42
+ status: "fail",
43
+ message: `${command} not found on PATH`,
44
+ durationMs: elapsed,
45
+ };
46
+ },
47
+ };
48
+ }
49
+ /**
50
+ * @param {string | null} value
51
+ * @returns {number | undefined}
52
+ */
53
+ function parseHeaderInt(value) {
54
+ if (value == null)
55
+ return undefined;
56
+ const n = parseInt(value, 10);
57
+ return Number.isNaN(n) ? undefined : n;
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Claude strategy
61
+ // ---------------------------------------------------------------------------
62
+ const claudeApiKeyCheck = {
63
+ id: "api_key_valid",
64
+ run: async (ctx) => {
65
+ const start = performance.now();
66
+ const apiKey = ctx.env.ANTHROPIC_API_KEY;
67
+ // No API key means subscription mode — valid for Claude Code CLI
68
+ if (!apiKey) {
69
+ return {
70
+ id: "api_key_valid",
71
+ status: "pass",
72
+ message: "No ANTHROPIC_API_KEY set — using subscription mode",
73
+ durationMs: performance.now() - start,
74
+ };
75
+ }
76
+ // Validate key format
77
+ if (!apiKey.startsWith("sk-ant-")) {
78
+ return {
79
+ id: "api_key_valid",
80
+ status: "fail",
81
+ message: "ANTHROPIC_API_KEY has unexpected format (expected sk-ant-* prefix)",
82
+ detail: { prefix: apiKey.slice(0, 7) },
83
+ durationMs: performance.now() - start,
84
+ };
85
+ }
86
+ return {
87
+ id: "api_key_valid",
88
+ status: "pass",
89
+ message: "ANTHROPIC_API_KEY format valid",
90
+ durationMs: performance.now() - start,
91
+ };
92
+ },
93
+ };
94
+ const claudeRateLimitCheck = {
95
+ id: "rate_limit_status",
96
+ run: async (ctx) => {
97
+ const start = performance.now();
98
+ const apiKey = ctx.env.ANTHROPIC_API_KEY;
99
+ if (!apiKey) {
100
+ return {
101
+ id: "rate_limit_status",
102
+ status: "skip",
103
+ message: "Subscription mode — cannot probe rate limits via API",
104
+ durationMs: performance.now() - start,
105
+ };
106
+ }
107
+ try {
108
+ const res = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
109
+ method: "POST",
110
+ headers: {
111
+ "x-api-key": apiKey,
112
+ "anthropic-version": "2023-06-01",
113
+ "content-type": "application/json",
114
+ },
115
+ body: JSON.stringify({
116
+ model: "claude-sonnet-4-20250514",
117
+ messages: [{ role: "user", content: "hi" }],
118
+ }),
119
+ signal: AbortSignal.timeout(4_000),
120
+ });
121
+ const elapsed = performance.now() - start;
122
+ if (res.status === 401) {
123
+ return {
124
+ id: "rate_limit_status",
125
+ status: "fail",
126
+ message: "API key is invalid (401 Unauthorized)",
127
+ durationMs: elapsed,
128
+ };
129
+ }
130
+ if (res.status === 429) {
131
+ const retryAfter = res.headers.get("retry-after");
132
+ return {
133
+ id: "rate_limit_status",
134
+ status: "fail",
135
+ message: `Currently rate limited (429)${retryAfter ? ` — retry after ${retryAfter}s` : ""}`,
136
+ detail: { retryAfter },
137
+ durationMs: elapsed,
138
+ };
139
+ }
140
+ // Parse rate limit headers
141
+ const remaining = {
142
+ requests: parseHeaderInt(res.headers.get("anthropic-ratelimit-requests-remaining")),
143
+ inputTokens: parseHeaderInt(res.headers.get("anthropic-ratelimit-input-tokens-remaining")),
144
+ outputTokens: parseHeaderInt(res.headers.get("anthropic-ratelimit-output-tokens-remaining")),
145
+ };
146
+ const resets = {
147
+ requests: res.headers.get("anthropic-ratelimit-requests-reset"),
148
+ inputTokens: res.headers.get("anthropic-ratelimit-input-tokens-reset"),
149
+ outputTokens: res.headers.get("anthropic-ratelimit-output-tokens-reset"),
150
+ };
151
+ if (remaining.requests === 0 || remaining.inputTokens === 0 || remaining.outputTokens === 0) {
152
+ return {
153
+ id: "rate_limit_status",
154
+ status: "fail",
155
+ message: "Rate limit quota exhausted",
156
+ detail: { remaining, resets },
157
+ durationMs: elapsed,
158
+ };
159
+ }
160
+ return {
161
+ id: "rate_limit_status",
162
+ status: "pass",
163
+ message: "Rate limit OK",
164
+ detail: { remaining, resets },
165
+ durationMs: elapsed,
166
+ };
167
+ }
168
+ catch (err) {
169
+ return {
170
+ id: "rate_limit_status",
171
+ status: "error",
172
+ message: `Rate limit probe failed: ${err instanceof Error ? err.message : String(err)}`,
173
+ durationMs: performance.now() - start,
174
+ };
175
+ }
176
+ },
177
+ };
178
+ const claudeStrategy = {
179
+ agentId: "claude-code",
180
+ command: "claude",
181
+ checks: [
182
+ checkCliInstalled("claude", "Claude Code"),
183
+ claudeApiKeyCheck,
184
+ claudeRateLimitCheck,
185
+ ],
186
+ };
187
+ // ---------------------------------------------------------------------------
188
+ // Codex strategy
189
+ // ---------------------------------------------------------------------------
190
+ // Combined API key validation + rate limit check via GET /v1/models (free, no tokens)
191
+ const codexApiKeyAndRateLimitCheck = [
192
+ {
193
+ id: "api_key_valid",
194
+ run: async (ctx) => {
195
+ const start = performance.now();
196
+ const apiKey = ctx.env.OPENAI_API_KEY;
197
+ if (!apiKey) {
198
+ return {
199
+ id: "api_key_valid",
200
+ status: "fail",
201
+ message: "OPENAI_API_KEY not set",
202
+ durationMs: performance.now() - start,
203
+ };
204
+ }
205
+ try {
206
+ const res = await fetch("https://api.openai.com/v1/models", {
207
+ headers: { Authorization: `Bearer ${apiKey}` },
208
+ signal: AbortSignal.timeout(4_000),
209
+ });
210
+ const elapsed = performance.now() - start;
211
+ if (res.status === 401) {
212
+ return {
213
+ id: "api_key_valid",
214
+ status: "fail",
215
+ message: "OPENAI_API_KEY is invalid (401 Unauthorized)",
216
+ durationMs: elapsed,
217
+ };
218
+ }
219
+ if (res.status === 403) {
220
+ return {
221
+ id: "api_key_valid",
222
+ status: "fail",
223
+ message: "OPENAI_API_KEY lacks permission (403 Forbidden)",
224
+ durationMs: elapsed,
225
+ };
226
+ }
227
+ return {
228
+ id: "api_key_valid",
229
+ status: "pass",
230
+ message: "OPENAI_API_KEY is valid",
231
+ durationMs: elapsed,
232
+ };
233
+ }
234
+ catch (err) {
235
+ return {
236
+ id: "api_key_valid",
237
+ status: "error",
238
+ message: `OpenAI probe failed: ${err instanceof Error ? err.message : String(err)}`,
239
+ durationMs: performance.now() - start,
240
+ };
241
+ }
242
+ },
243
+ },
244
+ {
245
+ id: "rate_limit_status",
246
+ run: async (ctx) => {
247
+ const start = performance.now();
248
+ const apiKey = ctx.env.OPENAI_API_KEY;
249
+ if (!apiKey) {
250
+ return {
251
+ id: "rate_limit_status",
252
+ status: "skip",
253
+ message: "No API key — cannot check rate limits",
254
+ durationMs: 0,
255
+ };
256
+ }
257
+ try {
258
+ const res = await fetch("https://api.openai.com/v1/models", {
259
+ headers: { Authorization: `Bearer ${apiKey}` },
260
+ signal: AbortSignal.timeout(4_000),
261
+ });
262
+ const elapsed = performance.now() - start;
263
+ if (res.status === 429) {
264
+ const retryAfter = res.headers.get("retry-after");
265
+ return {
266
+ id: "rate_limit_status",
267
+ status: "fail",
268
+ message: `Currently rate limited (429)${retryAfter ? ` — retry after ${retryAfter}s` : ""}`,
269
+ detail: { retryAfter },
270
+ durationMs: elapsed,
271
+ };
272
+ }
273
+ // Parse OpenAI rate limit headers if present
274
+ const remaining = {
275
+ requests: parseHeaderInt(res.headers.get("x-ratelimit-remaining-requests")),
276
+ tokens: parseHeaderInt(res.headers.get("x-ratelimit-remaining-tokens")),
277
+ };
278
+ const resets = {
279
+ requests: res.headers.get("x-ratelimit-reset-requests"),
280
+ tokens: res.headers.get("x-ratelimit-reset-tokens"),
281
+ };
282
+ const limits = {
283
+ requests: parseHeaderInt(res.headers.get("x-ratelimit-limit-requests")),
284
+ tokens: parseHeaderInt(res.headers.get("x-ratelimit-limit-tokens")),
285
+ };
286
+ const hasHeaders = remaining.requests !== undefined || remaining.tokens !== undefined;
287
+ if (hasHeaders && (remaining.requests === 0 || remaining.tokens === 0)) {
288
+ return {
289
+ id: "rate_limit_status",
290
+ status: "fail",
291
+ message: "Rate limit quota exhausted",
292
+ detail: { remaining, resets, limits },
293
+ durationMs: elapsed,
294
+ };
295
+ }
296
+ return {
297
+ id: "rate_limit_status",
298
+ status: "pass",
299
+ message: hasHeaders ? "Rate limit OK" : "Rate limit OK (no headers returned)",
300
+ detail: hasHeaders ? { remaining, resets, limits } : undefined,
301
+ durationMs: elapsed,
302
+ };
303
+ }
304
+ catch (err) {
305
+ return {
306
+ id: "rate_limit_status",
307
+ status: "error",
308
+ message: `Rate limit probe failed: ${err instanceof Error ? err.message : String(err)}`,
309
+ durationMs: performance.now() - start,
310
+ };
311
+ }
312
+ },
313
+ },
314
+ ];
315
+ const codexStrategy = {
316
+ agentId: "codex",
317
+ command: "codex",
318
+ checks: [
319
+ checkCliInstalled("codex", "Codex"),
320
+ ...codexApiKeyAndRateLimitCheck,
321
+ ],
322
+ };
323
+ // ---------------------------------------------------------------------------
324
+ // Gemini strategy
325
+ // ---------------------------------------------------------------------------
326
+ // Validate Google auth via GET /v1beta/models (free, no tokens)
327
+ const googleAuthCheck = {
328
+ id: "api_key_valid",
329
+ run: async (ctx) => {
330
+ const start = performance.now();
331
+ const apiKey = ctx.env.GOOGLE_API_KEY ?? ctx.env.GEMINI_API_KEY;
332
+ if (apiKey) {
333
+ // Probe the models endpoint to validate the key
334
+ try {
335
+ const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models", {
336
+ headers: { "x-goog-api-key": apiKey },
337
+ signal: AbortSignal.timeout(4_000),
338
+ });
339
+ const elapsed = performance.now() - start;
340
+ if (res.status === 400 || res.status === 403) {
341
+ return {
342
+ id: "api_key_valid",
343
+ status: "fail",
344
+ message: `Google API key is invalid (${res.status})`,
345
+ durationMs: elapsed,
346
+ };
347
+ }
348
+ return {
349
+ id: "api_key_valid",
350
+ status: "pass",
351
+ message: "Google API key is valid",
352
+ durationMs: elapsed,
353
+ };
354
+ }
355
+ catch (err) {
356
+ return {
357
+ id: "api_key_valid",
358
+ status: "error",
359
+ message: `Google API probe failed: ${err instanceof Error ? err.message : String(err)}`,
360
+ durationMs: performance.now() - start,
361
+ };
362
+ }
363
+ }
364
+ // No API key — check gcloud auth
365
+ const result = spawnSync("gcloud", ["auth", "print-access-token"], {
366
+ stdio: ["pipe", "pipe", "pipe"],
367
+ timeout: 3_000,
368
+ });
369
+ const elapsed = performance.now() - start;
370
+ if (result.status === 0 && result.stdout?.toString("utf8").trim()) {
371
+ return {
372
+ id: "api_key_valid",
373
+ status: "pass",
374
+ message: "Authenticated via gcloud",
375
+ durationMs: elapsed,
376
+ };
377
+ }
378
+ return {
379
+ id: "api_key_valid",
380
+ status: "fail",
381
+ message: "No GOOGLE_API_KEY/GEMINI_API_KEY set and gcloud auth not configured",
382
+ durationMs: elapsed,
383
+ };
384
+ },
385
+ };
386
+ const googleRateLimitCheck = {
387
+ id: "rate_limit_status",
388
+ run: async (ctx) => {
389
+ const start = performance.now();
390
+ const apiKey = ctx.env.GOOGLE_API_KEY ?? ctx.env.GEMINI_API_KEY;
391
+ if (!apiKey) {
392
+ return {
393
+ id: "rate_limit_status",
394
+ status: "skip",
395
+ message: "gcloud auth mode — cannot probe rate limits via API key",
396
+ durationMs: 0,
397
+ };
398
+ }
399
+ try {
400
+ const res = await fetch("https://generativelanguage.googleapis.com/v1beta/models", {
401
+ headers: { "x-goog-api-key": apiKey },
402
+ signal: AbortSignal.timeout(4_000),
403
+ });
404
+ const elapsed = performance.now() - start;
405
+ if (res.status === 429) {
406
+ const retryAfter = res.headers.get("retry-after");
407
+ return {
408
+ id: "rate_limit_status",
409
+ status: "fail",
410
+ message: `Currently rate limited (429)${retryAfter ? ` — retry after ${retryAfter}s` : ""}`,
411
+ detail: { retryAfter },
412
+ durationMs: elapsed,
413
+ };
414
+ }
415
+ return {
416
+ id: "rate_limit_status",
417
+ status: "pass",
418
+ message: "Rate limit OK",
419
+ durationMs: elapsed,
420
+ };
421
+ }
422
+ catch (err) {
423
+ return {
424
+ id: "rate_limit_status",
425
+ status: "error",
426
+ message: `Rate limit probe failed: ${err instanceof Error ? err.message : String(err)}`,
427
+ durationMs: performance.now() - start,
428
+ };
429
+ }
430
+ },
431
+ };
432
+ const geminiStrategy = {
433
+ agentId: "gemini",
434
+ command: "gemini",
435
+ checks: [
436
+ checkCliInstalled("gemini", "Gemini CLI"),
437
+ googleAuthCheck,
438
+ googleRateLimitCheck,
439
+ ],
440
+ };
441
+ // ---------------------------------------------------------------------------
442
+ // Pi strategy
443
+ // ---------------------------------------------------------------------------
444
+ const piStrategy = {
445
+ agentId: "pi",
446
+ command: "pi",
447
+ checks: [
448
+ checkCliInstalled("pi", "Pi"),
449
+ googleAuthCheck,
450
+ googleRateLimitCheck,
451
+ ],
452
+ };
453
+ // ---------------------------------------------------------------------------
454
+ // Amp strategy
455
+ // ---------------------------------------------------------------------------
456
+ const ampApiKeySkip = {
457
+ id: "api_key_valid",
458
+ run: async () => {
459
+ return {
460
+ id: "api_key_valid",
461
+ status: "skip",
462
+ message: "Amp uses its own auth — skipping API key check",
463
+ durationMs: 0,
464
+ };
465
+ },
466
+ };
467
+ const ampRateLimitSkip = {
468
+ id: "rate_limit_status",
469
+ run: async () => {
470
+ return {
471
+ id: "rate_limit_status",
472
+ status: "skip",
473
+ message: "Amp uses its own auth — skipping rate limit check",
474
+ durationMs: 0,
475
+ };
476
+ },
477
+ };
478
+ const ampStrategy = {
479
+ agentId: "amp",
480
+ command: "amp",
481
+ checks: [
482
+ checkCliInstalled("amp", "Amp"),
483
+ ampApiKeySkip,
484
+ ampRateLimitSkip,
485
+ ],
486
+ };
487
+ // ---------------------------------------------------------------------------
488
+ // Strategy registry
489
+ // ---------------------------------------------------------------------------
490
+ const strategies = {
491
+ claude: claudeStrategy,
492
+ codex: codexStrategy,
493
+ gemini: geminiStrategy,
494
+ pi: piStrategy,
495
+ amp: ampStrategy,
496
+ };
497
+ /**
498
+ * @param {string} command
499
+ * @returns {AgentDiagnosticStrategy | null}
500
+ */
501
+ export function getDiagnosticStrategy(command) {
502
+ return strategies[command] ?? null;
503
+ }
@@ -0,0 +1,13 @@
1
+ // @smithers-type-exports-begin
2
+ /** @typedef {import("./DiagnosticCheck.ts").DiagnosticCheck} DiagnosticCheck */
3
+ /** @typedef {import("./DiagnosticCheckId.ts").DiagnosticCheckId} DiagnosticCheckId */
4
+ /** @typedef {import("./DiagnosticCheck.ts").DiagnosticCheckStatus} DiagnosticCheckStatus */
5
+ /** @typedef {import("./DiagnosticContext.ts").DiagnosticContext} DiagnosticContext */
6
+ /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
7
+ // @smithers-type-exports-end
8
+
9
+ export { runDiagnostics } from "./runDiagnostics.js";
10
+ export { getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
11
+ export { enrichReportWithErrorAnalysis } from "./enrichReportWithErrorAnalysis.js";
12
+ export { formatDiagnosticSummary } from "./formatDiagnosticSummary.js";
13
+ export { launchDiagnostics } from "./launchDiagnostics.js";
@@ -0,0 +1,16 @@
1
+ import { getDiagnosticStrategy } from "./getDiagnosticStrategy.js";
2
+ import { runDiagnostics } from "./runDiagnostics.js";
3
+ /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
4
+
5
+ /**
6
+ * @param {string} command
7
+ * @param {Record<string, string>} env
8
+ * @param {string} cwd
9
+ * @returns {Promise<DiagnosticReport> | null}
10
+ */
11
+ export function launchDiagnostics(command, env, cwd) {
12
+ const strategy = getDiagnosticStrategy(command);
13
+ if (!strategy)
14
+ return null;
15
+ return runDiagnostics(strategy, { env, cwd }).catch(() => null);
16
+ }
@@ -0,0 +1,52 @@
1
+ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
2
+ /** @typedef {import("./DiagnosticCheckId.ts").DiagnosticCheckId} DiagnosticCheckId */
3
+
4
+ /**
5
+ * @typedef {{ agentId: string; command: string; checks: DiagnosticCheckDef[]; }} AgentDiagnosticStrategy
6
+ */
7
+ /** @typedef {import("./DiagnosticCheck.ts").DiagnosticCheck} DiagnosticCheck */
8
+ /**
9
+ * @typedef {{ id: DiagnosticCheckId; run: (ctx: DiagnosticContext) => Promise<DiagnosticCheck>; }} DiagnosticCheckDef
10
+ */
11
+ /** @typedef {import("./DiagnosticContext.ts").DiagnosticContext} DiagnosticContext */
12
+ /** @typedef {import("./DiagnosticReport.ts").DiagnosticReport} DiagnosticReport */
13
+
14
+ const PER_CHECK_TIMEOUT_MS = 5_000;
15
+ /**
16
+ * @param {DiagnosticCheckDef} check
17
+ * @param {DiagnosticContext} ctx
18
+ * @returns {Promise<DiagnosticCheck>}
19
+ */
20
+ async function runCheck(check, ctx) {
21
+ const start = performance.now();
22
+ try {
23
+ return await Promise.race([
24
+ check.run(ctx),
25
+ new Promise((_, reject) => setTimeout(() => reject(new SmithersError("AGENT_DIAGNOSTIC_TIMEOUT", "diagnostic check timed out", { timeoutMs: PER_CHECK_TIMEOUT_MS })), PER_CHECK_TIMEOUT_MS)),
26
+ ]);
27
+ }
28
+ catch (err) {
29
+ return {
30
+ id: check.id,
31
+ status: "error",
32
+ message: err instanceof Error ? err.message : String(err),
33
+ durationMs: performance.now() - start,
34
+ };
35
+ }
36
+ }
37
+ /**
38
+ * @param {AgentDiagnosticStrategy} strategy
39
+ * @param {DiagnosticContext} ctx
40
+ * @returns {Promise<DiagnosticReport>}
41
+ */
42
+ export async function runDiagnostics(strategy, ctx) {
43
+ const start = performance.now();
44
+ const results = await Promise.all(strategy.checks.map((check) => runCheck(check, ctx)));
45
+ return {
46
+ agentId: strategy.agentId,
47
+ command: strategy.command,
48
+ timestamp: new Date().toISOString(),
49
+ checks: results,
50
+ durationMs: performance.now() - start,
51
+ };
52
+ }