@orchagent/cli 0.3.33 → 0.3.34

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.
@@ -246,7 +246,8 @@ Paid Agents:
246
246
  }
247
247
  if (isOwner) {
248
248
  // Owner: show free message, no balance check needed
249
- process.stderr.write(`Cost: FREE (author)\n\n`);
249
+ if (!options.json)
250
+ process.stderr.write(`Cost: FREE (author)\n\n`);
250
251
  }
251
252
  else {
252
253
  // Non-owner: check balance
@@ -254,7 +255,8 @@ Paid Agents:
254
255
  pricingInfo = { price_cents: price ?? null };
255
256
  if (!price || price <= 0) {
256
257
  // Price missing or invalid - warn but proceed (server will enforce)
257
- process.stderr.write(`Warning: Pricing data unavailable. The server will verify payment.\n\n`);
258
+ if (!options.json)
259
+ process.stderr.write(`Warning: Pricing data unavailable. The server will verify payment.\n\n`);
258
260
  }
259
261
  else {
260
262
  // Valid price - check balance
@@ -272,11 +274,13 @@ Paid Agents:
272
274
  process.exit(errors_1.ExitCodes.PERMISSION_DENIED);
273
275
  }
274
276
  // Sufficient balance - show cost preview
275
- process.stderr.write(`Cost: $${(price / 100).toFixed(2)}/call\n\n`);
277
+ if (!options.json)
278
+ process.stderr.write(`Cost: $${(price / 100).toFixed(2)}/call\n\n`);
276
279
  }
277
280
  catch (err) {
278
281
  // Balance check failed - warn but proceed (server will enforce)
279
- process.stderr.write(`Warning: Could not verify balance. The server will check payment.\n\n`);
282
+ if (!options.json)
283
+ process.stderr.write(`Warning: Could not verify balance. The server will check payment.\n\n`);
280
284
  }
281
285
  }
282
286
  }
@@ -455,9 +459,9 @@ Paid Agents:
455
459
  sourceLabel = multipart.sourceLabel;
456
460
  }
457
461
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
458
- // Make the API call with a spinner
459
- const spinner = (0, spinner_1.createSpinner)(`Calling ${org}/${parsed.agent}@${parsed.version}...`);
460
- spinner.start();
462
+ // Make the API call with a spinner (suppress in --json mode for clean machine-readable output)
463
+ const spinner = options.json ? null : (0, spinner_1.createSpinner)(`Calling ${org}/${parsed.agent}@${parsed.version}...`);
464
+ spinner?.start();
461
465
  let response;
462
466
  try {
463
467
  response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
@@ -467,7 +471,7 @@ Paid Agents:
467
471
  });
468
472
  }
