@orchagent/cli 0.3.100 → 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.
@@ -12,21 +12,10 @@ const child_process_1 = require("child_process");
12
12
  const config_1 = require("../lib/config");
13
13
  const api_1 = require("../lib/api");
14
14
  const errors_1 = require("../lib/errors");
15
+ const resolve_agent_1 = require("../lib/resolve-agent");
15
16
  const analytics_1 = require("../lib/analytics");
16
17
  const output_1 = require("../lib/output");
17
18
  // ─── Helpers ────────────────────────────────────────────────────────────────
18
- function parsePullRef(value) {
19
- const [ref, versionPart] = value.split('@');
20
- const version = versionPart?.trim() || 'latest';
21
- const segments = ref.split('/');
22
- if (segments.length === 1) {
23
- return { agent: segments[0], version };
24
- }
25
- if (segments.length === 2) {
26
- return { org: segments[0], agent: segments[1], version };
27
- }
28
- throw new errors_1.CliError('Invalid agent reference. Use org/agent[@version] or agent[@version] format.');
29
- }
30
19
  function canonicalType(typeValue) {
31
20
  const normalized = (typeValue || 'agent').toLowerCase();
32
21
  if (['prompt', 'tool', 'agent', 'skill'].includes(normalized))
@@ -123,11 +112,14 @@ async function resolveAgent(config, org, agent, version, workspaceId) {
123
112
  if (!config.apiKey) {
124
113
  throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
125
114
  }
126
- const userOrg = await (0, api_1.getOrg)(config, workspaceId);
127
- if (userOrg.slug !== org) {
128
- 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);
129
122
  }
130
- const data = await resolveFromMyAgents(config, agent, version, org, workspaceId);
131
123
  if (!data) {
132
124
  throw new api_1.ApiError(`Agent '${org}/${agent}@${version}' not found`, 404);
133
125
  }
@@ -135,15 +127,10 @@ async function resolveAgent(config, org, agent, version, workspaceId) {
135
127
  }
136
128
  async function tryOwnerFallback(config, org, agent, version, workspaceId) {
137
129
  try {
138
- const myAgents = await (0, api_1.listMyAgents)(config, workspaceId);
139
- let match;
140
- if (version === 'latest') {
141
- match = myAgents
142
- .filter(a => a.name === agent && a.org_slug === org)
143
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
144
- }
145
- else {
146
- 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);
147
134
  }
148
135
  if (!match)
149
136
  return null;
@@ -154,6 +141,14 @@ async function tryOwnerFallback(config, org, agent, version, workspaceId) {
154
141
  return null;
155
142
  }
156
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
+ }
157
152
  async function resolveFromMyAgents(config, agent, version, org, workspaceId) {
158
153
  const agents = await (0, api_1.listMyAgents)(config, workspaceId);
159
154
  const matching = agents.filter(a => a.name === agent && a.org_slug === org);
@@ -359,19 +354,12 @@ Examples:
359
354
  process.stdout.write(message);
360
355
  };
361
356
  const config = await (0, config_1.getResolvedConfig)();
362
- const parsed = parsePullRef(agentRef);
363
- // Resolve org from workspace / defaultOrg fallback
364
- const configFile = await (0, config_1.loadConfig)();
365
- const org = parsed.org ?? configFile.workspace ?? config.defaultOrg;
366
- if (!org) {
367
- throw new errors_1.CliError('Missing org. Use org/agent[@version] format, or set a default org with:\n' +
368
- ' orch config set default-org <org>');
369
- }
370
- // Resolve workspace context for the target org
371
- const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(config, org);
372
- write(`Resolving ${org}/${parsed.agent}@${parsed.version}...\n`);
357
+ const { org, agent: agentName, version, workspaceId } = await (0, resolve_agent_1.resolveAgentContext)(agentRef, config, {
358
+ missingOrgMessage: 'Missing org. Use org/agent[@version] format, or set a default org with:\n orch config set default-org <org>',
359
+ });
360
+ write(`Resolving ${org}/${agentName}@${version}...\n`);
373
361
  // Resolve agent data
374
- const data = await resolveAgent(config, org, parsed.agent, parsed.version, workspaceId);
362
+ const data = await resolveAgent(config, org, agentName, version, workspaceId);
375
363
  // Reject skills
376
364
  if (canonicalType(data.type) === 'skill') {
377
365
  throw new errors_1.CliError("This is a skill. Use 'orch skill install <ref>' instead.");
@@ -455,7 +443,7 @@ Examples:
455
443
  // Track analytics
456
444
  await (0, analytics_1.track)('cli_pull', {
457
445
  org,
458
- agent: parsed.agent,
446
+ agent: agentName,
459
447
  version: data.version,
460
448
  engine,
461
449
  source: data.source,
@@ -465,7 +453,7 @@ Examples:
465
453
  if (options.json) {
466
454
  const result = {
467
455
  success: true,
468
- requested_ref: `${org}/${parsed.agent}@${parsed.version}`,
456
+ requested_ref: `${org}/${agentName}@${version}`,
469
457
  resolved_ref: resolvedRef,
470
458
  output_dir: outputDir,
471
459
  engine,
@@ -91,6 +91,21 @@ function registerReplayCommand(program) {
91
91
  .option('--override-policy <id>', 'Override provider policy ID for this replay')
92
92
  .option('--no-wait', 'Queue the replay and return immediately without waiting for results')
93
93
  .option('--json', 'Output as JSON')
94
+ .addHelpText('after', `
95
+ How snapshots work:
96
+ Every cloud run automatically captures a snapshot of its input, config,
97
+ secrets, and agent version before execution. This snapshot enables
98
+ deterministic replay — even if the agent has been updated since.
99
+
100
+ Local runs (--local) do not create snapshots and cannot be replayed.
101
+ Runs created before snapshot support was added also cannot be replayed.
102
+
103
+ Examples:
104
+ orch replay a1b2c3d4 Replay and wait for results
105
+ orch replay a1b2c3d4 --no-wait Queue and return immediately
106
+ orch replay a1b2c3d4 --reason "debug" Replay with audit reason
107
+ orch replay a1b2c3d4 --json Output as JSON
108
+ `)
94
109
  .action(async (runId, options) => {
95
110
  const config = await (0, config_1.getResolvedConfig)();
96
111
  if (!config.apiKey) {
@@ -109,15 +124,33 @@ function registerReplayCommand(program) {
109
124
  throw new errors_1.CliError(`Invalid run ID '${runId}'. Provide a full UUID or a short hex prefix (7+ characters).`);
110
125
  }
111
126
  // Submit replay request
112
- const body = {};
127
+ const reqBody = {};
113
128
  if (options.reason)
114
- body.reason = options.reason;
129
+ reqBody.reason = options.reason;
115
130
  if (options.overridePolicy)
116
- body.override_provider_policy_id = options.overridePolicy;
117
- const replay = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/runs/${resolvedRunId}/replay`, {
118
- body: JSON.stringify(body),
119
- headers: { 'Content-Type': 'application/json' },
120
- });
131
+ reqBody.override_provider_policy_id = options.overridePolicy;
132
+ let replay;
133
+ try {
134
+ replay = await (0, api_1.request)(config, 'POST', `/workspaces/${workspaceId}/runs/${resolvedRunId}/replay`, {
135
+ body: JSON.stringify(reqBody),
136
+ headers: { 'Content-Type': 'application/json' },
137
+ });
138
+ }
139
+ catch (err) {
140
+ if (err instanceof api_1.ApiError && err.status === 404) {
141
+ const isSnapshotMissing = err.message.toLowerCase().includes('snapshot');
142
+ if (isSnapshotMissing) {
143
+ throw new errors_1.CliError(`No snapshot available for run ${resolvedRunId.slice(0, 8)}.\n\n` +
144
+ `Snapshots are captured automatically for cloud runs. This run may have been:\n` +
145
+ ` - A local run (--local), which does not create snapshots\n` +
146
+ ` - Created before snapshot support was added\n\n` +
147
+ `Run \`orch logs ${resolvedRunId.slice(0, 8)}\` to inspect the original run.`);
148
+ }
149
+ throw new errors_1.CliError(`Run ${resolvedRunId.slice(0, 8)} not found. ` +
150
+ `Check the run ID and workspace, or run \`orch logs\` to list recent runs.`);
151
+ }
152
+ throw err;
153
+ }
121
154
  if (options.json && options.wait === false) {
122
155
  (0, output_1.printJson)(replay);
123
156
  return;
@@ -54,6 +54,7 @@ const child_process_1 = require("child_process");
54
54
  const chalk_1 = __importDefault(require("chalk"));
55
55
  const dotenv_1 = require("../lib/dotenv");
56
56
  const config_1 = require("../lib/config");
57
+ const resolve_agent_1 = require("../lib/resolve-agent");
57
58
  const api_1 = require("../lib/api");
58
59
  const errors_1 = require("../lib/errors");
59
60
  const output_1 = require("../lib/output");
@@ -84,18 +85,6 @@ function localCommandForEntrypoint(entrypoint) {
84
85
  }
85
86
  return 'python3';
86
87
  }
87
- function parseAgentRef(value) {
88
- const [ref, versionPart] = value.split('@');
89
- const version = versionPart?.trim() || DEFAULT_VERSION;
90
- const segments = ref.split('/');
91
- if (segments.length === 1) {
92
- return { agent: segments[0], version };
93
- }
94
- if (segments.length === 2) {
95
- return { org: segments[0], agent: segments[1], version };
96
- }
97
- throw new errors_1.CliError('Invalid agent reference. Use org/agent or agent format.');
98
- }
99
88
  function canonicalAgentType(typeValue) {
100
89
  const normalized = (typeValue || 'agent').toLowerCase();
101
90
  // Handle legacy type names: agentic → agent, code → tool
@@ -798,6 +787,13 @@ async function detectAllLlmKeys(supportedProviders, config) {
798
787
  return providers;
799
788
  }
800
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
+ }
801
797
  if (providerOverride) {
802
798
  (0, llm_1.validateProvider)(providerOverride);
803
799
  }
@@ -816,7 +812,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
816
812
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
817
813
  `Set an environment variable (e.g., OPENAI_API_KEY), run 'orch secrets set <PROVIDER>_API_KEY <key>', or configure in web dashboard`);
818
814
  }
819
- if (modelOverride && !providerOverride && allProviders.length > 1) {
815
+ if (modelOverride && allProviders.length > 1) {
820
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` +
821
817
  `Consider specifying --provider to ensure correct model/provider pairing.\n\n`);
822
818
  }
@@ -866,6 +862,13 @@ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, cu
866
862
  throw new errors_1.CliError('Python 3 is required for local agent execution.\n' +
867
863
  'Install Python 3: https://python.org/downloads');
868
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
+ }
869
872
  // 2. Detect LLM provider + key
870
873
  const supportedProviders = manifest?.supported_providers || ['any'];
871
874
  const providersToCheck = providerOverride
@@ -1869,15 +1872,8 @@ async function executeCloud(agentRef, file, options) {
1869
1872
  if (!resolved.apiKey) {
1870
1873
  throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
1871
1874
  }
1872
- const parsed = parseAgentRef(agentRef);
1873
- const configFile = await (0, config_1.loadConfig)();
1874
- const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
1875
- if (!org) {
1876
- throw new errors_1.CliError('Missing org. Use org/agent or set default org.');
1877
- }
1878
- // Resolve workspace context for the target org
1879
- const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(resolved, org);
1880
- const agentMeta = await (0, api_1.getAgentWithFallback)(resolved, org, parsed.agent, parsed.version, workspaceId);
1875
+ const { org, agent: agentName, version, workspaceId } = await (0, resolve_agent_1.resolveAgentContext)(agentRef, resolved);
1876
+ const agentMeta = await (0, api_1.getAgentWithFallback)(resolved, org, agentName, version, workspaceId);
1881
1877
  const cloudType = canonicalAgentType(agentMeta.type);
1882
1878
  const cloudEngine = resolveExecutionEngine({
1883
1879
  type: agentMeta.type,
@@ -1891,10 +1887,8 @@ async function executeCloud(agentRef, file, options) {
1891
1887
  const agentRequiredSecrets = agentMeta.required_secrets;
1892
1888
  if (agentRequiredSecrets?.length) {
1893
1889
  try {
1894
- // Use already-resolved workspaceId (or fall back to config workspace slug)
1895
- const wsId = workspaceId ?? (configFile.workspace ? (await (0, api_1.resolveWorkspaceIdForOrg)(resolved, configFile.workspace)) : undefined);
1896
- if (wsId) {
1897
- const secretsResult = await (0, api_1.request)(resolved, 'GET', `/workspaces/${wsId}/secrets`);
1890
+ if (workspaceId) {
1891
+ const secretsResult = await (0, api_1.request)(resolved, 'GET', `/workspaces/${workspaceId}/secrets`);
1898
1892
  const existingNames = new Set(secretsResult.secrets.map((s) => s.name));
1899
1893
  const missing = agentRequiredSecrets.filter((s) => !existingNames.has(s));
1900
1894
  if (missing.length > 0) {
@@ -1918,7 +1912,7 @@ async function executeCloud(agentRef, file, options) {
1918
1912
  // --estimate-only: show cost estimate and exit without running
1919
1913
  if (options.estimate || options.estimateOnly) {
1920
1914
  try {
1921
- const est = await (0, api_1.getAgentCostEstimate)(resolved, org, parsed.agent, parsed.version);
1915
+ const est = await (0, api_1.getAgentCostEstimate)(resolved, org, agentName, version);
1922
1916
  const e = est.estimate;
1923
1917
  if (e.sample_size === 0) {
1924
1918
  process.stderr.write(chalk_1.default.yellow('\nNo run history available for cost estimation.\n'));
@@ -1988,18 +1982,8 @@ async function executeCloud(agentRef, file, options) {
1988
1982
  throw new errors_1.CliError('When using --key, you must also specify --provider (openai, anthropic, or gemini)');
1989
1983
  }
1990
1984
  (0, llm_1.validateProvider)(effectiveProvider);
1991
- if (options.model && effectiveProvider) {
1992
- const modelLower = options.model.toLowerCase();
1993
- const providerPatterns = {
1994
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
1995
- anthropic: /^claude-/,
1996
- gemini: /^gemini-/,
1997
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
1998
- };
1999
- const expectedPattern = providerPatterns[effectiveProvider];
2000
- if (expectedPattern && !expectedPattern.test(modelLower)) {
2001
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
2002
- }
1985
+ if (options.model) {
1986
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
2003
1987
  }
2004
1988
  llmKey = options.key;
2005
1989
  llmProvider = effectiveProvider;
@@ -2010,17 +1994,7 @@ async function executeCloud(agentRef, file, options) {
2010
1994
  (0, llm_1.validateProvider)(effectiveProvider);
2011
1995
  providersToCheck = [effectiveProvider];
2012
1996
  if (options.model) {
2013
- const modelLower = options.model.toLowerCase();
2014
- const providerPatterns = {
2015
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
2016
- anthropic: /^claude-/,
2017
- gemini: /^gemini-/,
2018
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
2019
- };
2020
- const expectedPattern = providerPatterns[effectiveProvider];
2021
- if (expectedPattern && !expectedPattern.test(modelLower)) {
2022
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
2023
- }
1997
+ (0, llm_1.warnProviderModelMismatch)(options.model, effectiveProvider);
2024
1998
  }
2025
1999
  }
2026
2000
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, resolved);
@@ -2245,7 +2219,7 @@ async function executeCloud(agentRef, file, options) {
2245
2219
  }
2246
2220
  } // end of non-injection path
2247
2221
  const verboseQs = options.verbose ? '?verbose=true' : '';
2248
- const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}${verboseQs}`;
2222
+ const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${agentName}/${version}/${endpoint}${verboseQs}`;
2249
2223
  // Enable SSE streaming for sandbox-backed engines (unless --json or --no-stream or --output)
2250
2224
  const isManagedLoopAgent = cloudType === 'agent' && cloudEngine === 'managed_loop';
2251
2225
  const isCodeRuntimeAgent = cloudEngine === 'code_runtime';
@@ -2257,13 +2231,13 @@ async function executeCloud(agentRef, file, options) {
2257
2231
  }
2258
2232
  // Print verbose debug info before request
2259
2233
  if (options.verbose && !options.json) {
2260
- process.stderr.write(chalk_1.default.gray(`\n[verbose] ${org}/${parsed.agent}@${parsed.version}\n` +
2234
+ process.stderr.write(chalk_1.default.gray(`\n[verbose] ${org}/${agentName}@${version}\n` +
2261
2235
  `[verbose] type=${cloudType} engine=${cloudEngine} endpoint=${endpoint}\n` +
2262
2236
  `[verbose] stream=${wantStream ? 'yes' : 'no'} url=${url}\n`));
2263
2237
  }
2264
2238
  const { spinner, dispose: disposeSpinner } = options.json
2265
2239
  ? { spinner: null, dispose: () => { } }
2266
- : (0, spinner_1.createElapsedSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
2240
+ : (0, spinner_1.createElapsedSpinner)(`Running ${org}/${agentName}@${version}...`);
2267
2241
  spinner?.start();
2268
2242
  // Streamed sandbox runs can take longer; use 10 min timeout (or --wait-timeout).
2269
2243
  const waitTimeoutSec = options.waitTimeout ? parseInt(options.waitTimeout, 10) : undefined;
@@ -2432,11 +2406,11 @@ async function executeCloud(agentRef, file, options) {
2432
2406
  let finalPayload = null;
2433
2407
  let hadError = false;
2434
2408
  if (options.verbose) {
2435
- process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version} (verbose):\n`));
2409
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${agentName}@${version} (verbose):\n`));
2436
2410
  process.stderr.write(chalk_1.default.gray(` type=${cloudType} engine=${cloudEngine} endpoint=${endpoint}\n`));
2437
2411
  }
2438
2412
  else {
2439
- process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
2413
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${agentName}@${version}:\n`));
2440
2414
  }
2441
2415
  let progressErrorShown = false;
2442
2416
  let streamTimedOut = false;
@@ -2497,7 +2471,7 @@ async function executeCloud(agentRef, file, options) {
2497
2471
  ? ''
2498
2472
  : chalk_1.default.gray('Tip: Use --wait-timeout <seconds> to wait longer.\n')));
