@orchagent/cli 0.3.101 → 0.3.103

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.
@@ -193,6 +193,10 @@ function registerDagCommand(program) {
193
193
  throw new errors_1.CliError('Missing API key. Run `orch login` first.');
194
194
  }
195
195
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
196
+ // Accept req_xxx format (gateway request_id shown in run output)
197
+ if (/^req_[0-9a-f]+$/i.test(runId)) {
198
+ runId = runId.slice(4);
199
+ }
196
200
  // Resolve short run IDs
197
201
  let resolvedRunId = runId;
198
202
  if (isUuid(runId)) {
@@ -9,6 +9,7 @@ const config_1 = require("../lib/config");
9
9
  const api_1 = require("../lib/api");
10
10
  const agent_ref_1 = require("../lib/agent-ref");
11
11
  const errors_1 = require("../lib/errors");
12
+ const json_input_1 = require("../lib/json-input");
12
13
  const spinner_1 = require("../lib/spinner");
13
14
  const output_1 = require("../lib/output");
14
15
  const package_json_1 = __importDefault(require("../../package.json"));
@@ -194,13 +195,7 @@ function registerHealthCommand(program) {
194
195
  const inputSchema = agentMeta.input_schema;
195
196
  let body;
196
197
  if (options.data) {
197
- try {
198
- JSON.parse(options.data);
199
- body = options.data;
200
- }
201
- catch {
202
- throw new errors_1.CliError('Invalid JSON in --data option.');
203
- }
198
+ body = await (0, json_input_1.resolveJsonBody)(options.data);
204
199
  }
205
200
  else {
206
201
  const sample = generateSampleInput(inputSchema);
@@ -58,7 +58,7 @@ Reads JSON input from stdin, processes it, and writes JSON output to stdout.
58
58
  This is the standard orchagent tool protocol.
59
59
 
60
60
  Usage:
61
- echo '{"input": "hello"}' | python main.py
61
+ echo '{"input": "hello"}' | python3 main.py
62
62
  """
63
63
 
64
64
  import json
@@ -136,7 +136,7 @@ IMPORTANT: Port 8080 is reserved by the platform health server.
136
136
  Use a different port (default: 3000).
137
137
 
138
138
  Local development:
139
- python main.py
139
+ python3 main.py
140
140
  """
141
141
 
142
142
  import json
@@ -405,7 +405,7 @@ orchagent fan-out orchestrator.
405
405
  Calls multiple agents in parallel, combines their results.
406
406
 
407
407
  Usage:
408
- echo '{"task": "analyze this"}' | python main.py
408
+ echo '{"task": "analyze this"}' | python3 main.py
409
409
  """
410
410
 
411
411
  import asyncio
@@ -497,7 +497,7 @@ orchagent pipeline orchestrator.
497
497
  Calls agents sequentially — each step's output feeds into the next.
498
498
 
499
499
  Usage:
500
- echo '{"task": "process this data"}' | python main.py
500
+ echo '{"task": "process this data"}' | python3 main.py
501
501
  """
502
502
 
503
503
  import asyncio
@@ -584,7 +584,7 @@ orchagent map-reduce orchestrator.
584
584
  Splits input into chunks, processes each in parallel (map), then aggregates (reduce).
585
585
 
586
586
  Usage:
587
- echo '{"items": ["item1", "item2", "item3"]}' | python main.py
587
+ echo '{"items": ["item1", "item2", "item3"]}' | python3 main.py
588
588
  """
589
589
 
590
590
  import asyncio
@@ -803,7 +803,7 @@ At least one platform token is required.
803
803
 
804
804
  \`\`\`sh
805
805
  pip install -r requirements.txt
806
- python main.py
806
+ python3 main.py
807
807
  \`\`\`
808
808
 
809
809
  ### 5. Deploy
@@ -848,7 +848,7 @@ cp .env.example .env
848
848
  # Fill in DISCORD_BOT_TOKEN, ANTHROPIC_API_KEY, DISCORD_CHANNEL_IDS
849
849
 
850
850
  pip install -r requirements.txt
851
- python main.py
851
+ python3 main.py
852
852
  \`\`\`
853
853
 
854
854
  ### 4. Deploy
@@ -1068,7 +1068,7 @@ Reads JSON input from stdin, processes the task, and writes JSON output to stdou
1068
1068
  This is a code-runtime agent — you control the logic and can call any LLM provider.
1069
1069
 
1070
1070
  Usage:
1071
- echo '{"task": "summarize this text"}' | python main.py
1071
+ echo '{"task": "summarize this text"}' | python3 main.py
1072
1072
  """
1073
1073
 
1074
1074
  import json
@@ -1203,7 +1203,7 @@ Reads JSON input from stdin, calls dependency agents via the orchagent SDK,
1203
1203
  and writes JSON output to stdout.
1204
1204
 
1205
1205
  Usage:
1206
- echo '{"task": "do something"}' | python main.py
1206
+ echo '{"task": "do something"}' | python3 main.py
1207
1207
  """
1208
1208
 
1209
1209
  import asyncio
@@ -1328,7 +1328,7 @@ Listens for messages in configured channels and responds using the Anthropic API
1328
1328
  Local development:
1329
1329
  1. Copy .env.example to .env and fill in your tokens
1330
1330
  2. pip install -r requirements.txt
1331
- 3. python main.py
1331
+ 3. python3 main.py
1332
1332
  """
1333
1333
 
1334
1334
  import asyncio
@@ -1686,7 +1686,7 @@ function registerInitCommand(program) {
1686
1686
  type: 'agent',
1687
1687
  description: 'Multi-platform support agent powered by Claude. Connects to Discord, Telegram, and/or Slack.',
1688
1688
  run_mode: 'always_on',
1689
- runtime: { command: 'python main.py' },
1689
+ runtime: { command: 'python3 main.py' },
1690
1690
  entrypoint: 'main.py',
1691
1691
  supported_providers: ['anthropic'],
1692
1692
  default_models: { anthropic: 'claude-sonnet-4-5-20250929' },
@@ -1734,7 +1734,7 @@ function registerInitCommand(program) {
1734
1734
  process.stdout.write(` ${s}. Edit config.py with your product name and description\n`);
1735
1735
  process.stdout.write(` ${s + 1}. Replace knowledge/ files with your own docs\n`);
1736
1736
  process.stdout.write(` ${s + 2}. Copy .env.example to .env and add platform tokens\n`);
1737
- process.stdout.write(` ${s + 3}. Test locally: pip install -r requirements.txt && python main.py\n`);
1737
+ process.stdout.write(` ${s + 3}. Test locally: pip install -r requirements.txt && python3 main.py\n`);
1738
1738
  process.stdout.write(` ${s + 4}. Deploy: orch publish && orch service deploy\n`);
1739
1739
  process.stdout.write(AGENT_BUILDER_HINT);
1740
1740
  return;
@@ -1904,7 +1904,7 @@ function registerInitCommand(program) {
1904
1904
  type: 'agent',
1905
1905
  description: `A ${templateLabel} orchestrator agent`,
1906
1906
  run_mode: runMode,
1907
- runtime: { command: isJavaScript ? 'node main.js' : 'python main.py' },
1907
+ runtime: { command: isJavaScript ? 'node main.js' : 'python3 main.py' },
1908
1908
  manifest: {
1909
1909
  manifest_version: 1,
1910
1910
  dependencies,
@@ -1977,7 +1977,7 @@ function registerInitCommand(program) {
1977
1977
  type: 'tool',
1978
1978
  description: 'A scheduled job that runs on a cron schedule',
1979
1979
  run_mode: 'on_demand',
1980
- runtime: { command: isJavaScript ? 'node main.js' : 'python main.py' },
1980
+ runtime: { command: isJavaScript ? 'node main.js' : 'python3 main.py' },
1981
1981
  required_secrets: [],
1982
1982
  tags: ['scheduled', 'cron'],
1983
1983
  };
@@ -2018,7 +2018,7 @@ function registerInitCommand(program) {
2018
2018
  process.stdout.write(` 1. cd ${name}\n`);
2019
2019
  }
2020
2020
  const mainFile = isJavaScript ? 'main.js' : 'main.py';
2021
- const testCmd = isJavaScript ? 'node main.js' : 'python main.py';
2021
+ const testCmd = isJavaScript ? 'node main.js' : 'python3 main.py';
2022
2022
  process.stdout.write(` ${stepNum}. Edit ${mainFile} with your job logic\n`);
2023
2023
  process.stdout.write(` ${stepNum + 1}. Test: echo '{}' | ${testCmd}\n`);
2024
2024
  process.stdout.write(` ${stepNum + 2}. Publish: orch publish\n`);
@@ -2040,7 +2040,7 @@ function registerInitCommand(program) {
2040
2040
  }
2041
2041
  }
2042
2042
  if (initMode.flavor !== 'code_runtime' && initMode.flavor !== 'orchestrator' && initMode.flavor !== 'discord' && runMode === 'always_on') {
2043
- throw new errors_1.CliError("run_mode=always_on requires runtime.command in orchagent.json (e.g. \"runtime\": { \"command\": \"python main.py\" }). Use --type tool or --type agentic for code-runtime agents.");
2043
+ throw new errors_1.CliError("run_mode=always_on requires runtime.command in orchagent.json (e.g. \"runtime\": { \"command\": \"python3 main.py\" }). Use --type tool or --type agentic for code-runtime agents.");
2044
2044
  }
2045
2045
  // Create manifest and type-specific files
2046
2046
  const manifest = JSON.parse(MANIFEST_TEMPLATE);
@@ -2054,7 +2054,7 @@ function registerInitCommand(program) {
2054
2054
  manifest.entrypoint = 'main.js';
2055
2055
  }
2056
2056
  else {
2057
- manifest.runtime = { command: 'python main.py' };
2057
+ manifest.runtime = { command: 'python3 main.py' };
2058
2058
  }
2059
2059
  manifest.manifest = {
2060
2060
  manifest_version: 1,
@@ -2073,7 +2073,7 @@ function registerInitCommand(program) {
2073
2073
  }
2074
2074
  else if (initMode.flavor === 'discord') {
2075
2075
  manifest.description = 'An always-on Discord bot powered by Claude';
2076
- manifest.runtime = { command: 'python main.py' };
2076
+ manifest.runtime = { command: 'python3 main.py' };
2077
2077
  manifest.supported_providers = ['anthropic'];
2078
2078
  manifest.required_secrets = ['ANTHROPIC_API_KEY', 'DISCORD_BOT_TOKEN', 'DISCORD_CHANNEL_IDS'];
2079
2079
  manifest.tags = ['discord', 'always-on'];
@@ -2085,7 +2085,7 @@ function registerInitCommand(program) {
2085
2085
  manifest.entrypoint = 'main.js';
2086
2086
  }
2087
2087
  else {
2088
- manifest.runtime = { command: 'python main.py' };
2088
+ manifest.runtime = { command: 'python3 main.py' };
2089
2089
  }
2090
2090
  manifest.required_secrets = [];
2091
2091
  }
@@ -2202,7 +2202,7 @@ function registerInitCommand(program) {
2202
2202
  process.stdout.write(` ${stepNum}. Create a Discord bot at https://discord.com/developers/applications\n`);
2203
2203
  process.stdout.write(` ${stepNum + 1}. Enable Message Content Intent in bot settings\n`);
2204
2204
  process.stdout.write(` ${stepNum + 2}. Copy .env.example to .env and fill in your tokens\n`);
2205
- process.stdout.write(` ${stepNum + 3}. Test locally: pip install -r requirements.txt && python main.py\n`);
2205
+ process.stdout.write(` ${stepNum + 3}. Test locally: pip install -r requirements.txt && python3 main.py\n`);
2206
2206
  process.stdout.write(` ${stepNum + 4}. Deploy: orch publish\n`);
2207
2207
  }
2208
2208
  else if (initMode.flavor === 'code_runtime') {
@@ -2212,7 +2212,7 @@ function registerInitCommand(program) {
2212
2212
  }
2213
2213
  if (runMode === 'always_on') {
2214
2214
  const mainFile = isJavaScript ? 'main.js' : 'main.py';
2215
- const testCmd = isJavaScript ? 'node main.js' : 'python main.py';
2215
+ const testCmd = isJavaScript ? 'node main.js' : 'python3 main.py';
2216
2216
  process.stdout.write(` ${stepNum}. Edit ${mainFile} with your service logic\n`);
2217
2217
  process.stdout.write(` ${stepNum + 1}. Test locally: ${testCmd}\n`);
2218
2218
  process.stdout.write(` ${stepNum + 2}. Publish: orch publish\n`);
@@ -2229,7 +2229,7 @@ function registerInitCommand(program) {
2229
2229
  const inputField = initMode.type === 'agent' ? 'task' : 'input';
2230
2230
  process.stdout.write(` ${stepNum}. Edit main.py with your agent logic\n`);
2231
2231
  process.stdout.write(` ${stepNum + 1}. Edit schema.json with your input/output schemas\n`);
2232
- process.stdout.write(` ${stepNum + 2}. Test: echo '{"${inputField}": "test"}' | python main.py\n`);
2232
+ process.stdout.write(` ${stepNum + 2}. Test: echo '{"${inputField}": "test"}' | python3 main.py\n`);
2233
2233
  process.stdout.write(` ${stepNum + 3}. Run: orchagent publish\n`);
2234
2234
  }
2235
2235
  }
@@ -2242,6 +2242,10 @@ function registerInitCommand(program) {
2242
2242
  process.stdout.write(` ${stepNum + 1}. Edit schema.json with your input/output schemas\n`);
2243
2243
  process.stdout.write(` ${stepNum + 2}. Run: orchagent publish\n`);
2244
2244
  }
2245
+ if (initMode.flavor === 'managed_loop') {
2246
+ process.stdout.write(`\n Note: supported_providers: ["any"] means anthropic, openai, or gemini\n`);
2247
+ process.stdout.write(` Your vault key determines which is used (set with: orch secrets set)\n`);
2248
+ }
2245
2249
  process.stdout.write(AGENT_BUILDER_HINT);
2246
2250
  });
2247
2251
  }
@@ -42,6 +42,7 @@ exports.scanUndeclaredEnvVars = scanUndeclaredEnvVars;
42
42
  exports.scanReservedPort = scanReservedPort;
43
43
  exports.detectSdkCompatible = detectSdkCompatible;
44
44
  exports.checkDependencies = checkDependencies;
45
+ exports.checkWorkspaceLlmKeys = checkWorkspaceLlmKeys;
45
46
  exports.batchPublish = batchPublish;
46
47
  exports.registerPublishCommand = registerPublishCommand;
47
48
  const promises_1 = __importDefault(require("fs/promises"));
@@ -394,7 +395,7 @@ function commandForEntrypoint(entrypoint) {
394
395
  if (entrypoint.endsWith('.js') || entrypoint.endsWith('.mjs') || entrypoint.endsWith('.cjs') || entrypoint.endsWith('.ts')) {
395
396
  return `node ${entrypoint}`;
396
397
  }
397
- return `python ${entrypoint}`;
398
+ return `python3 ${entrypoint}`;
398
399
  }
399
400
  /**
400
401
  * Check if manifest dependencies are published and callable.
@@ -439,13 +440,71 @@ async function checkDependencies(config, dependencies, publishingOrgSlug, worksp
439
440
  }
440
441
  catch (err) {
441
442
  if (err?.status === 404) {
442
- return { ref, status: 'not_found' };
443
+ // Could be unpublished OR published-but-private — we can't tell from a 404
444
+ return { ref, status: 'not_found_cross_org' };
443
445
  }
444
446
  // Network/unexpected error — don't false alarm
445
447
  return { ref, status: 'found_callable' };
446
448
  }
447
449
  }));
448
450
  }
451
+ /**
452
+ * Provider names the platform supports for LLM vault keys.
453
+ * Must stay in sync with gateway's _PROVIDER_TO_SECRET_NAME (db.py).
454
+ */
455
+ const PROVIDER_TO_SECRET_NAME = {
456
+ anthropic: 'ANTHROPIC_API_KEY',
457
+ openai: 'OPENAI_API_KEY',
458
+ gemini: 'GEMINI_API_KEY',
459
+ };
460
+ /**
461
+ * After a successful publish, check whether the target workspace has LLM vault
462
+ * keys that match the agent's supported_providers. If not, print a warning so
463
+ * the user knows cloud runs will fail.
464
+ *
465
+ * Best-effort: API errors are silently swallowed (the agent is already published).
466
+ */
467
+ async function checkWorkspaceLlmKeys(config, workspaceId, workspaceSlug, executionEngine, supportedProviders) {
468
+ // Only direct_llm and managed_loop engines need LLM keys
469
+ if (executionEngine !== 'direct_llm' && executionEngine !== 'managed_loop') {
470
+ return;
471
+ }
472
+ let secrets;
473
+ try {
474
+ const result = await (0, api_1.request)(config, 'GET', `/workspaces/${workspaceId}/secrets`);
475
+ secrets = result.secrets;
476
+ if (!Array.isArray(secrets))
477
+ return;
478
+ }
479
+ catch {
480
+ return; // Can't reach API or unexpected response — skip warning silently
481
+ }
482
+ const llmKeys = secrets.filter(s => s.secret_type === 'llm_key' && s.llm_provider);
483
+ const availableProviders = new Set(llmKeys.map(s => s.llm_provider));
484
+ // Determine which providers the agent needs
485
+ const needsAny = supportedProviders.includes('any');
486
+ if (needsAny) {
487
+ // 'any' means any single provider works
488
+ if (availableProviders.size > 0)
489
+ return;
490
+ }
491
+ else {
492
+ // Check if at least one of the supported providers has a key
493
+ const hasMatch = supportedProviders.some(p => availableProviders.has(p));
494
+ if (hasMatch)
495
+ return;
496
+ }
497
+ // Build the warning message
498
+ const providerList = needsAny
499
+ ? Object.keys(PROVIDER_TO_SECRET_NAME).join(', ')
500
+ : supportedProviders.join(', ');
501
+ const exampleSecretName = needsAny
502
+ ? 'ANTHROPIC_API_KEY'
503
+ : (PROVIDER_TO_SECRET_NAME[supportedProviders[0]] || `${supportedProviders[0].toUpperCase()}_API_KEY`);
504
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ No LLM vault keys found in workspace '${workspaceSlug}' for providers: ${providerList}\n`) +
505
+ ` Cloud runs will fail until you add keys.\n` +
506
+ ` Add a key: ${chalk_1.default.cyan(`orch secrets set ${exampleSecretName} <key>`)}\n`);
507
+ }
449
508
  /**
450
509
  * Batch publish all agents found in subdirectories, in dependency order.
451
510
  * Discovers orchagent.json/SKILL.md in immediate subdirectories,
@@ -489,11 +548,21 @@ async function batchPublish(rootDir, options) {
489
548
  catch {
490
549
  // Non-critical — just won't show org prefix
491
550
  }
492
- const plan = (0, batch_publish_1.formatPublishPlan)(sorted, orgSlug);
493
- process.stderr.write(plan);
551
+ // Dry-run: show ordering preview and exit (no subprocesses)
494
552
  if (options.dryRun) {
495
- process.stderr.write(chalk_1.default.cyan('DRY RUN running orch publish --dry-run in each directory:\n\n'));
553
+ const summary = (0, batch_publish_1.formatDryRunSummary)(sorted, orgSlug);
554
+ process.stderr.write(summary);
555
+ await (0, analytics_1.track)('cli_publish_all', {
556
+ total: sorted.length,
557
+ succeeded: 0,
558
+ failed: 0,
559
+ skipped: sorted.length,
560
+ dry_run: true,
561
+ });
562
+ return;
496
563
  }
564
+ const plan = (0, batch_publish_1.formatPublishPlan)(sorted, orgSlug);
565
+ process.stderr.write(plan);
497
566
  // Build the CLI args to forward (exclude --all)
498
567
  const forwardArgs = [];
499
568
  if (options.profile)
@@ -979,6 +1048,7 @@ function registerPublishCommand(program) {
979
1048
  if (manifestDeps?.length) {
980
1049
  const depResults = await checkDependencies(config, manifestDeps, org.slug, workspaceId);
981
1050
  const notFound = depResults.filter(r => r.status === 'not_found');
1051
+ const notFoundCrossOrg = depResults.filter(r => r.status === 'not_found_cross_org');
982
1052
  const notCallable = depResults.filter(r => r.status === 'found_not_callable');
983
1053
  if (notFound.length > 0) {
984
1054
  process.stderr.write(chalk_1.default.yellow(`\n⚠ Unpublished dependencies:\n`));
@@ -988,6 +1058,14 @@ function registerPublishCommand(program) {
988
1058
  process.stderr.write(`\n These agents must be published before this orchestrator can call them.\n` +
989
1059
  ` Publish each dependency first, then re-run this publish.\n\n`);
990
1060
  }
1061
+ if (notFoundCrossOrg.length > 0) {
1062
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies not found (unpublished or not accessible from this workspace):\n`));
1063
+ for (const dep of notFoundCrossOrg) {
1064
+ process.stderr.write(chalk_1.default.yellow(` - ${dep.ref}\n`));
1065
+ }
1066
+ process.stderr.write(`\n If the dependency is published in another workspace, ensure it's in the same\n` +
1067
+ ` workspace as this orchestrator, or use agent access grants.\n\n`);
1068
+ }
991
1069
  if (notCallable.length > 0) {
992
1070
  process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies have callable: false:\n`));
993
1071
  for (const dep of notCallable) {
@@ -998,6 +1076,23 @@ function registerPublishCommand(program) {
998
1076
  ` the field to use the default) and republish each dependency.\n\n`);
999
1077
  }
1000
1078
  }
1079
+ // UX-13-02: Warn when managed-loop orchestrator has dependencies but no custom_tools.
1080
+ // Without custom_tools, the LLM has no way to call its declared dependencies.
1081
+ if (executionEngine === 'managed_loop' && manifestDeps?.length) {
1082
+ const mergedTools = Array.isArray(loopConfig?.custom_tools) ? loopConfig.custom_tools : [];
1083
+ if (mergedTools.length === 0) {
1084
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ This managed-loop agent declares dependencies but no custom_tools.\n` +
1085
+ ` Without custom_tools, the LLM cannot call dependencies at runtime —\n` +
1086
+ ` it will waste turns exploring the filesystem instead.\n\n` +
1087
+ ` Use 'orch scaffold orchestration' to auto-generate the correct\n` +
1088
+ ` custom_tools configuration, or add them manually to your loop config:\n\n` +
1089
+ ` "loop": {\n` +
1090
+ ` "custom_tools": [\n` +
1091
+ ` { "name": "call_worker", "description": "...", "command": "python3 /home/user/helpers/orch_call.py org/worker@v1" }\n` +
1092
+ ` ]\n` +
1093
+ ` }\n\n`));
1094
+ }
1095
+ }
1001
1096
  // UX-2: Default required_secrets to [] when omitted for tool/agent types.