469
473
  catch (err) {
470
- spinner.fail(`Call failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
474
+ spinner?.fail(`Call failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
471
475
  throw err;
472
476
  }
473
477
  if (!response.ok) {
@@ -485,7 +489,7 @@ Paid Agents:
485
489
  : undefined;
486
490
  // Part 2: Handle 402 Payment Required
487
491
  if (response.status === 402 || errorCode === 'INSUFFICIENT_CREDITS') {
488
- spinner.fail('Insufficient credits');
492
+ spinner?.fail('Insufficient credits');
489
493
  let errorMessage = 'Insufficient credits to call this agent.\n\n';
490
494
  // Use pricing info from pre-call check if available
491
495
  if (pricingInfo?.price_cents) {
@@ -498,22 +502,31 @@ Paid Agents:
498
502
  throw new errors_1.CliError(errorMessage, errors_1.ExitCodes.PERMISSION_DENIED);
499
503
  }
500
504
  if (errorCode === 'LLM_KEY_REQUIRED') {
501
- spinner.fail('LLM key required');
505
+ spinner?.fail('LLM key required');
502
506
  throw new errors_1.CliError('This public agent requires you to provide an LLM key.\n' +
503
507
  'Use --key <key> --provider <provider> or set OPENAI_API_KEY/ANTHROPIC_API_KEY env var.');
504
508
  }
509
+ if (errorCode === 'LLM_RATE_LIMITED') {
510
+ const rateLimitMsg = typeof payload === 'object' && payload
511
+ ? payload.error?.message || 'Rate limit exceeded'
512
+ : 'Rate limit exceeded';
513
+ spinner?.fail('Rate limited by LLM provider');
514
+ throw new errors_1.CliError(rateLimitMsg + '\n\n' +
515
+ 'This is the LLM provider\'s rate limit on your API key, not an OrchAgent limit.\n' +
516
+ 'To switch providers: orch call <agent> --provider <gemini|anthropic|openai>', errors_1.ExitCodes.RATE_LIMITED);
517
+ }
505
518
  const message = typeof payload === 'object' && payload
506
519
  ? payload.error
507
520
  ?.message ||
508
521
  payload.message ||
509
522
  response.statusText
510
523
  : response.statusText;
511
- spinner.fail(`Call failed: ${message}`);
524
+ spinner?.fail(`Call failed: ${message}`);
512
525
  throw new errors_1.CliError(message);
513
526
  }
514
- spinner.succeed(`Called ${org}/${parsed.agent}@${parsed.version}`);
515
- // After successful call, if it was a paid agent, show cost
516
- if ((0, pricing_1.isPaidAgent)(agentMeta) && pricingInfo?.price_cents && pricingInfo.price_cents > 0) {
527
+ spinner?.succeed(`Called ${org}/${parsed.agent}@${parsed.version}`);
528
+ // After successful call, if it was a paid agent, show cost (suppress in --json mode)
529
+ if (!options.json && (0, pricing_1.isPaidAgent)(agentMeta) && pricingInfo?.price_cents && pricingInfo.price_cents > 0) {
517
530
  process.stderr.write(`\nCost: $${(pricingInfo.price_cents / 100).toFixed(2)} USD\n`);
518
531
  }
519
532
  // Track successful call
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractTemplateVariables = extractTemplateVariables;
7
+ exports.deriveInputSchema = deriveInputSchema;
6
8
  exports.registerPublishCommand = registerPublishCommand;
7
9
  const promises_1 = __importDefault(require("fs/promises"));
8
10
  const path_1 = __importDefault(require("path"));
@@ -14,6 +16,44 @@ const api_1 = require("../lib/api");
14
16
  const errors_1 = require("../lib/errors");
15
17
  const analytics_1 = require("../lib/analytics");
16
18
  const bundle_1 = require("../lib/bundle");
19
+ /**
20
+ * Extract template placeholders from a prompt template.
21
+ * Matches double-brace patterns like {{variable}}.
22
+ * Returns unique variable names in order of first appearance.
23
+ */
24
+ function extractTemplateVariables(template) {
25
+ const seen = new Set();
26
+ const result = [];
27
+ // Match double-brace template variables: two opening braces, word chars, two closing braces
28
+ const pattern = /\{\{(\w+)\}\}/g;
29
+ let match;
30
+ while ((match = pattern.exec(template)) !== null) {
31
+ const name = match[1];
32
+ if (!seen.has(name)) {
33
+ seen.add(name);
34
+ result.push(name);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ /**
40
+ * Derive a JSON Schema input object from template variable names.
41
+ * Each variable becomes a required string property.
42
+ */
43
+ function deriveInputSchema(variables) {
44
+ const properties = {};
45
+ for (const name of variables) {
46
+ properties[name] = {
47
+ type: 'string',
48
+ description: `Value for the ${name} template variable`,
49
+ };
50
+ }
51
+ return {
52
+ type: 'object',
53
+ properties,
54
+ required: [...variables],
55
+ };
56
+ }
17
57
  /**
18
58
  * Handle security flagged error response (422 with error: 'content_flagged')
19
59
  * Returns true if the error was handled, false otherwise
@@ -273,6 +313,7 @@ function registerPublishCommand(program) {
273
313
  const price = parseFloat(options.price);
274
314
  process.stdout.write(`Pricing: $${price.toFixed(2)} USD per call\n`);
275
315
  }
316
+ process.stdout.write(`\nView analytics and usage: https://orchagent.io/dashboard\n`);
276
317
  }
277
318
  catch (err) {
278
319
  if (handleSecurityFlaggedError(err)) {
@@ -338,12 +379,14 @@ function registerPublishCommand(program) {
338
379
  // Read schemas
339
380
  let inputSchema;
340
381
  let outputSchema;
382
+ let schemaFromFile = false;
341
383
  const schemaPath = path_1.default.join(cwd, 'schema.json');
342
384
  try {
343
385
  const raw = await promises_1.default.readFile(schemaPath, 'utf-8');
344
386
  const schemas = JSON.parse(raw);
345
387
  inputSchema = schemas.input;
346
388
  outputSchema = schemas.output;
389
+ schemaFromFile = true;
347
390
  }
348
391
  catch (err) {
349
392
  // Schema is optional
@@ -351,6 +394,33 @@ function registerPublishCommand(program) {
351
394
  throw new errors_1.CliError(`Failed to read schema.json: ${err}`);
352
395
  }
353
396
  }
397
+ // For prompt agents, derive input schema from template variables if needed
398
+ if (prompt && (manifest.type === 'prompt' || manifest.type === 'skill')) {
399
+ const templateVars = extractTemplateVariables(prompt);
400
+ if (templateVars.length > 0) {
401
+ if (!schemaFromFile) {
402
+ // No schema.json provided - auto-generate from template variables
403
+ inputSchema = deriveInputSchema(templateVars);
404
+ }
405
+ else if (inputSchema && 'properties' in inputSchema) {
406
+ // schema.json exists - check for mismatches with template variables
407
+ const schemaProps = Object.keys(inputSchema.properties || {});
408
+ const missing = templateVars.filter(v => !schemaProps.includes(v));
409
+ const extra = schemaProps.filter(p => !templateVars.includes(p));
410
+ if (missing.length > 0 || extra.length > 0) {
411
+ const parts = [];
412
+ if (missing.length > 0) {
413
+ parts.push(`template uses {{${missing.join('}}, {{')}}} but schema.json doesn't define ${missing.join(', ')}`);
414
+ }
415
+ if (extra.length > 0) {
416
+ parts.push(`schema.json defines ${extra.join(', ')} but template doesn't use {{${extra.join('}}, {{')}}}`);
417
+ }
418
+ process.stderr.write(chalk_1.default.yellow(`Warning: Schema mismatch - ${parts.join('; ')}.\n`));
419
+ process.stderr.write(chalk_1.default.yellow(` Consider updating schema.json to match your prompt.md template variables.\n`));
420
+ }
421
+ }
422
+ }
423
+ }
354
424
  // For code-based agents, either --url is required OR we bundle the code
355
425
  let agentUrl = options.url;
356
426
  let shouldUploadBundle = false;
@@ -393,10 +463,14 @@ function registerPublishCommand(program) {
393
463
  // Prompt agent validations
394
464
  const promptBytes = prompt ? Buffer.byteLength(prompt, 'utf-8') : 0;
395
465
  process.stderr.write(` ✓ prompt.md found (${promptBytes.toLocaleString()} bytes)\n`);
396
- if (inputSchema || outputSchema) {
466
+ if (schemaFromFile) {
397
467
  const schemaTypes = [inputSchema ? 'input' : null, outputSchema ? 'output' : null].filter(Boolean).join(' + ');
398
468
  process.stderr.write(` ✓ schema.json found (${schemaTypes} schemas)\n`);
399
469
  }
470
+ else if (inputSchema) {
471
+ const vars = prompt ? extractTemplateVariables(prompt) : [];
472
+ process.stderr.write(` ✓ Input schema derived from template variables: ${vars.join(', ')}\n`);
473
+ }
400
474
  }
401
475
  else if (manifest.type === 'code') {
402
476
  // Code agent validations
@@ -583,5 +657,6 @@ function registerPublishCommand(program) {
583
657
  if (shouldUploadBundle) {
584
658
  process.stdout.write(`\nNote: Hosted code execution is in beta. Contact support for full deployment.\n`);
585
659
  }
660
+ process.stdout.write(`\nView analytics and usage: https://orchagent.io/dashboard\n`);
586
661
  });
587
662
  }
@@ -16,6 +16,7 @@ function registerSearchCommand(program) {
16
16
  .option('--recent', 'Show most recently published')
17
17
  .option('--mine', 'Show only your own agents (including private)')
18
18
  .option('--type <type>', 'Filter by type: agents, skills, code, prompt, skill, all (default: all)', 'all')
19
+ .option('--tags <tags>', 'Filter by tags (comma-separated, e.g., security,devops)')
19
20
  .option('--limit <n>', `Max results (default: ${DEFAULT_LIMIT})`, String(DEFAULT_LIMIT))
20
21
  .option('--free', 'Show only free agents')
21
22
  .option('--paid', 'Show only paid agents')
@@ -25,6 +26,9 @@ Pricing Filters:
25
26
  --free Show only free agents
26
27
  --paid Show only paid agents
27
28
 
29
+ Tag Filters:
30
+ --tags security,devops Show agents matching any of these tags
31
+
28
32
  Ownership Filters:
29
33
  --mine Show your own agents (public and private). Requires login.
30
34
  `)
@@ -34,6 +38,7 @@ Ownership Filters:
34
38
  // Map type filter for API (null means no filter)
35
39
  const typeFilter = options.type === 'all' ? undefined : options.type;
36
40
  const sort = options.popular ? 'stars' : options.recent ? 'recent' : undefined;
41
+ const tags = options.tags ? options.tags.split(',').map(t => t.trim()).filter(Boolean) : undefined;
37
42
  let agents;
38
43
  if (options.mine) {
39
44
  // --mine: search within user's own agents (public + private)
@@ -51,12 +56,12 @@ Ownership Filters:
51
56
  options.popular = true;
52
57
  }
53
58
  if (query) {
54
- agents = await (0, api_1.searchAgents)(config, query, { sort, type: typeFilter });
55
- await (0, analytics_1.track)('cli_search', { query, type: options.type });
59
+ agents = await (0, api_1.searchAgents)(config, query, { sort, tags, type: typeFilter });
60
+ await (0, analytics_1.track)('cli_search', { query, type: options.type, tags: options.tags });
56
61
  }
57
62
  else {
58
- agents = await (0, api_1.listPublicAgents)(config, { sort, type: typeFilter });
59
- await (0, analytics_1.track)('cli_search', { mode: options.popular ? 'popular' : 'recent', type: options.type });
63
+ agents = await (0, api_1.listPublicAgents)(config, { sort, tags, type: typeFilter });
64
+ await (0, analytics_1.track)('cli_search', { mode: options.popular ? 'popular' : 'recent', type: options.type, tags: options.tags });
60
65
  }
61
66
  }
62
67
  // Filter by pricing if requested
@@ -67,7 +67,7 @@ function formatSummaryOutput(result) {
67
67
  process.stdout.write('━'.repeat(50) + '\n\n');
68
68
  // Agent info
69
69
  process.stdout.write(`${chalk_1.default.bold('Agent:')} ${result.agent_id}\n`);
70
- process.stdout.write(`${chalk_1.default.bold('Scan Time:')} ${result.scan_timestamp}\n\n`);
70
+ process.stdout.write(`${chalk_1.default.bold('Scan Time:')} ${result.scanned_at}\n\n`);
71
71
  // Risk level banner
72
72
  process.stdout.write(`${chalk_1.default.bold('Risk Level:')} ${riskLevelColor(result.risk_level)}\n\n`);
73
73
  // Summary stats
@@ -282,7 +282,7 @@ function generateMarkdownReport(result) {
282
282
  lines.push('# Security Scan Report');
283
283
  lines.push('');
284
284
  lines.push(`**Agent:** ${result.agent_id}`);
285
- lines.push(`**Scan Time:** ${result.scan_timestamp}`);
285
+ lines.push(`**Scan Time:** ${result.scanned_at}`);
286
286
  lines.push(`**Risk Level:** ${result.risk_level.toUpperCase()}`);
287
287
  lines.push('');
288
288
  lines.push('## Summary');
@@ -33,9 +33,16 @@ function registerStarCommand(program) {
33
33
  process.stdout.write(`Removed star from ${org}/${name}/${version}\n`);
34
34
  }
35
35
  else {
36
- await (0, api_1.starAgent)(config, agentInfo.id);
37
- await (0, analytics_1.track)('cli_star', { agent: `${org}/${name}/${version}` });
38
- process.stdout.write(`Starred ${org}/${name}/${version}\n`);
36
+ const result = await (0, api_1.starAgent)(config, agentInfo.id);
37
+ if (result.starred) {
38
+ await (0, analytics_1.track)('cli_star', { agent: `${org}/${name}/${version}` });
39
+ process.stdout.write(`Starred ${org}/${name}/${version}\n`);
40
+ }
41
+ else {
42
+ // Already starred — toggle off
43
+ await (0, api_1.unstarAgent)(config, agentInfo.id);
44
+ process.stdout.write(`Unstarred ${org}/${name}/${version}\n`);
45
+ }
39
46
  }
40
47
  });
41
48
  }
@@ -13,6 +13,24 @@ function registerWhoamiCommand(program) {
13
13
  process.stdout.write(`Logged in as: ${org.name}\n`);
14
14
  process.stdout.write(`Org slug: ${org.slug}\n`);
15
15
  process.stdout.write(`API URL: ${config.apiUrl}\n`);
16
+ // Show active workspace if one is set
17
+ const configFile = await (0, config_1.loadConfig)();
18
+ if (configFile.workspace) {
19
+ try {
20
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
21
+ const workspace = response.workspaces.find((w) => w.slug === configFile.workspace);
22
+ if (workspace) {
23
+ process.stdout.write(`Active workspace: ${workspace.name} (${workspace.slug})\n`);
24
+ }
25
+ else {
26
+ process.stdout.write(`Active workspace: ${configFile.workspace} (not found)\n`);
27
+ }
28
+ }
29
+ catch {
30
+ // Workspace fetch failed - show slug only
31
+ process.stdout.write(`Active workspace: ${configFile.workspace}\n`);
32
+ }
33
+ }
16
34
  // Show balance after org info
17
35
  try {
18
36
  const balance = await (0, api_1.getCreditsBalance)(config);
package/dist/index.js CHANGED
@@ -47,8 +47,10 @@ const errors_1 = require("./lib/errors");
47
47
  const analytics_1 = require("./lib/analytics");
48
48
  const config_1 = require("./lib/config");
49
49
  const spinner_1 = require("./lib/spinner");
50
+ const update_notifier_1 = require("./lib/update-notifier");
50
51
  const package_json_1 = __importDefault(require("../package.json"));
51
52
  (0, analytics_1.initPostHog)();
53
+ (0, update_notifier_1.checkForUpdates)();
52
54
  const program = new commander_1.Command();
53
55
  program
54
56
  .name('orchagent')
@@ -89,4 +91,7 @@ async function main() {
89
91
  }
90
92
  main()
91
93
  .catch(errors_1.exitWithError)
92
- .finally(() => (0, analytics_1.shutdownPostHog)());
94
+ .finally(() => {
95
+ (0, update_notifier_1.printUpdateNotification)();
96
+ return (0, analytics_1.shutdownPostHog)();
97
+ });
package/dist/lib/api.js CHANGED
@@ -238,6 +238,8 @@ async function listPublicAgents(config, options) {
238
238
  const params = new URLSearchParams();
239
239
  if (options?.sort)
240
240
  params.append('sort', options.sort);
241
+ if (options?.tags?.length)
242
+ params.append('tags', options.tags.join(','));
241
243
  if (options?.type)
242
244
  params.append('type', options.type);
243
245
  const queryStr = params.toString();
@@ -257,7 +259,7 @@ async function createAgent(config, data) {
257
259
  });
258
260
  }
259
261
  async function starAgent(config, agentId) {
260
- await request(config, 'POST', `/agents/${agentId}/star`);
262
+ return request(config, 'POST', `/agents/${agentId}/star`);
261
263
  }
262
264
  async function unstarAgent(config, agentId) {
263
265
  await request(config, 'DELETE', `/agents/${agentId}/star`);
@@ -1,128 +1,150 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkServerLlmKeys = checkServerLlmKeys;
4
- exports.checkLocalLlmEnvVars = checkLocalLlmEnvVars;
5
3
  exports.runLlmChecks = runLlmChecks;
6
4
  const config_1 = require("../../config");
7
5
  const api_1 = require("../../api");
8
- // Common LLM provider environment variables
9
- const LLM_ENV_VARS = [
10
- { name: 'OPENAI_API_KEY', provider: 'OpenAI' },
11
- { name: 'ANTHROPIC_API_KEY', provider: 'Anthropic' },
12
- { name: 'GOOGLE_API_KEY', provider: 'Google' },
13
- { name: 'GEMINI_API_KEY', provider: 'Gemini' },
6
+ // All supported LLM providers (single source of truth)
7
+ const PROVIDERS = [
8
+ { id: 'openai', displayName: 'OpenAI', envVars: ['OPENAI_API_KEY'], keyPrefix: 'sk-' },
9
+ { id: 'anthropic', displayName: 'Anthropic', envVars: ['ANTHROPIC_API_KEY'], keyPrefix: 'sk-ant-' },
10
+ { id: 'gemini', displayName: 'Gemini', envVars: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'] },
11
+ { id: 'ollama', displayName: 'Ollama', envVars: ['OLLAMA_HOST'], isEndpoint: true },
14
12
  ];
15
13
  /**
16
- * Check if LLM keys are configured on the server.
14
+ * Get a format hint for a key value, or null if the format looks OK.
15
+ * Hints are informational only — never errors or warnings.
17
16
  */
18
- async function checkServerLlmKeys() {
19
- const config = await (0, config_1.getResolvedConfig)();
20
- if (!config.apiKey) {
21
- return {
22
- category: 'llm',
23
- name: 'server_llm_keys',
24
- status: 'warning',
25
- message: 'Cannot check server LLM keys (not logged in)',
26
- details: { reason: 'no api key' },
27
- };
17
+ function getFormatHint(provider, value) {
18
+ if ('isEndpoint' in provider && provider.isEndpoint)
19
+ return null;
20
+ if (!('keyPrefix' in provider) || !provider.keyPrefix)
21
+ return null;
22
+ const prefix = provider.keyPrefix;
23
+ if (!value.startsWith(prefix)) {
24
+ return `Key doesn't match expected format (${prefix}...)`;
28
25
  }
29
- try {
30
- const keys = await (0, api_1.fetchLlmKeys)(config);
31
- if (keys.length === 0) {
32
- return {
33
- category: 'llm',
34
- name: 'server_llm_keys',
35
- status: 'warning',
36
- message: 'No LLM keys configured on server',
37
- fix: 'Run `orch keys add <provider>` or add keys at orchagent.io/settings',
38
- details: { count: 0, providers: [] },
39
- };
40
- }
41
- const providers = keys.map((k) => k.provider);
42
- // Warn if only one provider configured (no fallback for rate limits)
43
- if (keys.length === 1) {
44
- return {
45
- category: 'llm',
46
- name: 'server_llm_keys',
47
- status: 'warning',
48
- message: `Only 1 LLM provider configured (${providers[0]}). Consider adding a backup for rate limit fallback.`,
49
- fix: 'Run: orchagent keys add <provider>',
50
- details: { count: keys.length, providers },
51
- };
52
- }
53
- return {
54
- category: 'llm',
55
- name: 'server_llm_keys',
56
- status: 'success',
57
- message: `Server LLM keys configured (${providers.join(', ')})`,
58
- details: { count: keys.length, providers },
59
- };
60
- }
61
- catch (err) {
62
- if (err instanceof api_1.ApiError && err.status === 401) {
63
- return {
64
- category: 'llm',
65
- name: 'server_llm_keys',
66
- status: 'warning',
67
- message: 'Cannot check server LLM keys (auth failed)',
68
- details: { error: err.message },
69
- };
26
+ return null;
27
+ }
28
+ /**
29
+ * Gather per-provider status from server keys and local env vars.
30
+ */
31
+ function gatherProviderStatuses(serverProviders) {
32
+ return PROVIDERS.map((provider) => {
33
+ // Server status
34
+ const server = serverProviders === null ? null : serverProviders.includes(provider.id);
35
+ // Local status check each env var
36
+ let local = false;
37
+ let localEnvVar;
38
+ let formatHint;
39
+ for (const envVar of provider.envVars) {
40
+ const value = process.env[envVar];
41
+ if (value) {
42
+ local = true;
43
+ localEnvVar = envVar;
44
+ const hint = getFormatHint(provider, value);
45
+ if (hint)
46
+ formatHint = hint;
47
+ break;
48
+ }
70
49
  }
71
50
  return {
72
- category: 'llm',
73
- name: 'server_llm_keys',
74
- status: 'warning',
75
- message: 'Could not check server LLM keys',
76
- details: { error: err instanceof Error ? err.message : 'unknown error' },
51
+ providerId: provider.id,
52
+ displayName: provider.displayName,
53
+ server,
54
+ local,
55
+ localEnvVar,
56
+ formatHint,
77
57
  };
58
+ });
59
+ }
60
+ /**
61
+ * Build a human-readable location string from server/local status.
62
+ */
63
+ function locationString(status) {
64
+ if (status.server === null) {
65
+ // Server unknown (offline)
66
+ if (status.local)
67
+ return 'Server unknown, local configured';
68
+ return 'Server unknown, not local';
78
69
  }
70
+ if (status.server && status.local)
71
+ return 'Configured (server + local)';
72
+ if (status.server)
73
+ return 'Configured (server)';
74
+ if (status.local)
75
+ return 'Configured (local)';
76
+ return 'Not configured';
79
77
  }
80
78
  /**
81
- * Check if common LLM provider API keys are set in environment.
79
+ * Run all LLM configuration checks with per-provider breakdown.
80
+ *
81
+ * When skipServer is true, server status is null for all providers
82
+ * (shown as "unknown" in output). Use this when the gateway is unreachable.
82
83
  */
83
- async function checkLocalLlmEnvVars() {
84
- const configuredProviders = [];
85
- const details = {};
86
- for (const { name, provider } of LLM_ENV_VARS) {
87
- const isSet = !!process.env[name];
88
- details[name] = isSet;
89
- if (isSet) {
90
- configuredProviders.push(provider);
84
+ async function runLlmChecks(options) {
85
+ let serverProviders = null;
86
+ if (!options?.skipServer) {
87
+ try {
88
+ const config = await (0, config_1.getResolvedConfig)();
89
+ if (config.apiKey) {
90
+ const keys = await (0, api_1.fetchLlmKeys)(config);
91
+ serverProviders = keys.map((k) => k.provider);
92
+ }
93
+ }
94
+ catch (err) {
95
+ // If we can't reach the server, treat as unknown
96
+ if (err instanceof api_1.ApiError && err.status === 401) {
97
+ // Auth failed — server providers unknown
98
+ }
99
+ // Network error or other — server providers unknown
91
100
  }
92
101
  }
93
- if (configuredProviders.length === 0) {
94
- return {
102
+ const statuses = gatherProviderStatuses(serverProviders);
103
+ const results = [];
104
+ // Per-provider results
105
+ for (const status of statuses) {
106
+ const configured = status.server === true || status.local;
107
+ results.push({
95
108
  category: 'llm',
96
- name: 'local_llm_env',
97
- status: 'warning',
98
- message: 'No local LLM API keys found in environment',
99
- fix: 'Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or similar for local runs',
100
- details,
101
- };
109
+ name: `llm_provider_${status.providerId}`,
110
+ status: configured ? 'success' : 'info',
111
+ message: `${status.providerId} ${locationString(status)}`,
112
+ details: {
113
+ providerId: status.providerId,
114
+ displayName: status.displayName,
115
+ server: status.server,
116
+ local: status.local,
117
+ ...(status.localEnvVar && { localEnvVar: status.localEnvVar }),
118
+ ...(status.formatHint && { formatHint: status.formatHint }),
119
+ },
120
+ });
102
121
  }
103
- // Deduplicate (Google and Gemini might both be set)
104
- const uniqueProviders = [...new Set(configuredProviders)];
105
- return {
106
- category: 'llm',
107
- name: 'local_llm_env',
108
- status: 'success',
109
- message: `Local LLM keys found (${uniqueProviders.join(', ')})`,
110
- details,
111
- };
112
- }
113
- /**
114
- * Run all LLM configuration checks.
115
- * If server keys are configured, local keys warning becomes informational.
116
- */
117
- async function runLlmChecks() {
118
- const serverResult = await checkServerLlmKeys();
119
- const localResult = await checkLocalLlmEnvVars();
120
- // If server keys are configured, downgrade local keys warning to info
121
- // Users who only use server-side calls don't need local keys
122
- if (serverResult.status === 'success' && localResult.status === 'warning') {
123
- localResult.status = 'info';
124
- localResult.message = 'No local LLM API keys (using server keys)';
125
- localResult.fix = undefined;
122
+ // Summary result
123
+ const configuredCount = statuses.filter((s) => s.server === true || s.local).length;
124
+ const firstUnconfigured = statuses.find((s) => s.server !== true && !s.local);
125
+ let summaryStatus;
126
+ let summaryMessage;
127
+ let summaryFix;
128
+ if (configuredCount === 0) {
129
+ summaryStatus = 'warning';
130
+ summaryMessage = 'No LLM providers configured';
131
+ summaryFix = firstUnconfigured ? `Run: orch keys add ${firstUnconfigured.providerId}` : undefined;
132
+ }
133
+ else {
134
+ summaryStatus = 'success';
135
+ summaryMessage =
136
+ configuredCount < 2
137
+ ? 'Tip: Multiple providers enable automatic rate limit fallback.'
138
+ : `${configuredCount} providers configured`;
139
+ summaryFix = firstUnconfigured ? `Run: orch keys add ${firstUnconfigured.providerId}` : undefined;
126
140
  }
127
- return [serverResult, localResult];
141
+ results.push({
142
+ category: 'llm',
143
+ name: 'llm_provider_summary',
144
+ status: summaryStatus,
145
+ message: summaryMessage,
146
+ fix: summaryFix,
147
+ details: { configuredCount, totalProviders: PROVIDERS.length },
148
+ });
149
+ return results;
128
150
  }