2499
2473
  await (0, analytics_1.track)('cli_run', {
2500
- agent: `${org}/${parsed.agent}@${parsed.version}`,
2474
+ agent: `${org}/${agentName}@${version}`,
2501
2475
  input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
2502
2476
  mode: 'cloud',
2503
2477
  streamed: true,
@@ -2508,7 +2482,7 @@ async function executeCloud(agentRef, file, options) {
2508
2482
  throw err;
2509
2483
  }
2510
2484
  await (0, analytics_1.track)('cli_run', {
2511
- agent: `${org}/${parsed.agent}@${parsed.version}`,
2485
+ agent: `${org}/${agentName}@${version}`,
2512
2486
  input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
2513
2487
  mode: 'cloud',
2514
2488
  streamed: true,
@@ -2549,14 +2523,14 @@ async function executeCloud(agentRef, file, options) {
2549
2523
  }
2550
2524
  const runId = response.headers?.get?.('x-run-id');
2551
2525
  if (runId) {
2552
- process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2526
+ process.stderr.write(chalk_1.default.gray(`Snapshot saved · Logs: orch logs ${runId} · Replay: orch replay ${runId}\n`));
2553
2527
  }
2554
2528
  }
2555
2529
  }
2556
2530
  }
2557
2531
  return;
2558
2532
  }