1002
1097
  // Prompt and skill types are exempt (prompt agents get LLM keys from platform,
1003
1098
  // skills don't run standalone).
@@ -1409,6 +1504,8 @@ function registerPublishCommand(program) {
1409
1504
  process.stderr.write(chalk_1.default.yellow(`⚠ ${warning}\n`));
1410
1505
  }
1411
1506
  }
1507
+ // Warn if workspace has no LLM vault keys for this agent's providers (UX-13-01)
1508
+ await checkWorkspaceLlmKeys(config, workspaceId || org.id, org.slug, executionEngine, supportedProviders);
1412
1509
  // Show required secrets with setup instructions (F-18)
1413
1510
  if (manifest.required_secrets?.length) {
1414
1511
  process.stdout.write(`\nRequired secrets:\n`);
@@ -50,7 +50,7 @@ function commandForEntrypoint(entrypoint) {
50
50
  || entrypoint.endsWith('.ts')) {
51
51
  return `node ${entrypoint}`;
52
52
  }
53
- return `python ${entrypoint}`;
53
+ return `python3 ${entrypoint}`;
54
54
  }
55
55
  // ─── Agent Resolution ───────────────────────────────────────────────────────
56
56
  async function resolveAgent(config, org, agent, version, workspaceId) {
@@ -112,11 +112,14 @@ async function resolveAgent(config, org, agent, version, workspaceId) {
112
112
  if (!config.apiKey) {
113
113
  throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
114
114
  }
115
- const userOrg = await (0, api_1.getOrg)(config, workspaceId);
116
- if (userOrg.slug !== org) {
117
- throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
115
+ // Try workspace-scoped search first, then fall back to personal org.
116
+ // This handles the case where the user is in a team workspace but the
117
+ // agent lives in their personal org (or vice versa). The org_slug
118
+ // filter inside resolveFromMyAgents prevents cross-org contamination.
119
+ let data = await resolveFromMyAgents(config, agent, version, org, workspaceId);
120
+ if (!data && workspaceId) {
121
+ data = await resolveFromMyAgents(config, agent, version, org, undefined);
118
122
  }
119
- const data = await resolveFromMyAgents(config, agent, version, org, workspaceId);
120
123
  if (!data) {
121
124
  throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
122
125
  }
@@ -124,15 +127,10 @@ async function resolveAgent(config, org, agent, version, workspaceId) {
124
127
  }
125
128
  async function tryOwnerFallback(config, org, agent, version, workspaceId) {
126
129
  try {
127
- const myAgents = await (0, api_1.listMyAgents)(config, workspaceId);
128
- let match;
129
- if (version === 'latest') {
130
- match = myAgents
131
- .filter(a => a.name === agent && a.org_slug === org)
132
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
133
- }
134
- else {
135
- match = myAgents.find(a => a.name === agent && a.version === version && a.org_slug === org);
130
+ let match = findOwnerMatch(await (0, api_1.listMyAgents)(config, workspaceId), agent, version, org);
131
+ // Retry without workspace restriction to find agents in personal org
132
+ if (!match && workspaceId) {
133
+ match = findOwnerMatch(await (0, api_1.listMyAgents)(config, undefined), agent, version, org);
136
134
  }
137
135
  if (!match)
138
136
  return null;
@@ -143,6 +141,14 @@ async function tryOwnerFallback(config, org, agent, version, workspaceId) {
143
141
  return null;
144
142
  }
145
143
  }
144
+ function findOwnerMatch(agents, agent, version, org) {
145
+ if (version === 'latest') {
146
+ return agents
147
+ .filter(a => a.name === agent && a.org_slug === org)
148
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
149
+ }
150
+ return agents.find(a => a.name === agent && a.version === version && a.org_slug === org);
151
+ }
146
152
  async function resolveFromMyAgents(config, agent, version, org, workspaceId) {
147
153
  const agents = await (0, api_1.listMyAgents)(config, workspaceId);
148
154
  const matching = agents.filter(a => a.name === agent && a.org_slug === org);
@@ -57,6 +57,7 @@ const config_1 = require("../lib/config");
57
57
  const resolve_agent_1 = require("../lib/resolve-agent");
58
58
  const api_1 = require("../lib/api");
59
59
  const errors_1 = require("../lib/errors");
60
+ const json_input_1 = require("../lib/json-input");
60
61
  const output_1 = require("../lib/output");
61
62
  const spinner_1 = require("../lib/spinner");
62
63
  const llm_1 = require("../lib/llm");
@@ -360,32 +361,6 @@ async function buildMultipartBody(filePaths, metadata) {
360
361
  sourceLabel: filePaths.length === 1 ? filePaths[0] : `${filePaths.length} files`,
361
362
  };
362
363
  }
363
- async function resolveJsonBody(input) {
364
- let raw = input;
365
- if (input.startsWith('@')) {
366
- const source = input.slice(1);
367
- if (!source) {
368
- throw new errors_1.CliError('Invalid JSON input. Use a JSON string or @file.');
369
- }
370
- if (source === '-') {
371
- const stdinData = await readStdin();
372
- if (!stdinData) {
373
- throw new errors_1.CliError('No stdin provided for JSON input.');
374
- }
375
- raw = stdinData.toString('utf8');
376
- }
377
- else {
378
- await validateFilePath(source);
379
- raw = await promises_1.default.readFile(source, 'utf8');
380
- }
381
- }
382
- try {
383
- return JSON.stringify(JSON.parse(raw));
384
- }
385
- catch {
386
- throw (0, errors_1.jsonInputError)('data');
387
- }
388
- }
389
364
  // ─── Keyed file & mount helpers ──────────────────────────────────────────────
390
365
  const KEYED_FILE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
391
366
  function isKeyedFileArg(arg) {
@@ -494,7 +469,7 @@ async function buildInjectedPayload(options) {
494
469
  let merged = {};
495
470
  // 1. Start with --data
496
471
  if (options.dataOption) {
497
- const resolved = await resolveJsonBody(options.dataOption);
472
+ const resolved = await (0, json_input_1.resolveJsonBody)(options.dataOption);
498
473
  merged = JSON.parse(resolved);
499
474
  }
500
475
  let totalBytes = 0;
@@ -787,6 +762,13 @@ async function detectAllLlmKeys(supportedProviders, config) {
787
762
  return providers;
788
763
  }
789
764
  async function executePromptLocally(agentData, inputData, skillPrompts = [], config, providerOverride, modelOverride) {
765
+ // Auto-detect provider from model name if not explicitly specified
766
+ if (!providerOverride && modelOverride) {
767
+ const detected = (0, llm_1.detectProviderFromModel)(modelOverride);
768
+ if (detected) {
769
+ providerOverride = detected;
770
+ }
771
+ }
790
772
  if (providerOverride) {
791
773
  (0, llm_1.validateProvider)(providerOverride);
792
774
  }
@@ -805,7 +787,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
805
787
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
806
788
  `Set an environment variable (e.g., OPENAI_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
807
789
  }
808
- if (modelOverride && !providerOverride && allProviders.length > 1) {
790
+ if (modelOverride && allProviders.length > 1) {
809
791
  process.stderr.write(`Warning: --model specified without --provider. The model '${modelOverride}' will be used for all ${allProviders.length} fallback providers, which may cause errors if the model is incompatible.\n` +
810
792
  `Consider specifying --provider to ensure correct model/provider pairing.\n\n`);
811
793
  }
@@ -855,6 +837,13 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
855
837
  throw new errors_1.CliError('Python 3 is required for local agent execution.\n' +
856
838
  'Install Python 3: https://python.org/downloads');
857
839
  }
840
+ // Auto-detect provider from model name if not explicitly specified
841
+ if (!providerOverride && modelOverride) {
842
+ const detected = (0, llm_1.detectProviderFromModel)(modelOverride);
843
+ if (detected) {
844
+ providerOverride = detected;
845
+ }
846
+ }
858
847
  // 2. Detect LLM provider + key
859
848
  const supportedProviders = manifest?.supported_providers || ['any'];
860
849
  const providersToCheck = providerOverride
@@ -1938,12 +1927,19 @@ async function executeCloud(agentRef, file, options) {
1938
1927
  return;
1939
1928
  }
1940
1929
  }
1941
- catch {
1942
- // Non-fatal: if estimate fails, proceed with the run (or exit if --estimate-only)
1930
+ catch (err) {
1931
+ // Provide specific error messages based on failure type
1932
+ const detail = err instanceof api_1.ApiError && err.status === 404
1933
+ ? 'Agent not found.'
1934
+ : err instanceof api_1.ApiError && err.status === 429
1935
+ ? 'Rate limited — try again shortly.'
1936
+ : err instanceof api_1.ApiError
1937
+ ? `API error (${err.status}).`
1938
+ : 'Network error — check your connection.';
1943
1939
  if (options.estimateOnly) {
1944
- throw new errors_1.CliError('Could not fetch cost estimate.');
1940
+ throw new errors_1.CliError(`Could not fetch cost estimate: ${detail}`);
1945
1941
  }
1946
- process.stderr.write(chalk_1.default.gray('Could not fetch cost estimate. Proceeding...\n\n'));
1942
+ process.stderr.write(chalk_1.default.gray(`Could not fetch cost estimate: ${detail} Proceeding with run...\n\n`));
1947
1943
  }
1948
1944
  }
1949
1945
  const endpoint = options.endpoint?.trim() || agentMeta.default_endpoint || 'analyze';
@@ -1968,18 +1964,8 @@ async function executeCloud(agentRef, file, options) {
1968
1964
  throw new errors_1.CliError('When using --key, you must also specify --provider (openai, anthropic, or gemini)');
1969
1965
  }
1970
1966
  (0, llm_1.validateProvider)(effectiveProvider);
1971
- if (options.model && effectiveProvider) {
1972
- const modelLower = options.model.toLowerCase();
1973
- const providerPatterns = {
1974
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
1975
- anthropic: /^claude-/,
1976
- gemini: /^gemini-/,
1977
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
1978
- };
1979
- const expectedPattern = providerPatterns[effectiveProvider];
1980
- if (expectedPattern && !expectedPattern.test(modelLower)) {
1981
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
1982
- }
1967
+ if (options.model) {
1968
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
1983
1969
  }
1984
1970
  llmKey = options.key;
1985
1971
  llmProvider = effectiveProvider;
@@ -1990,17 +1976,7 @@ async function executeCloud(agentRef, file, options) {
1990
1976
  (0, llm_1.validateProvider)(effectiveProvider);
1991
1977
  providersToCheck = [effectiveProvider];
1992
1978
  if (options.model) {
1993
- const modelLower = options.model.toLowerCase();
1994
- const providerPatterns = {
1995
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
1996
- anthropic: /^claude-/,
1997
- gemini: /^gemini-/,
1998
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
1999
- };
2000
- const expectedPattern = providerPatterns[effectiveProvider];
2001
- if (expectedPattern && !expectedPattern.test(modelLower)) {
2002
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
2003
- }
1979
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
2004
1980
  }
2005
1981
  }
2006
1982
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, resolved);
@@ -2075,7 +2051,7 @@ async function executeCloud(agentRef, file, options) {
2075
2051
  }
2076
2052
  if (options.data && filePaths.length > 0) {
2077
2053
  // Merge file content into --data
2078
- const resolvedBody = await resolveJsonBody(options.data);
2054
+ const resolvedBody = await (0, json_input_1.resolveJsonBody)(options.data);
2079
2055
  const bodyObj = JSON.parse(resolvedBody);
2080
2056
  if (cloudEngine !== 'code_runtime') {
2081
2057
  const fieldName = resolveFileField(options.fileField, agentMeta.input_schema);
@@ -2123,7 +2099,7 @@ async function executeCloud(agentRef, file, options) {
2123
2099
  }
2124
2100
  }
2125
2101
  else if (options.data) {
2126
- const resolvedBody = await resolveJsonBody(options.data);
2102
+ const resolvedBody = await (0, json_input_1.resolveJsonBody)(options.data);
2127
2103
  warnIfLocalPathReference(resolvedBody);
2128
2104
  const parsedBody = JSON.parse(resolvedBody);
2129
2105
  warnInputSchemaErrors(parsedBody, agentMeta.input_schema);
@@ -2579,7 +2555,22 @@ async function executeCloud(agentRef, file, options) {
2579
2555
  process.stdout.write(`${payload}\n`);
2580
2556
  return;
2581
2557
  }
2582
- (0, output_1.printJson)(payload);
2558
+ // In verbose mode, strip stdout/stderr from the JSON payload since they'll
2559
+ // be displayed in dedicated colored sections below (avoids duplication)
2560
+ if (options.verbose && typeof payload === 'object' && payload !== null && 'metadata' in payload) {
2561
+ const payloadObj = payload;
2562
+ const meta = payloadObj.metadata;
2563
+ if (meta && (meta.stdout || meta.stderr)) {
2564
+ const { stdout: _s, stderr: _e, ...cleanMeta } = meta;
2565
+ (0, output_1.printJson)({ ...payloadObj, metadata: cleanMeta });
2566
+ }
2567
+ else {
2568
+ (0, output_1.printJson)(payload);
2569
+ }
2570
+ }
2571
+ else {
2572
+ (0, output_1.printJson)(payload);
2573
+ }
2583
2574
  // Display timing metadata on stderr (non-json mode only)
2584
2575
  if (typeof payload === 'object' && payload !== null && 'metadata' in payload) {
2585
2576
  const meta = payload.metadata;
@@ -2591,7 +2582,14 @@ async function executeCloud(agentRef, file, options) {
2591
2582
  if (stderr) {
2592
2583
  process.stderr.write(chalk_1.default.bold.yellow('\n--- stderr ---') + '\n' + stderr + '\n');
2593
2584
  }
2594
- if (stdout) {
2585
+ // For code_runtime agents, stdout IS the data — skip if it would
2586
+ // duplicate what's already visible in the JSON data field
2587
+ const dataStr = typeof payload.data === 'string'
2588
+ ? payload.data
2589
+ : JSON.stringify(payload.data);
2590
+ const stdoutDuplicatesData = isCodeRuntimeAgent && stdout && dataStr &&
2591
+ stdout.trim() === dataStr.trim();
2592
+ if (stdout && !stdoutDuplicatesData) {
2595
2593
  process.stderr.write(chalk_1.default.bold.cyan('\n--- stdout ---') + '\n' + stdout + '\n');
2596
2594
  }
2597
2595
  if (!stderr && !stdout) {
@@ -2637,16 +2635,13 @@ async function executeLocal(agentRef, args, options) {
2637
2635
  options.input = JSON.stringify({ path: options.path });
2638
2636
  }
2639
2637
  if (options.model && options.provider) {
2640
- const modelLower = options.model.toLowerCase();
2641
- const providerPatterns = {
2642
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
2643
- anthropic: /^claude-/,
2644
- gemini: /^gemini-/,
2645
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
2646
- };
2647
- const expectedPattern = providerPatterns[options.provider];
2648
- if (expectedPattern && !expectedPattern.test(modelLower)) {
2649
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${options.provider} model.\n\n`);
2638
+ (0, llm_1.warnProviderModelMismatch)(options.model, options.provider);
2639
+ }
2640
+ else if (options.model && !options.provider) {
2641
+ const detected = (0, llm_1.detectProviderFromModel)(options.model);
2642
+ if (detected) {
2643
+ options.provider = detected;
2644
+ process.stderr.write(`Auto-detected provider: ${detected} (from model '${options.model}')\n\n`);
2650
2645
  }
2651
2646
  }
2652
2647
  const resolved = await (0, config_1.getResolvedConfig)();
@@ -2694,7 +2689,7 @@ async function executeLocal(agentRef, args, options) {
2694
2689
  return;
2695
2690
  }
2696
2691
  // Resolve @file.json / @- stdin syntax before parsing
2697
- const resolvedInput = await resolveJsonBody(options.input);
2692
+ const resolvedInput = await (0, json_input_1.resolveJsonBody)(options.input);
2698
2693
  let agentInputData;
2699
2694
  try {
2700
2695
  agentInputData = JSON.parse(resolvedInput);
@@ -11,6 +11,7 @@ const promises_1 = __importDefault(require("readline/promises"));
11
11
  const config_1 = require("../lib/config");
12
12
  const api_1 = require("../lib/api");
13
13
  const errors_1 = require("../lib/errors");
14
+ const json_input_1 = require("../lib/json-input");
14
15
  const output_1 = require("../lib/output");
15
16
  const agent_ref_1 = require("../lib/agent-ref");
16
17
  const api_2 = require("../lib/api");
@@ -187,12 +188,8 @@ function registerScheduleCommand(program) {
187
188
  const rawInput = options.data ?? options.input;
188
189
  let inputData;
189
190
  if (rawInput) {
190
- try {
191
- inputData = JSON.parse(rawInput);
192
- }
193
- catch {
194
- throw new errors_1.CliError('Invalid JSON in --data. Use single quotes: --data \'{"key": "value"}\'');
195
- }
191
+ const resolved = await (0, json_input_1.resolveJsonBody)(rawInput);
192
+ inputData = JSON.parse(resolved);
196
193
  }
197
194
  const scheduleType = options.webhook ? 'webhook' : 'cron';
198
195
  const body = {
@@ -22,7 +22,7 @@ Schedule examples:
22
22
  orch schedule create org/my-job --cron "0 */6 * * *" # Every 6 hours
23
23
 
24
24
  Local test:
25
- echo '{}' | python main.py
25
+ echo '{}' | python3 main.py
26
26
  """
27
27
 
28
28
  import json
@@ -14,7 +14,7 @@ exports.TEMPLATE_MANIFEST = `{
14
14
  "type": "agent",
15
15
  "description": "Weekly GitHub activity summary delivered to Discord. Uses Claude to analyse commits, PRs, and issues — surfaces patterns, risks, and trends.",
16
16
  "runtime": {
17
- "command": "python main.py"
17
+ "command": "python3 main.py"
18
18
  },
19
19
  "required_secrets": [
20
20
  "ORCHAGENT_API_KEY",
@@ -640,7 +640,7 @@ function runEntrypointWithInput(agentDir, entrypoint, stdinData, verbose) {
640
640
  /**
641
641
  * Run fixture tests for code_runtime agents by executing the entrypoint
642
642
  * with fixture input as stdin and validating the JSON output.
643
- * Same interface as E2B: python main.py < input.json
643
+ * Same interface as E2B: python3 main.py < input.json
644
644
  */
645
645
  async function runCodeRuntimeFixtureTests(agentDir, fixtures, entrypoint, verbose) {
646
646
  process.stderr.write(chalk_1.default.blue('\nRunning fixture tests (code runtime)...\n\n'));
@@ -660,7 +660,7 @@ async function runCodeRuntimeFixtureTests(agentDir, fixtures, entrypoint, verbos
660
660
  throw new errors_1.CliError(`Invalid JSON in ${fixtureName}: ${e.message}`);
661
661
  }
662
662
  const fixture = validateFixture(parsed, fixturePath);
663
- // Run entrypoint with fixture input as stdin (same as E2B: python main.py < input.json)
663
+ // Run entrypoint with fixture input as stdin (same as E2B: python3 main.py < input.json)
664
664
  const inputJson = JSON.stringify(fixture.input);
665
665
  const result = await runEntrypointWithInput(agentDir, entrypoint, inputJson, verbose);
666
666
  if (result.code !== 0) {
@@ -105,6 +105,10 @@ function registerTraceCommand(program) {
105
105
  throw new errors_1.CliError('Missing API key. Run `orch login` first.');
106
106
  }
107
107
  const workspaceId = await resolveWorkspaceId(config, options.workspace);
108
+ // Accept req_xxx format (gateway request_id shown in run output)
109
+ if (/^req_[0-9a-f]+$/i.test(runId)) {
110
+ runId = runId.slice(4);
111
+ }
108
112
  // Resolve short run IDs
109
113
  let resolvedRunId = runId;
110
114
  if (isUuid(runId)) {
@@ -172,6 +172,7 @@ Options:
172
172
  const org = await (0, api_1.getOrg)(config, workspaceId);
173
173
  const depResults = await (0, publish_1.checkDependencies)(config, deps, org.slug, workspaceId);
174
174
  const notFound = depResults.filter(r => r.status === 'not_found');
175
+ const notFoundCrossOrg = depResults.filter(r => r.status === 'not_found_cross_org');
175
176
  const notCallable = depResults.filter(r => r.status === 'found_not_callable');
176
177
  if (notFound.length > 0) {
177
178
  for (const dep of notFound) {
@@ -181,6 +182,14 @@ Options:
181
182
  });
182
183
  }
183
184
  }
185
+ if (notFoundCrossOrg.length > 0) {
186
+ for (const dep of notFoundCrossOrg) {
187
+ serverIssues.push({
188
+ level: 'warning',
189
+ message: `Dependency not found (unpublished or not accessible from this workspace): ${dep.ref}`,
190
+ });
191
+ }
192
+ }
184
193
  if (notCallable.length > 0) {
185
194
  for (const dep of notCallable) {
186
195
  serverIssues.push({
package/dist/lib/api.js CHANGED
@@ -380,15 +380,18 @@ async function getAgentWithFallback(config, org, agentName, version, workspaceId
380
380
  if (!config.apiKey) {
381
381
  throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
382
382
  }
383
- const userOrg = await getOrg(config, workspaceId);
384
- if (userOrg.slug !== org) {
385
- throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
386
- }
383
+ // Try authenticated lookup in the resolved workspace context
387
384
  const myAgent = await getMyAgent(config, agentName, version, workspaceId);
388
- if (!myAgent) {
389
- throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
385
+ if (myAgent)
386
+ return myAgent;
387
+ // Fallback: if workspace was specified, also check personal org —
388
+ // handles cross-workspace lookups (e.g., team context looking up personal agent)
389
+ if (workspaceId) {
390
+ const personalAgent = await getMyAgent(config, agentName, version);
391
+ if (personalAgent)
392
+ return personalAgent;
390
393
  }
391
- return myAgent;
394
+ throw new ApiError(`Agent '${org}/${agentName}@${version}' not found`, 404);
392
395
  }
393
396
  /**
394
397
  * Resolve a workspace ID from an org slug.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.discoverAgents = discoverAgents;
7
7
  exports.topoSort = topoSort;
8
8
  exports.formatPublishPlan = formatPublishPlan;
9
+ exports.formatDryRunSummary = formatDryRunSummary;
9
10
  const promises_1 = __importDefault(require("fs/promises"));
10
11
  const path_1 = __importDefault(require("path"));
11
12
  const chalk_1 = __importDefault(require("chalk"));
@@ -221,3 +222,75 @@ function formatPublishPlan(sorted, orgSlug) {
221
222
  lines.push('');
222
223
  return lines.join('\n');
223
224
  }
225
+ /**
226
+ * Format an enhanced dry-run summary for --all --dry-run.
227
+ * Shows publish ordering, local/external dependencies, and graph health.
228
+ */
229
+ function formatDryRunSummary(sorted, orgSlug) {
230
+ const lines = [];
231
+ const localNames = new Set(sorted.map(a => a.name));
232
+ // Collect external deps (referenced but not in the project)
233
+ const externalDeps = new Map(); // ref → [agent names that reference it]
234
+ for (const agent of sorted) {
235
+ for (const ref of agent.dependencyRefs) {
236
+ const depName = ref.includes('/') ? ref.split('/')[1] : ref;
237
+ if (!localNames.has(depName)) {
238
+ const consumers = externalDeps.get(ref) || [];
239
+ consumers.push(agent.name);
240
+ externalDeps.set(ref, consumers);
241
+ }
242
+ }
243
+ }
244
+ // Publish order table
245
+ lines.push('');
246
+ lines.push(chalk_1.default.bold(` Publish order (${sorted.length} agent${sorted.length === 1 ? '' : 's'}):`));
247
+ lines.push('');
248
+ for (let i = 0; i < sorted.length; i++) {
249
+ const agent = sorted[i];
250
+ const type = agent.isSkill ? 'skill' : 'agent';
251
+ const prefix = orgSlug ? `${orgSlug}/` : '';
252
+ // Separate local and external deps for clarity
253
+ const localDeps = [];
254
+ const extDeps = [];
255
+ for (const ref of agent.dependencyRefs) {
256
+ const depName = ref.includes('/') ? ref.split('/')[1] : ref;
257
+ if (localNames.has(depName)) {
258
+ localDeps.push(depName);
259
+ }
260
+ else {
261
+ extDeps.push(ref);
262
+ }
263
+ }
264
+ let depInfo = '';
265
+ if (localDeps.length > 0 || extDeps.length > 0) {
266
+ const parts = [];
267
+ if (localDeps.length > 0)
268
+ parts.push(localDeps.join(', '));
269
+ if (extDeps.length > 0)
270
+ parts.push(extDeps.map(d => `${d} ${chalk_1.default.yellow('(external)')}`).join(', '));
271
+ depInfo = ` ${chalk_1.default.gray('→')} ${parts.join(', ')}`;
272
+ }
273
+ lines.push(` ${chalk_1.default.bold(`${i + 1}.`)} ${prefix}${agent.name} ${chalk_1.default.gray(`[${type}]`)}${depInfo}`);
274
+ lines.push(` ${chalk_1.default.gray(agent.dirName + '/')}`);
275
+ }
276
+ // External dependencies section
277
+ if (externalDeps.size > 0) {
278
+ lines.push('');
279
+ lines.push(chalk_1.default.yellow(` External dependencies (must already be published):`));
280
+ for (const [ref, consumers] of externalDeps) {
281
+ lines.push(` ${ref} ${chalk_1.default.gray(`← ${consumers.join(', ')}`)}`);
282
+ }
283
+ }
284
+ // Summary
285
+ lines.push('');
286
+ const skillCount = sorted.filter(a => a.isSkill).length;
287
+ const agentCount = sorted.length - skillCount;
288
+ const parts = [];
289
+ if (agentCount > 0)
290
+ parts.push(`${agentCount} agent${agentCount === 1 ? '' : 's'}`);
291
+ if (skillCount > 0)
292
+ parts.push(`${skillCount} skill${skillCount === 1 ? '' : 's'}`);
293
+ lines.push(chalk_1.default.green(` ✓ ${parts.join(', ')} ready to publish (no circular dependencies)`));
294
+ lines.push('');
295
+ return lines.join('\n');
296
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveJsonBody = resolveJsonBody;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const errors_1 = require("./errors");
9
+ async function readStdin() {
10
+ if (process.stdin.isTTY)
11
+ return null;
12
+ const chunks = [];
13
+ for await (const chunk of process.stdin) {
14
+ chunks.push(Buffer.from(chunk));
15
+ }
16
+ if (!chunks.length)
17
+ return null;
18
+ return Buffer.concat(chunks);
19
+ }
20
+ async function validateFilePath(filePath) {
21
+ const stat = await promises_1.default.stat(filePath);
22
+ if (stat.isDirectory()) {
23
+ throw new errors_1.CliError(`Expected a file but got a directory: ${filePath}\n\n` +
24
+ `Provide a JSON file path, e.g. --data @input.json`);
25
+ }
26
+ }
27
+ /**
28
+ * Resolve a --data value to a validated JSON string.
29
+ *
30
+ * Supports three input forms:
31
+ * - Plain JSON string: '{"key":"value"}'
32
+ * - File reference: @input.json
33
+ * - Stdin pipe: @-
34
+ */
35
+ async function resolveJsonBody(input) {
36
+ let raw = input;
37
+ if (input.startsWith('@')) {
38
+ const source = input.slice(1);
39
+ if (!source) {
40
+ throw new errors_1.CliError('Invalid JSON input. Use a JSON string or @file.');
41
+ }
42
+ if (source === '-') {
43
+ const stdinData = await readStdin();
44
+ if (!stdinData) {
45
+ throw new errors_1.CliError('No stdin provided for JSON input.');
46
+ }
47
+ raw = stdinData.toString('utf8');
48
+ }
49
+ else {
50
+ await validateFilePath(source);
51
+ raw = await promises_1.default.readFile(source, 'utf8');
52
+ }
53
+ }
54
+ try {
55
+ return JSON.stringify(JSON.parse(raw));
56
+ }
57
+ catch {
58
+ throw (0, errors_1.jsonInputError)('data');
59
+ }
60
+ }
package/dist/lib/llm.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Used by run, call, and skill commands.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = exports.LlmError = void 0;
9
+ exports.MODEL_PROVIDER_PATTERNS = exports.DEFAULT_MODELS = exports.PROVIDER_ENV_VARS = exports.LlmError = void 0;
10
10
  exports.isRateLimitError = isRateLimitError;
11
11
  exports.detectLlmKeyFromEnv = detectLlmKeyFromEnv;
12
12
  exports.detectLlmKey = detectLlmKey;
@@ -15,6 +15,8 @@ exports.buildPrompt = buildPrompt;
15
15
  exports.callLlm = callLlm;
16
16
  exports.callLlmWithFallback = callLlmWithFallback;
17
17
  exports.validateProvider = validateProvider;
18
+ exports.detectProviderFromModel = detectProviderFromModel;
19
+ exports.warnProviderModelMismatch = warnProviderModelMismatch;
18
20
  const errors_1 = require("./errors");
19
21
  const llm_errors_1 = require("./llm-errors");
20
22
  class LlmError extends errors_1.CliError {
@@ -265,3 +267,37 @@ function validateProvider(provider) {
265
267
  throw new errors_1.CliError(`Invalid provider: ${provider}. Valid: ${validProviders.join(', ')}`);
266
268
  }
267
269
  }
270
+ /**
271
+ * Model-name patterns for auto-detecting the LLM provider.
272
+ * Tested against the lowercased model string.
273
+ */
274
+ exports.MODEL_PROVIDER_PATTERNS = {
275
+ openai: /^(gpt-|o1-|o3-|o4-|davinci|text-)/,
276
+ anthropic: /^claude-/,
277
+ gemini: /^gemini-/,
278
+ ollama: /^(llama|mistral|deepseek|phi|qwen)/,
279
+ };
280
+ /**
281
+ * Auto-detect the LLM provider from a model name using prefix patterns.
282
+ * Returns the provider string if a match is found, or null if ambiguous/unknown.
283
+ */
284
+ function detectProviderFromModel(model) {
285
+ const modelLower = model.toLowerCase();
286
+ for (const [provider, pattern] of Object.entries(exports.MODEL_PROVIDER_PATTERNS)) {
287
+ if (pattern.test(modelLower)) {
288
+ return provider;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ /**
294
+ * Warn if a model name doesn't match the expected provider's pattern.
295
+ * Used when both --model and --provider are explicitly specified.
296
+ */
297
+ function warnProviderModelMismatch(model, provider) {
298
+ const modelLower = model.toLowerCase();
299
+ const expectedPattern = exports.MODEL_PROVIDER_PATTERNS[provider];
300
+ if (expectedPattern && !expectedPattern.test(modelLower)) {
301
+ process.stderr.write(`Warning: Model '${model}' may not be a ${provider} model.\n\n`);
302
+ }
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.101",
3
+ "version": "0.3.103",
4
4
  "description": "Command-line interface for orchagent — deploy and run AI agents for your team",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",