@orchagent/cli 0.3.101 → 0.3.102

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.
@@ -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
  }
@@ -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,
@@ -979,6 +1038,7 @@ function registerPublishCommand(program) {
979
1038
  if (manifestDeps?.length) {
980
1039
  const depResults = await checkDependencies(config, manifestDeps, org.slug, workspaceId);
981
1040
  const notFound = depResults.filter(r => r.status === 'not_found');
1041
+ const notFoundCrossOrg = depResults.filter(r => r.status === 'not_found_cross_org');
982
1042
  const notCallable = depResults.filter(r => r.status === 'found_not_callable');
983
1043
  if (notFound.length > 0) {
984
1044
  process.stderr.write(chalk_1.default.yellow(`\n⚠ Unpublished dependencies:\n`));
@@ -988,6 +1048,14 @@ function registerPublishCommand(program) {
988
1048
  process.stderr.write(`\n These agents must be published before this orchestrator can call them.\n` +
989
1049
  ` Publish each dependency first, then re-run this publish.\n\n`);
990
1050
  }
1051
+ if (notFoundCrossOrg.length > 0) {
1052
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies not found (unpublished or not accessible from this workspace):\n`));
1053
+ for (const dep of notFoundCrossOrg) {
1054
+ process.stderr.write(chalk_1.default.yellow(` - ${dep.ref}\n`));
1055
+ }
1056
+ process.stderr.write(`\n If the dependency is published in another workspace, ensure it's in the same\n` +
1057
+ ` workspace as this orchestrator, or use agent access grants.\n\n`);
1058
+ }
991
1059
  if (notCallable.length > 0) {
992
1060
  process.stderr.write(chalk_1.default.yellow(`\n⚠ Dependencies have callable: false:\n`));
993
1061
  for (const dep of notCallable) {
@@ -998,6 +1066,23 @@ function registerPublishCommand(program) {
998
1066
  ` the field to use the default) and republish each dependency.\n\n`);
999
1067
  }
1000
1068
  }
1069
+ // UX-13-02: Warn when managed-loop orchestrator has dependencies but no custom_tools.
1070
+ // Without custom_tools, the LLM has no way to call its declared dependencies.
1071
+ if (executionEngine === 'managed_loop' && manifestDeps?.length) {
1072
+ const mergedTools = Array.isArray(loopConfig?.custom_tools) ? loopConfig.custom_tools : [];
1073
+ if (mergedTools.length === 0) {
1074
+ process.stderr.write(chalk_1.default.yellow(`\n⚠ This managed-loop agent declares dependencies but no custom_tools.\n` +
1075
+ ` Without custom_tools, the LLM cannot call dependencies at runtime —\n` +
1076
+ ` it will waste turns exploring the filesystem instead.\n\n` +
1077
+ ` Use 'orch scaffold orchestration' to auto-generate the correct\n` +
1078
+ ` custom_tools configuration, or add them manually to your loop config:\n\n` +
1079
+ ` "loop": {\n` +
1080
+ ` "custom_tools": [\n` +
1081
+ ` { "name": "call_worker", "description": "...", "command": "python3 /home/user/helpers/orch_call.py org/worker@v1" }\n` +
1082
+ ` ]\n` +
1083
+ ` }\n\n`));
1084
+ }
1085
+ }
1001
1086
  // UX-2: Default required_secrets to [] when omitted for tool/agent types.
1002
1087
  // Prompt and skill types are exempt (prompt agents get LLM keys from platform,
1003
1088
  // skills don't run standalone).
@@ -1409,6 +1494,8 @@ function registerPublishCommand(program) {
1409
1494
  process.stderr.write(chalk_1.default.yellow(`⚠ ${warning}\n`));
1410
1495
  }
1411
1496
  }
1497
+ // Warn if workspace has no LLM vault keys for this agent's providers (UX-13-01)
1498
+ await checkWorkspaceLlmKeys(config, workspaceId || org.id, org.slug, executionEngine, supportedProviders);
1412
1499
  // Show required secrets with setup instructions (F-18)
1413
1500
  if (manifest.required_secrets?.length) {
1414
1501
  process.stdout.write(`\nRequired secrets:\n`);
@@ -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);
@@ -787,6 +787,13 @@ async function detectAllLlmKeys(supportedProviders, config) {
787
787
  return providers;
788
788
  }
789
789
  async function executePromptLocally(agentData, inputData, skillPrompts = [], config, providerOverride, modelOverride) {
790
+ // Auto-detect provider from model name if not explicitly specified
791
+ if (!providerOverride && modelOverride) {
792
+ const detected = (0, llm_1.detectProviderFromModel)(modelOverride);
793
+ if (detected) {
794
+ providerOverride = detected;
795
+ }
796
+ }
790
797
  if (providerOverride) {
791
798
  (0, llm_1.validateProvider)(providerOverride);
792
799
  }
@@ -805,7 +812,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
805
812
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
806
813
  `Set an environment variable (e.g., OPENAI_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
807
814
  }
808
- if (modelOverride && !providerOverride && allProviders.length > 1) {
815
+ if (modelOverride && allProviders.length > 1) {
809
816
  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
817
  `Consider specifying --provider to ensure correct model/provider pairing.\n\n`);
811
818
  }
@@ -855,6 +862,13 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
855
862
  throw new errors_1.CliError('Python 3 is required for local agent execution.\n' +
856
863
  'Install Python 3: https://python.org/downloads');
857
864
  }
865
+ // Auto-detect provider from model name if not explicitly specified
866
+ if (!providerOverride && modelOverride) {
867
+ const detected = (0, llm_1.detectProviderFromModel)(modelOverride);
868
+ if (detected) {
869
+ providerOverride = detected;
870
+ }
871
+ }
858
872
  // 2. Detect LLM provider + key
859
873
  const supportedProviders = manifest?.supported_providers || ['any'];
860
874
  const providersToCheck = providerOverride
@@ -1968,18 +1982,8 @@ async function executeCloud(agentRef, file, options) {
1968
1982
  throw new errors_1.CliError('When using --key, you must also specify --provider (openai, anthropic, or gemini)');
1969
1983
  }
1970
1984
  (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
- }
1985
+ if (options.model) {
1986
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
1983
1987
  }
1984
1988
  llmKey = options.key;
1985
1989
  llmProvider = effectiveProvider;
@@ -1990,17 +1994,7 @@ async function executeCloud(agentRef, file, options) {
1990
1994
  (0, llm_1.validateProvider)(effectiveProvider);
1991
1995
  providersToCheck = [effectiveProvider];
1992
1996
  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
- }
1997
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
2004
1998
  }
2005
1999
  }
2006
2000
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, resolved);
@@ -2637,16 +2631,13 @@ async function executeLocal(agentRef, args, options) {
2637
2631
  options.input = JSON.stringify({ path: options.path });
2638
2632
  }
2639
2633
  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`);
2634
+ (0, llm_1.warnProviderModelMismatch)(options.model, options.provider);
2635
+ }
2636
+ else if (options.model && !options.provider) {
2637
+ const detected = (0, llm_1.detectProviderFromModel)(options.model);
2638
+ if (detected) {
2639
+ options.provider = detected;
2640
+ process.stderr.write(`Auto-detected provider: ${detected} (from model '${options.model}')\n\n`);
2650
2641
  }
2651
2642
  }
2652
2643
  const resolved = await (0, config_1.getResolvedConfig)();
@@ -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) {
@@ -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/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.102",
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>",