2559
- spinner?.succeed(`Ran ${org}/${parsed.agent}@${parsed.version}`);
2533
+ spinner?.succeed(`Ran ${org}/${agentName}@${version}`);
2560
2534
  const inputType = hasInjection
2561
2535
  ? 'file_injection'
2562
2536
  : unkeyedFileArgs.length > 0
@@ -2569,7 +2543,7 @@ async function executeCloud(agentRef, file, options) {
2569
2543
  ? 'metadata'
2570
2544
  : 'empty';
2571
2545
  await (0, analytics_1.track)('cli_run', {
2572
- agent: `${org}/${parsed.agent}@${parsed.version}`,
2546
+ agent: `${org}/${agentName}@${version}`,
2573
2547
  input_type: inputType,
2574
2548
  mode: 'cloud',
2575
2549
  });
@@ -2638,7 +2612,7 @@ async function executeCloud(agentRef, file, options) {
2638
2612
  }
2639
2613
  const runId = response.headers?.get?.('x-run-id');
2640
2614
  if (runId) {
2641
- process.stderr.write(chalk_1.default.gray(`View logs: orch logs ${runId}\n`));
2615
+ process.stderr.write(chalk_1.default.gray(`Snapshot saved · Logs: orch logs ${runId} · Replay: orch replay ${runId}\n`));
2642
2616
  }
2643
2617
  }
2644
2618
  }
@@ -2657,34 +2631,24 @@ async function executeLocal(agentRef, args, options) {
2657
2631
  options.input = JSON.stringify({ path: options.path });
2658
2632
  }
2659
2633
  if (options.model && options.provider) {
2660
- const modelLower = options.model.toLowerCase();
2661
- const providerPatterns = {
2662
- openai: /^(gpt-|o1-|o3-|davinci|text-)/,
2663
- anthropic: /^claude-/,
2664
- gemini: /^gemini-/,
2665
- ollama: /^(llama|mistral|deepseek|phi|qwen)/,
2666
- };
2667
- const expectedPattern = providerPatterns[options.provider];
2668
- if (expectedPattern && !expectedPattern.test(modelLower)) {
2669
- 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`);
2670
2641
  }
2671
2642
  }
2672
2643
  const resolved = await (0, config_1.getResolvedConfig)();
2673
- const parsed = parseAgentRef(agentRef);
2674
- const configFile = await (0, config_1.loadConfig)();
2675
- const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
2676
- if (!org) {
2677
- throw new errors_1.CliError('Missing org. Use org/agent format.');
2678
- }
2679
- // Resolve workspace context for the target org
2680
- const workspaceId = await (0, api_1.resolveWorkspaceIdForOrg)(resolved, org);
2644
+ const { org, agent: localAgentName, version: localVersion, workspaceId } = await (0, resolve_agent_1.resolveAgentContext)(agentRef, resolved);
2681
2645
  // Download agent definition with spinner
2682
- const agentData = await (0, spinner_1.withSpinner)(`Downloading ${org}/${parsed.agent}@${parsed.version}...`, async () => {
2646
+ const agentData = await (0, spinner_1.withSpinner)(`Downloading ${org}/${localAgentName}@${localVersion}...`, async () => {
2683
2647
  try {
2684
- return await downloadAgent(resolved, org, parsed.agent, parsed.version, workspaceId);
2648
+ return await downloadAgent(resolved, org, localAgentName, localVersion, workspaceId);
2685
2649
  }
2686
2650
  catch (err) {
2687
- const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, parsed.agent, parsed.version);
2651
+ const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, localAgentName, localVersion);
2688
2652
  return {
2689
2653
  type: agentMeta.type || 'agent',
2690
2654
  run_mode: agentMeta.run_mode ?? null,
@@ -2696,7 +2660,7 @@ async function executeLocal(agentRef, args, options) {
2696
2660
  supported_providers: agentMeta.supported_providers || ['any'],
2697
2661
  };
2698
2662
  }
2699
- }, { successText: `Downloaded ${org}/${parsed.agent}@${parsed.version}` });
2663
+ }, { successText: `Downloaded ${org}/${localAgentName}@${localVersion}` });
2700
2664
  const localType = canonicalAgentType(agentData.type);
2701
2665
  const localEngine = resolveExecutionEngine(agentData);
2702
2666
  // Skills cannot be run directly
@@ -2704,8 +2668,8 @@ async function executeLocal(agentRef, args, options) {
2704
2668
  throw new errors_1.CliError('Skills cannot be run directly.\n\n' +
2705
2669
  'Skills are instructions meant to be injected into AI agent contexts.\n\n' +
2706
2670
  'Options:\n' +
2707
- ` Install for AI tools: orchagent skill install ${org}/${parsed.agent}\n` +
2708
- ` Use with an agent: orchagent run <agent> --skills ${org}/${parsed.agent}`);
2671
+ ` Install for AI tools: orchagent skill install ${org}/${localAgentName}\n` +
2672
+ ` Use with an agent: orchagent run <agent> --skills ${org}/${localAgentName}`);
2709
2673
  }
2710
2674
  // Managed-loop agents execute locally with the agent runner.
2711
2675
  if (localEngine === 'managed_loop') {
@@ -2713,11 +2677,11 @@ async function executeLocal(agentRef, args, options) {
2713
2677
  throw new errors_1.CliError('Agent prompt not available for local execution.\n\n' +
2714
2678
  'This agent may have local download disabled.\n' +
2715
2679
  'Remove the --local flag to run in the cloud:\n' +
2716
- ` orch run ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
2680
+ ` orch run ${org}/${localAgentName}@${localVersion} --data '{"task": "..."}'`);
2717
2681
  }
2718
2682
  if (!options.input) {
2719
2683
  process.stderr.write(`\nAgent downloaded. Run with:\n`);
2720
- process.stderr.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --data '{\"task\": \"...\"}'\n`);
2684
+ process.stderr.write(` orch run ${org}/${localAgentName}@${localVersion} --local --data '{\"task\": \"...\"}'\n`);
2721
2685
  return;
2722
2686
  }
2723
2687
  // Resolve @file.json / @- stdin syntax before parsing
@@ -2731,7 +2695,7 @@ async function executeLocal(agentRef, args, options) {
2731
2695
  }
2732
2696
  warnInputSchemaErrors(agentInputData, agentData.input_schema);
2733
2697
  // Write prompt to temp dir and run
2734
- const tempAgentDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-${parsed.agent}-${Date.now()}`);
2698
+ const tempAgentDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-${localAgentName}-${Date.now()}`);
2735
2699
  await promises_1.default.mkdir(tempAgentDir, { recursive: true });
2736
2700
  try {
2737
2701
  await promises_1.default.writeFile(path_1.default.join(tempAgentDir, 'prompt.md'), agentData.prompt);
@@ -2761,7 +2725,7 @@ async function executeLocal(agentRef, args, options) {
2761
2725
  }
2762
2726
  if (choice === 'server') {
2763
2727
  process.stderr.write(`\nRun without --local for server execution:\n`);
2764
- process.stderr.write(` orch run ${org}/${parsed.agent}@${parsed.version} --data '{...}'\n\n`);
2728
+ process.stderr.write(` orch run ${org}/${localAgentName}@${localVersion} --data '{...}'\n\n`);
2765
2729
  process.exit(0);
2766
2730
  }
2767
2731
  await downloadDependenciesRecursively(resolved, depStatuses, new Set(), workspaceId);
@@ -2784,13 +2748,13 @@ async function executeLocal(agentRef, args, options) {
2784
2748
  }
2785
2749
  }
2786
2750
  // Save locally
2787
- const agentDir = await saveAgentLocally(org, parsed.agent, agentData);
2751
+ const agentDir = await saveAgentLocally(org, localAgentName, agentData);
2788
2752
  process.stderr.write(`\nAgent saved to: ${agentDir}\n`);
2789
2753
  if (localEngine === 'code_runtime') {
2790
2754
  if (agentData.has_bundle) {
2791
2755
  if (options.downloadOnly) {
2792
2756
  process.stdout.write(`\nCode runtime bundle is available for local execution.\n`);
2793
- process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
2757
+ process.stdout.write(`Run with: orch run ${org}/${localAgentName} --local [args...]\n`);
2794
2758
  return;
2795
2759
  }
2796
2760
  // Pre-build injected payload for bundle agent if keyed files/mounts present
@@ -2806,13 +2770,13 @@ async function executeLocal(agentRef, args, options) {
2806
2770
  });
2807
2771
  bundleInput = injected.body;
2808
2772
  }
2809
- await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, bundleInput, workspaceId);
2773
+ await executeBundleAgent(resolved, org, localAgentName, localVersion, agentData, args, bundleInput, workspaceId);
2810
2774
  return;
2811
2775
  }
2812
2776
  if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
2813
2777
  if (options.downloadOnly) {
2814
2778
  process.stdout.write(`\nTool ready for local execution.\n`);
2815
- process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
2779
+ process.stdout.write(`Run with: orch run ${org}/${localAgentName} --local [args...]\n`);
2816
2780
  return;
2817
2781
  }
2818
2782
  await executeTool(agentData, args);
@@ -2820,12 +2784,12 @@ async function executeLocal(agentRef, args, options) {
2820
2784
  }
2821
2785
  // Fallback: code runtime agent doesn't support local execution.
2822
2786
  process.stdout.write(`\nThis code runtime agent is configured for server execution.\n`);
2823
- process.stdout.write(`\nRun without --local: orch run ${org}/${parsed.agent}@${parsed.version} --data '{...}'\n`);
2787
+ process.stdout.write(`\nRun without --local: orch run ${org}/${localAgentName}@${localVersion} --data '{...}'\n`);
2824
2788
  return;
2825
2789
  }
2826
2790
  if (options.downloadOnly) {
2827
2791
  process.stdout.write(`\nAgent downloaded. Run with:\n`);
2828
- process.stdout.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
2792
+ process.stdout.write(` orch run ${org}/${localAgentName}@${localVersion} --local --input '{...}'\n`);
2829
2793
  return;
2830
2794
  }
2831
2795
  // Check for keyed file/mount injection
@@ -2835,7 +2799,7 @@ async function executeLocal(agentRef, args, options) {
2835
2799
  // Direct LLM agents execute locally via prompt composition.
2836
2800
  if (!options.input && !execLocalHasInjection) {
2837
2801
  process.stdout.write(`\nAgent ready.\n`);
2838
- process.stdout.write(`Run with: orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
2802
+ process.stdout.write(`Run with: orch run ${org}/${localAgentName}@${localVersion} --local --input '{...}'\n`);
2839
2803
  return;
2840
2804
  }
2841
2805
  let inputData;