@orchagent/cli 0.3.43 → 0.3.44

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.
@@ -47,6 +47,8 @@ const errors_1 = require("../lib/errors");
47
47
  const output_1 = require("../lib/output");
48
48
  const spinner_1 = require("../lib/spinner");
49
49
  const llm_1 = require("../lib/llm");
50
+ const analytics_1 = require("../lib/analytics");
51
+ const pricing_1 = require("../lib/pricing");
50
52
  const DEFAULT_VERSION = 'latest';
51
53
  const AGENTS_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent', 'agents');
52
54
  // Local execution environment variables
@@ -56,6 +58,10 @@ const CALL_CHAIN_ENV = 'ORCHAGENT_CALL_CHAIN';
56
58
  const DEADLINE_MS_ENV = 'ORCHAGENT_DEADLINE_MS';
57
59
  const MAX_HOPS_ENV = 'ORCHAGENT_MAX_HOPS';
58
60
  const DOWNSTREAM_REMAINING_ENV = 'ORCHAGENT_DOWNSTREAM_REMAINING';
61
+ // Well-known field names for file content in prompt agent schemas (priority order)
62
+ const CONTENT_FIELD_NAMES = ['code', 'content', 'text', 'source', 'input', 'file_content', 'body'];
63
+ // Keys that might indicate local file path references in JSON payloads
64
+ const LOCAL_PATH_KEYS = ['path', 'directory', 'file', 'filepath', 'dir', 'folder', 'local'];
59
65
  function parseAgentRef(value) {
60
66
  const [ref, versionPart] = value.split('@');
61
67
  const version = versionPart?.trim() || DEFAULT_VERSION;
@@ -68,6 +74,125 @@ function parseAgentRef(value) {
68
74
  }
69
75
  throw new errors_1.CliError('Invalid agent reference. Use org/agent or agent format.');
70
76
  }
77
+ // ─── Cloud execution helpers (from call.ts) ─────────────────────────────────
78
+ function findLocalPathKey(obj) {
79
+ if (typeof obj !== 'object' || obj === null) {
80
+ return undefined;
81
+ }
82
+ const keys = Object.keys(obj);
83
+ for (const key of keys) {
84
+ if (LOCAL_PATH_KEYS.includes(key.toLowerCase())) {
85
+ return key;
86
+ }
87
+ }
88
+ return undefined;
89
+ }
90
+ function warnIfLocalPathReference(jsonBody) {
91
+ try {
92
+ const parsed = JSON.parse(jsonBody);
93
+ const pathKey = findLocalPathKey(parsed);
94
+ if (pathKey) {
95
+ process.stderr.write(`Warning: Your payload contains a local path reference ('${pathKey}').\n` +
96
+ `Remote agents cannot access your local filesystem. The path will be interpreted\n` +
97
+ `by the server, not your local machine.\n\n` +
98
+ `Tip: Use 'orch run <agent> --local' to execute locally with filesystem access.\n\n`);
99
+ }
100
+ }
101
+ catch {
102
+ // If parsing fails, skip the warning
103
+ }
104
+ }
105
+ function inferFileField(inputSchema) {
106
+ if (!inputSchema || typeof inputSchema !== 'object')
107
+ return 'content';
108
+ const props = inputSchema.properties;
109
+ if (!props || typeof props !== 'object')
110
+ return 'content';
111
+ const properties = props;
112
+ for (const field of CONTENT_FIELD_NAMES) {
113
+ if (properties[field] && properties[field].type === 'string')
114
+ return field;
115
+ }
116
+ const required = (inputSchema.required ?? []);
117
+ const stringProps = Object.entries(properties)
118
+ .filter(([, v]) => v.type === 'string')
119
+ .map(([k]) => k);
120
+ if (stringProps.length === 1)
121
+ return stringProps[0];
122
+ const requiredStrings = stringProps.filter(k => required.includes(k));
123
+ if (requiredStrings.length === 1)
124
+ return requiredStrings[0];
125
+ return 'content';
126
+ }
127
+ async function readStdin() {
128
+ if (process.stdin.isTTY)
129
+ return null;
130
+ const chunks = [];
131
+ for await (const chunk of process.stdin) {
132
+ chunks.push(Buffer.from(chunk));
133
+ }
134
+ if (!chunks.length)
135
+ return null;
136
+ return Buffer.concat(chunks);
137
+ }
138
+ async function buildMultipartBody(filePaths, metadata) {
139
+ if (!filePaths || filePaths.length === 0) {
140
+ const stdinData = await readStdin();
141
+ if (stdinData) {
142
+ const form = new FormData();
143
+ form.append('files[]', new Blob([new Uint8Array(stdinData)]), 'stdin');
144
+ if (metadata) {
145
+ form.append('metadata', metadata);
146
+ }
147
+ return { body: form, sourceLabel: 'stdin' };
148
+ }
149
+ if (metadata) {
150
+ const form = new FormData();
151
+ form.append('metadata', metadata);
152
+ return { body: form, sourceLabel: 'metadata' };
153
+ }
154
+ return {};
155
+ }
156
+ const form = new FormData();
157
+ for (const filePath of filePaths) {
158
+ const buffer = await promises_1.default.readFile(filePath);
159
+ const filename = path_1.default.basename(filePath);
160
+ form.append('files[]', new Blob([new Uint8Array(buffer)]), filename);
161
+ }
162
+ if (metadata) {
163
+ form.append('metadata', metadata);
164
+ }
165
+ return {
166
+ body: form,
167
+ sourceLabel: filePaths.length === 1 ? filePaths[0] : `${filePaths.length} files`,
168
+ };
169
+ }
170
+ async function resolveJsonBody(input) {
171
+ let raw = input;
172
+ if (input.startsWith('@')) {
173
+ const source = input.slice(1);
174
+ if (!source) {
175
+ throw new errors_1.CliError('Invalid JSON input. Use a JSON string or @file.');
176
+ }
177
+ if (source === '-') {
178
+ const stdinData = await readStdin();
179
+ if (!stdinData) {
180
+ throw new errors_1.CliError('No stdin provided for JSON input.');
181
+ }
182
+ raw = stdinData.toString('utf8');
183
+ }
184
+ else {
185
+ raw = await promises_1.default.readFile(source, 'utf8');
186
+ }
187
+ }
188
+ try {
189
+ return JSON.stringify(JSON.parse(raw));
190
+ }
191
+ catch {
192
+ throw (0, errors_1.jsonInputError)('data');
193
+ }
194
+ }
195
+ // ─── Local execution helpers ────────────────────────────────────────────────
71
196
  async function downloadAgent(config, org, agent, version) {
72
197
  // Try public endpoint first
73
198
  try {
@@ -87,7 +212,6 @@ async function downloadAgent(config, org, agent, version) {
87
212
  if (matchingAgent) {
88
213
  // Owner! Fetch from authenticated endpoint
89
214
  const agentData = await (0, api_1.request)(config, 'GET', `/agents/${matchingAgent.id}`);
90
- // Convert Agent to AgentDownload format
91
215
  return {
92
216
  id: agentData.id,
93
217
  type: agentData.type,
@@ -117,11 +241,11 @@ async function downloadAgent(config, org, agent, version) {
117
241
  const price = payload.error.price_per_call_cents || 0;
118
242
  const priceStr = price ? `$${(price / 100).toFixed(2)}/call` : 'PAID';
119
243
  throw new errors_1.CliError(`This agent is paid (${priceStr}) and runs on server only.\n\n` +
120
- `Use: orch call ${org}/${agent}@${version} --input '{...}'`);
244
+ `Run without --local: orch run ${org}/${agent}@${version} --data '{...}'`);
121
245
  }
122
246
  else {
123
247
  throw new errors_1.CliError(`This agent is server-only and cannot be downloaded.\n\n` +
124
- `Use: orch call ${org}/${agent}@${version} --input '{...}'`);
248
+ `Run without --local: orch run ${org}/${agent}@${version} --data '{...}'`);
125
249
  }
126
250
  }
127
251
  }
@@ -153,7 +277,6 @@ async function downloadAgent(config, org, agent, version) {
153
277
  else {
154
278
  targetAgent = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
155
279
  }
156
- // Convert Agent to AgentDownload format
157
280
  return {
158
281
  id: targetAgent.id,
159
282
  type: targetAgent.type,
@@ -174,7 +297,6 @@ async function downloadAgent(config, org, agent, version) {
174
297
  };
175
298
  }
176
299
  async function downloadBundleWithFallback(config, org, agentName, version, agentId) {
177
- // Try public endpoint first
178
300
  try {
179
301
  return await (0, api_1.downloadCodeBundle)(config, org, agentName, version);
180
302
  }
@@ -182,7 +304,6 @@ async function downloadBundleWithFallback(config, org, agentName, version, agent
182
304
  if (!(err instanceof api_1.ApiError) || err.status !== 404)
183
305
  throw err;
184
306
  }
185
- // Fallback to authenticated endpoint
186
307
  if (!config.apiKey || !agentId) {
187
308
  throw new api_1.ApiError(`Bundle for '${org}/${agentName}@${version}' not found`, 404);
188
309
  }
@@ -198,17 +319,15 @@ async function checkDependencies(config, dependencies) {
198
319
  results.push({ dep, downloadable, agentData });
199
320
  }
200
321
  catch {
201
- // Agent not found or not downloadable
202
322
  results.push({ dep, downloadable: false });
203
323
  }
204
324
  }
205
325
  return results;
206
326
  }
207
327
  async function promptUserForDeps(depStatuses) {
208
- // In non-interactive mode (CI, piped input), skip deps by default and let agent run
209
328
  if (!process.stdin.isTTY) {
210
329
  process.stderr.write('Non-interactive mode: skipping dependencies (use --with-deps to include them).\n');
211
- return 'local'; // Skip deps, let agent run
330
+ return 'local';
212
331
  }
213
332
  const readline = await Promise.resolve().then(() => __importStar(require('readline')));
214
333
  const rl = readline.createInterface({
@@ -228,7 +347,7 @@ async function promptUserForDeps(depStatuses) {
228
347
  process.stderr.write(`Note: ${cloudOnlyCount} dependency(s) are cloud-only and cannot run locally.\n\n`);
229
348
  }
230
349
  process.stderr.write('Options:\n');
231
- process.stderr.write(' [1] Run on server (orch call) - recommended\n');
350
+ process.stderr.write(' [1] Run on server (orch run) - recommended\n');
232
351
  if (downloadableCount > 0) {
233
352
  process.stderr.write(` [2] Download ${downloadableCount} available deps, run locally\n`);
234
353
  }
@@ -263,46 +382,34 @@ async function downloadDependenciesRecursively(config, depStatuses, visited = ne
263
382
  visited.add(depRef);
264
383
  const [org, agent] = status.dep.id.split('/');
265
384
  await (0, spinner_1.withSpinner)(`Downloading dependency: ${depRef}...`, async () => {
266
- // Save the dependency metadata locally
267
385
  await saveAgentLocally(org, agent, status.agentData);
268
- // For bundle-based agents, also extract the bundle
269
386
  if (status.agentData.has_bundle) {
270
387
  await saveBundleLocally(config, org, agent, status.dep.version, status.agentData.id);
271
388
  }
272
- // Install if it's a pip/source tool agent
273
389
  if (status.agentData.type === 'tool' && (status.agentData.source_url || status.agentData.pip_package)) {
274
390
  await installTool(status.agentData);
275
391
  }
276
392
  }, { successText: `Downloaded ${depRef}` });
277
- // Download default skills
278
393
  const defaultSkills = status.agentData.default_skills || [];
279
394
  for (const skillRef of defaultSkills) {
280
395
  try {
281
396
  await downloadSkillDependency(config, skillRef, org);
282
397
  }
283
398
  catch {
284
- // Skill download failed - not critical, continue
285
399
  process.stderr.write(` Warning: Failed to download skill ${skillRef}\n`);
286
400
  }
287
401
  }
288
- // Recursively download its dependencies
289
402
  if (status.agentData.dependencies && status.agentData.dependencies.length > 0) {
290
403
  const nestedStatuses = await checkDependencies(config, status.agentData.dependencies);
291
404
  await downloadDependenciesRecursively(config, nestedStatuses, visited);
292
405
  }
293
406
  }
294
407
  }
295
- /**
296
- * Detect all available LLM providers from environment and server.
297
- * Returns array of provider configs for fallback support.
298
- */
299
408
  async function detectAllLlmKeys(supportedProviders, config) {
300
409
  const providers = [];
301
410
  const seen = new Set();
302
- // Check environment variables for all providers
303
411
  for (const provider of supportedProviders) {
304
412
  if (provider === 'any') {
305
- // Check all known providers
306
413
  for (const [p, envVar] of Object.entries(llm_1.PROVIDER_ENV_VARS)) {
307
414
  const key = process.env[envVar];
308
415
  if (key && !seen.has(p)) {
@@ -322,7 +429,6 @@ async function detectAllLlmKeys(supportedProviders, config) {
322
429
  }
323
430
  }
324
431
  }
325
- // Also check server keys if available
326
432
  if (config?.apiKey) {
327
433
  try {
328
434
  const { fetchLlmKeys } = await Promise.resolve().then(() => __importStar(require('../lib/api')));
@@ -345,22 +451,17 @@ async function detectAllLlmKeys(supportedProviders, config) {
345
451
  return providers;
346
452
  }
347
453
  async function executePromptLocally(agentData, inputData, skillPrompts = [], config, providerOverride, modelOverride) {
348
- // If provider override specified, validate and use only that provider
349
454
  if (providerOverride) {
350
455
  (0, llm_1.validateProvider)(providerOverride);
351
456
  }
352
- // Determine which providers to check for keys
353
457
  const providersToCheck = providerOverride
354
458
  ? [providerOverride]
355
459
  : agentData.supported_providers;
356
- // Combine skill prompts with agent prompt (skills first, then agent)
357
460
  let basePrompt = agentData.prompt || '';
358
461
  if (skillPrompts.length > 0) {
359
462
  basePrompt = [...skillPrompts, basePrompt].join('\n\n---\n\n');
360
463
  }
361
- // Build the prompt with input data (matches server behavior)
362
464
  const prompt = (0, llm_1.buildPrompt)(basePrompt, inputData);
363
- // When no provider override, detect all available providers for fallback support
364
465
  if (!providerOverride) {
365
466
  const allProviders = await detectAllLlmKeys(providersToCheck, config);
366
467
  if (allProviders.length === 0) {
@@ -368,22 +469,18 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
368
469
  throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
369
470
  `Set an environment variable (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
370
471
  }
371
- // Warn if --model specified without --provider and multiple providers available
372
472
  if (modelOverride && !providerOverride && allProviders.length > 1) {
373
473
  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` +
374
474
  `Consider specifying --provider to ensure correct model/provider pairing.\n\n`);
375
475
  }
376
- // Apply agent default models to each provider config
377
476
  const providersWithModels = allProviders.map((p) => ({
378
477
  ...p,
379
478
  model: modelOverride || p.model || agentData.default_models?.[p.provider] || (0, llm_1.getDefaultModel)(p.provider),
380
479
  }));
381
- // Show which provider is being used (primary)
382
480
  const primary = providersWithModels[0];
383
481
  const spinnerText = providersWithModels.length > 1
384
482
  ? `Running with ${primary.provider} (${primary.model}), ${providersWithModels.length - 1} fallback(s) available...`
385
483
  : `Running with ${primary.provider} (${primary.model})...`;
386
- // Use fallback if multiple providers, otherwise single call
387
484
  return await (0, spinner_1.withSpinner)(spinnerText, async () => {
388
485
  if (providersWithModels.length > 1) {
389
486
  return await (0, llm_1.callLlmWithFallback)(providersWithModels, prompt, agentData.output_schema);
@@ -393,7 +490,6 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
393
490
  }
394
491
  }, { successText: `Completed with ${primary.provider}` });
395
492
  }
396
- // Provider override: use single provider (existing behavior)
397
493
  const detected = await (0, llm_1.detectLlmKey)(providersToCheck, config);
398
494
  if (!detected) {
399
495
  const providers = providersToCheck.join(', ');
@@ -401,9 +497,7 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
401
497
  `Set an environment variable (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
402
498
  }
403
499
  const { provider, key, model: serverModel } = detected;
404
- // Priority: CLI override > server config model > agent default model > hardcoded default
405
500
  const model = modelOverride || serverModel || agentData.default_models?.[provider] || (0, llm_1.getDefaultModel)(provider);
406
- // Call the LLM with spinner
407
501
  return await (0, spinner_1.withSpinner)(`Running with ${provider} (${model})...`, async () => {
408
502
  return await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
409
503
  }, { successText: `Completed with ${provider}` });
@@ -428,14 +522,11 @@ async function loadSkillPrompts(config, skillRefs, defaultOrg) {
428
522
  if (!org) {
429
523
  throw new errors_1.CliError(`Missing org for skill: ${ref}. Use org/skill format.`);
430
524
  }
431
- // Fetch skill metadata
432
525
  const skillMeta = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${parsed.skill}/${parsed.version}`);
433
- // Verify it's a skill
434
526
  const skillType = skillMeta.type;
435
527
  if (skillType !== 'skill') {
436
528
  throw new errors_1.CliError(`${org}/${parsed.skill} is not a skill (type: ${skillType || 'prompt'})`);
437
529
  }
438
- // Get the skill prompt (need to download for full content)
439
530
  const skillData = await (0, api_1.publicRequest)(config, `/public/agents/${org}/${parsed.skill}/${parsed.version}/download`);
440
531
  if (!skillData.prompt) {
441
532
  throw new errors_1.CliError(`Skill has no content: ${ref}`);
@@ -480,9 +571,8 @@ async function installTool(agentData) {
480
571
  const installSource = agentData.pip_package || agentData.source_url;
481
572
  if (!installSource) {
482
573
  throw new errors_1.CliError('This tool does not support local execution.\n' +
483
- 'Use `orch call` to run it on the server instead.');
574
+ 'Remove the --local flag to run it on the server.');
484
575
  }
485
- // Check if already installed (for pip packages)
486
576
  if (agentData.pip_package) {
487
577
  const installed = await checkPackageInstalled(agentData.pip_package);
488
578
  if (installed) {
@@ -506,11 +596,9 @@ async function installTool(agentData) {
506
596
  async function executeTool(agentData, args) {
507
597
  if (!agentData.run_command) {
508
598
  throw new errors_1.CliError('This tool does not have a run command defined.\n' +
509
- 'Use `orch call` to run it on the server instead.');
599
+ 'Remove the --local flag to run it on the server.');
510
600
  }
511
- // Install the agent if needed
512
601
  await installTool(agentData);
513
- // Parse the run command and append user args
514
602
  const [cmd, ...cmdArgs] = agentData.run_command.split(' ');
515
603
  const fullArgs = [...cmdArgs, ...args];
516
604
  process.stderr.write(`\nRunning: ${cmd} ${fullArgs.join(' ')}\n\n`);
@@ -520,7 +608,6 @@ async function executeTool(agentData, args) {
520
608
  }
521
609
  }
522
610
  async function unzipBundle(zipPath, destDir) {
523
- // Use spawn with array arguments to avoid shell injection
524
611
  return new Promise((resolve, reject) => {
525
612
  const proc = (0, child_process_1.spawn)('unzip', ['-q', zipPath, '-d', destDir], {
526
613
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -545,26 +632,21 @@ async function unzipBundle(zipPath, destDir) {
545
632
  });
546
633
  }
547
634
  async function executeBundleAgent(config, org, agentName, version, agentData, args, inputOption) {
548
- // Capture the user's working directory before we change anything
549
635
  const userCwd = process.cwd();
550
- // Create temp directory for the bundle
551
636
  const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-${agentName}-${Date.now()}`);
552
637
  await promises_1.default.mkdir(tempDir, { recursive: true });
553
638
  const bundleZip = path_1.default.join(tempDir, 'bundle.zip');
554
639
  const extractDir = path_1.default.join(tempDir, 'agent');
555
640
  try {
556
- // Download the bundle with spinner
557
641
  const bundleBuffer = await (0, spinner_1.withSpinner)(`Downloading ${org}/${agentName}@${version} bundle...`, async () => {
558
642
  const buffer = await downloadBundleWithFallback(config, org, agentName, version, agentData.id);
559
643
  await promises_1.default.writeFile(bundleZip, buffer);
560
644
  return buffer;
561
645
  }, { successText: (buf) => `Downloaded bundle (${buf.length} bytes)` });
562
- // Extract the bundle with spinner
563
646
  await promises_1.default.mkdir(extractDir, { recursive: true });
564
647
  await (0, spinner_1.withSpinner)('Extracting bundle...', async () => {
565
648
  await unzipBundle(bundleZip, extractDir);
566
649
  }, { successText: 'Bundle extracted' });
567
- // Check if requirements.txt exists and install dependencies
568
650
  const requirementsPath = path_1.default.join(extractDir, 'requirements.txt');
569
651
  try {
570
652
  await promises_1.default.access(requirementsPath);
@@ -579,26 +661,19 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
579
661
  if (err.code !== 'ENOENT') {
580
662
  throw err;
581
663
  }
582
- // requirements.txt doesn't exist, skip installation
583
664
  }
584
- // Determine entrypoint
585
665
  const entrypoint = agentData.entrypoint || 'sandbox_main.py';
586
666
  const entrypointPath = path_1.default.join(extractDir, entrypoint);
587
- // Verify entrypoint exists
588
667
  try {
589
668
  await promises_1.default.access(entrypointPath);
590
669
  }
591
670
  catch {
592
671
  throw new errors_1.CliError(`Entrypoint not found: ${entrypoint}`);
593
672
  }
594
- // Build input JSON from --input option or positional args
595
673
  let inputJson = '{}';
596
674
  if (inputOption) {
597
- // --input was provided, use it directly (should be valid JSON)
598
675
  try {
599
- // Parse and re-stringify to validate JSON
600
676
  const parsed = JSON.parse(inputOption);
601
- // Resolve any relative paths in the input to absolute paths
602
677
  if (typeof parsed === 'object' && parsed !== null) {
603
678
  for (const key of ['path', 'directory', 'file_path']) {
604
679
  if (typeof parsed[key] === 'string' && !path_1.default.isAbsolute(parsed[key])) {
@@ -614,63 +689,48 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
614
689
  }
615
690
  else if (args.length > 0) {
616
691
  const firstArg = args[0];
617
- // Resolve to absolute path relative to user's working directory
618
692
  const resolvedArg = path_1.default.isAbsolute(firstArg) ? firstArg : path_1.default.resolve(userCwd, firstArg);
619
- // Check if it's a file path
620
693
  try {
621
694
  const stat = await promises_1.default.stat(resolvedArg);
622
695
  if (stat.isFile()) {
623
- // Read file content as input
624
696
  const fileContent = await promises_1.default.readFile(resolvedArg, 'utf-8');
625
- // Check if it's already JSON
626
697
  try {
627
698
  JSON.parse(fileContent);
628
699
  inputJson = fileContent;
629
700
  }
630
701
  catch {
631
- // Wrap as file_path in JSON (use absolute path)
632
702
  inputJson = JSON.stringify({ file_path: resolvedArg });
633
703
  }
634
704
  }
635
705
  else if (stat.isDirectory()) {
636
- // Pass directory path (use absolute path)
637
706
  inputJson = JSON.stringify({ directory: resolvedArg });
638
707
  }
639
708
  }
640
709
  catch {
641
- // Not a file, check if it's JSON
642
710
  try {
643
711
  JSON.parse(firstArg);
644
712
  inputJson = firstArg;
645
713
  }
646
714
  catch {
647
- // Treat as a simple string input (could be a URL)
648
715
  inputJson = JSON.stringify({ input: firstArg });
649
716
  }
650
717
  }
651
718
  }
652
- // Run the entrypoint with input via stdin
653
719
  process.stderr.write(`\nRunning: python3 ${entrypoint}\n\n`);
654
- // Pass auth credentials to subprocess for orchestrator agents calling sub-agents
655
720
  const subprocessEnv = { ...process.env };
656
721
  if (config.apiKey) {
657
722
  subprocessEnv.ORCHAGENT_SERVICE_KEY = config.apiKey;
658
723
  subprocessEnv.ORCHAGENT_API_URL = config.apiUrl;
659
724
  }
660
- // For orchestrator agents with dependencies, enable local execution mode
661
725
  if (agentData.dependencies && agentData.dependencies.length > 0) {
662
726
  subprocessEnv[LOCAL_EXECUTION_ENV] = 'true';
663
727
  subprocessEnv[AGENTS_DIR_ENV] = AGENTS_DIR;
664
- // Initialize call chain with this agent
665
728
  const agentRef = `${org}/${agentName}@${version}`;
666
729
  subprocessEnv[CALL_CHAIN_ENV] = agentRef;
667
- // Set deadline from manifest timeout (default 120s)
668
730
  const manifest = agentData;
669
731
  const timeoutMs = manifest.manifest?.timeout_ms || 120000;
670
732
  subprocessEnv[DEADLINE_MS_ENV] = String(Date.now() + timeoutMs);
671
- // Set max hops from manifest (default 10)
672
733
  subprocessEnv[MAX_HOPS_ENV] = String(manifest.manifest?.max_hops || 10);
673
- // Set downstream cap
674
734
  subprocessEnv[DOWNSTREAM_REMAINING_ENV] = String(manifest.manifest?.per_call_downstream_cap || 100);
675
735
  }
676
736
  const proc = (0, child_process_1.spawn)('python3', [entrypointPath], {
@@ -678,10 +738,8 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
678
738
  stdio: ['pipe', 'pipe', 'pipe'],
679
739
  env: subprocessEnv,
680
740
  });
681
- // Send input JSON via stdin
682
741
  proc.stdin.write(inputJson);
683
742
  proc.stdin.end();
684
- // Collect output
685
743
  let stdout = '';
686
744
  let stderr = '';
687
745
  proc.stdout?.on('data', (data) => {
@@ -702,26 +760,21 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
702
760
  resolve(1);
703
761
  });
704
762
  });
705
- // Handle output - check for errors in stdout even on failure
706
763
  if (stdout.trim()) {
707
764
  try {
708
765
  const result = JSON.parse(stdout.trim());
709
- // Check if it's an error response
710
766
  if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
711
767
  throw new errors_1.CliError(`Agent error: ${result.error}`);
712
768
  }
713
769
  if (exitCode !== 0) {
714
- // Non-zero exit but output isn't an error object - show it and fail
715
770
  (0, output_1.printJson)(result);
716
771
  throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
717
772
  }
718
- // Success - print result
719
773
  (0, output_1.printJson)(result);
720
774
  }
721
775
  catch (err) {
722
776
  if (err instanceof errors_1.CliError)
723
777
  throw err;
724
- // Not JSON, print as-is
725
778
  process.stdout.write(stdout);
726
779
  if (exitCode !== 0) {
727
780
  throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
@@ -729,7 +782,6 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
729
782
  }
730
783
  }
731
784
  else if (exitCode !== 0) {
732
- // No stdout, check stderr
733
785
  if (stderr.trim()) {
734
786
  throw new errors_1.CliError(`Agent exited with code ${exitCode}\n\nError output:\n${stderr.trim()}`);
735
787
  }
@@ -742,7 +794,6 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
742
794
  }
743
795
  }
744
796
  finally {
745
- // Clean up temp directory
746
797
  try {
747
798
  await promises_1.default.rm(tempDir, { recursive: true, force: true });
748
799
  }
@@ -754,13 +805,10 @@ async function executeBundleAgent(config, org, agentName, version, agentData, ar
754
805
  async function saveAgentLocally(org, agent, agentData) {
755
806
  const agentDir = path_1.default.join(AGENTS_DIR, org, agent);
756
807
  await promises_1.default.mkdir(agentDir, { recursive: true });
757
- // Save metadata
758
808
  await promises_1.default.writeFile(path_1.default.join(agentDir, 'agent.json'), JSON.stringify(agentData, null, 2));
759
- // For prompt agents, save the prompt
760
809
  if (agentData.type === 'prompt' && agentData.prompt) {
761
810
  await promises_1.default.writeFile(path_1.default.join(agentDir, 'prompt.md'), agentData.prompt);
762
811
  }
763
- // For tools, save files if provided
764
812
  if (agentData.files) {
765
813
  for (const file of agentData.files) {
766
814
  const filePath = path_1.default.join(agentDir, file.path);
@@ -773,16 +821,14 @@ async function saveAgentLocally(org, agent, agentData) {
773
821
  async function saveBundleLocally(config, org, agent, version, agentId) {
774
822
  const agentDir = path_1.default.join(AGENTS_DIR, org, agent);
775
823
  const bundleDir = path_1.default.join(agentDir, 'bundle');
776
- // Check if already extracted with same version
777
824
  const metaPath = path_1.default.join(agentDir, 'agent.json');
778
825
  try {
779
826
  const existingMeta = await promises_1.default.readFile(metaPath, 'utf-8');
780
827
  const existing = JSON.parse(existingMeta);
781
828
  if (existing.version === version) {
782
- // Check if bundle dir exists
783
829
  try {
784
830
  await promises_1.default.access(bundleDir);
785
- return bundleDir; // Already cached
831
+ return bundleDir;
786
832
  }
787
833
  catch {
788
834
  // Bundle dir doesn't exist, need to extract
@@ -792,11 +838,9 @@ async function saveBundleLocally(config, org, agent, version, agentId) {
792
838
  catch {
793
839
  // Metadata doesn't exist, need to download
794
840
  }
795
- // Download and extract bundle
796
841
  const bundleBuffer = await (0, spinner_1.withSpinner)(`Downloading bundle for ${org}/${agent}@${version}...`, async () => downloadBundleWithFallback(config, org, agent, version, agentId), { successText: `Downloaded bundle for ${org}/${agent}@${version}` });
797
842
  const tempZip = path_1.default.join(os_1.default.tmpdir(), `bundle-${Date.now()}.zip`);
798
843
  await promises_1.default.writeFile(tempZip, bundleBuffer);
799
- // Clean and recreate bundle directory
800
844
  try {
801
845
  await promises_1.default.rm(bundleDir, { recursive: true, force: true });
802
846
  }
@@ -805,7 +849,6 @@ async function saveBundleLocally(config, org, agent, version, agentId) {
805
849
  }
806
850
  await promises_1.default.mkdir(bundleDir, { recursive: true });
807
851
  await unzipBundle(tempZip, bundleDir);
808
- // Clean up temp file
809
852
  try {
810
853
  await promises_1.default.rm(tempZip);
811
854
  }
@@ -814,51 +857,92 @@ async function saveBundleLocally(config, org, agent, version, agentId) {
814
857
  }
815
858
  return bundleDir;
816
859
  }
817
- function registerRunCommand(program) {
818
- program
819
- .command('run <agent> [args...]')
820
- .description('Download and run an agent locally')
821
- .option('--local', 'Run locally using local LLM keys (default for run command)')
822
- .option('--input <json>', 'JSON input data')
823
- .option('--data <json>', 'Alias for --input')
824
- .option('--download-only', 'Just download the agent, do not execute')
825
- .option('--with-deps', 'Automatically download all dependencies (skip prompt)')
826
- .option('--json', 'Output raw JSON')
827
- .option('--skills <skills>', 'Add skills (comma-separated)')
828
- .option('--skills-only <skills>', 'Use only these skills')
829
- .option('--no-skills', 'Ignore default skills')
830
- .option('--here', 'Scan current directory (passes absolute path to agent)')
831
- .option('--path <dir>', 'Shorthand for --input \'{"path": "<dir>"}\'')
832
- .option('--provider <name>', 'LLM provider to use (openai, anthropic, gemini, ollama)')
833
- .option('--model <model>', 'LLM model to use (overrides agent default)')
834
- .addHelpText('after', `
835
- Examples:
836
- orch run orchagent/leak-finder --input '{"path": "."}'
837
- orch run orchagent/leak-finder --input '{"repo_url": "https://github.com/org/repo"}'
838
- orch run joe/summarizer --input '{"text": "Hello world"}'
839
- orch run orchagent/leak-finder --download-only
840
-
841
- Note: Use 'run' for local execution, 'call' for server-side execution.
842
-
843
- Paid Agents:
844
- Paid agents run on server only for non-owners.
845
- You CAN download and run your own paid agents for development/testing.
846
-
847
- For other users' paid agents, use 'orch call' instead.
848
- `)
849
- .action(async (agentRef, args, options) => {
850
- // Merge --data alias into --input
851
- if (options.data && !options.input) {
852
- options.input = options.data;
860
+ // ─── Cloud execution path ───────────────────────────────────────────────────
861
+ async function executeCloud(agentRef, file, options) {
862
+ // Merge --input alias into --data
863
+ const dataValue = options.data || options.input;
864
+ options.data = dataValue;
865
+ const resolved = await (0, config_1.getResolvedConfig)();
866
+ if (!resolved.apiKey) {
867
+ throw new errors_1.CliError('Missing API key. Run `orchagent login` first.');
868
+ }
869
+ const parsed = parseAgentRef(agentRef);
870
+ const configFile = await (0, config_1.loadConfig)();
871
+ const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
872
+ if (!org) {
873
+ throw new errors_1.CliError('Missing org. Use org/agent or set default org.');
874
+ }
875
+ const agentMeta = await (0, api_1.getAgentWithFallback)(resolved, org, parsed.agent, parsed.version);
876
+ // Pre-call balance check for paid agents
877
+ let pricingInfo;
878
+ if ((0, pricing_1.isPaidAgent)(agentMeta)) {
879
+ let isOwner = false;
880
+ try {
881
+ const callerOrg = await (0, api_1.getOrg)(resolved);
882
+ const agentOrgId = agentMeta.org_id;
883
+ const agentOrgSlug = agentMeta.org_slug;
884
+ if (agentOrgId && callerOrg.id === agentOrgId) {
885
+ isOwner = true;
886
+ }
887
+ else if (agentOrgSlug && callerOrg.slug === agentOrgSlug) {
888
+ isOwner = true;
889
+ }
890
+ }
891
+ catch {
892
+ isOwner = false;
893
+ }
894
+ if (isOwner) {
895
+ if (!options.json)
896
+ process.stderr.write(`Cost: FREE (author)\n\n`);
853
897
  }
854
- // Handle --here and --path shortcuts
855
- if (options.here) {
856
- options.input = JSON.stringify({ path: process.cwd() });
898
+ else {
899
+ const price = agentMeta.price_per_call_cents;
900
+ pricingInfo = { price_cents: price ?? null };
901
+ if (!price || price <= 0) {
902
+ if (!options.json)
903
+ process.stderr.write(`Warning: Pricing data unavailable. The server will verify payment.\n\n`);
904
+ }
905
+ else {
906
+ try {
907
+ const balanceData = await (0, api_1.getCreditsBalance)(resolved);
908
+ const balance = balanceData.balance_cents;
909
+ if (balance < price) {
910
+ process.stderr.write(`Insufficient credits:\n` +
911
+ ` Balance: $${(balance / 100).toFixed(2)}\n` +
912
+ ` Required: $${(price / 100).toFixed(2)}\n\n` +
913
+ `Add credits:\n` +
914
+ ` orch billing add 5\n` +
915
+ ` orch billing balance # check current balance\n`);
916
+ process.exit(errors_1.ExitCodes.PERMISSION_DENIED);
917
+ }
918
+ if (!options.json)
919
+ process.stderr.write(`Cost: $${(price / 100).toFixed(2)}/call\n\n`);
920
+ }
921
+ catch (err) {
922
+ if (!options.json)
923
+ process.stderr.write(`Warning: Could not verify balance. The server will check payment.\n\n`);
924
+ }
925
+ }
857
926
  }
858
- else if (options.path) {
859
- options.input = JSON.stringify({ path: options.path });
927
+ }
928
+ const endpoint = options.endpoint?.trim() || agentMeta.default_endpoint || 'analyze';
929
+ const headers = {
930
+ Authorization: `Bearer ${resolved.apiKey}`,
931
+ };
932
+ if (options.tenant) {
933
+ headers['X-OrchAgent-Tenant'] = options.tenant;
934
+ }
935
+ const supportedProviders = agentMeta.supported_providers || ['any'];
936
+ let llmKey;
937
+ let llmProvider;
938
+ const configDefaultProvider = await (0, config_1.getDefaultProvider)();
939
+ const effectiveProvider = options.provider ?? configDefaultProvider;
940
+ if (options.key) {
941
+ if (!effectiveProvider) {
942
+ throw new errors_1.CliError('When using --key, you must also specify --provider (openai, anthropic, or gemini)');
860
943
  }
861
- if (options.model && options.provider) {
944
+ (0, llm_1.validateProvider)(effectiveProvider);
945
+ if (options.model && effectiveProvider) {
862
946
  const modelLower = options.model.toLowerCase();
863
947
  const providerPatterns = {
864
948
  openai: /^(gpt-|o1-|o3-|davinci|text-)/,
@@ -866,168 +950,476 @@ Paid Agents:
866
950
  gemini: /^gemini-/,
867
951
  ollama: /^(llama|mistral|deepseek|phi|qwen)/,
868
952
  };
869
- const expectedPattern = providerPatterns[options.provider];
953
+ const expectedPattern = providerPatterns[effectiveProvider];
870
954
  if (expectedPattern && !expectedPattern.test(modelLower)) {
871
- process.stderr.write(`Warning: Model '${options.model}' may not be a ${options.provider} model.\n\n`);
955
+ process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
872
956
  }
873
957
  }
874
- const resolved = await (0, config_1.getResolvedConfig)();
875
- const parsed = parseAgentRef(agentRef);
876
- const configFile = await (0, config_1.loadConfig)();
877
- const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
878
- if (!org) {
879
- throw new errors_1.CliError('Missing org. Use org/agent format.');
880
- }
881
- // Download agent definition with spinner
882
- const agentData = await (0, spinner_1.withSpinner)(`Downloading ${org}/${parsed.agent}@${parsed.version}...`, async () => {
883
- try {
884
- return await downloadAgent(resolved, org, parsed.agent, parsed.version);
885
- }
886
- catch (err) {
887
- // Fall back to getting public agent info if download endpoint not available
888
- const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, parsed.agent, parsed.version);
889
- return {
890
- type: agentMeta.type || 'tool',
891
- name: agentMeta.name,
892
- version: agentMeta.version,
893
- description: agentMeta.description || undefined,
894
- supported_providers: agentMeta.supported_providers || ['any'],
958
+ llmKey = options.key;
959
+ llmProvider = effectiveProvider;
960
+ }
961
+ else {
962
+ let providersToCheck = supportedProviders;
963
+ if (effectiveProvider) {
964
+ (0, llm_1.validateProvider)(effectiveProvider);
965
+ providersToCheck = [effectiveProvider];
966
+ if (options.model) {
967
+ const modelLower = options.model.toLowerCase();
968
+ const providerPatterns = {
969
+ openai: /^(gpt-|o1-|o3-|davinci|text-)/,
970
+ anthropic: /^claude-/,
971
+ gemini: /^gemini-/,
972
+ ollama: /^(llama|mistral|deepseek|phi|qwen)/,
895
973
  };
974
+ const expectedPattern = providerPatterns[effectiveProvider];
975
+ if (expectedPattern && !expectedPattern.test(modelLower)) {
976
+ process.stderr.write(`Warning: Model '${options.model}' may not be a ${effectiveProvider} model.\n\n`);
977
+ }
896
978
  }
897
- }, { successText: `Downloaded ${org}/${parsed.agent}@${parsed.version}` });
898
- // Skills cannot be run directly - they're instructions to inject into agents
899
- if (agentData.type === 'skill') {
900
- throw new errors_1.CliError('Skills cannot be run directly.\n\n' +
901
- 'Skills are instructions meant to be injected into AI agent contexts.\n\n' +
902
- 'Options:\n' +
903
- ` Install for AI tools: orchagent skill install ${org}/${parsed.agent}\n` +
904
- ` Use with an agent: orchagent run <agent> --skills ${org}/${parsed.agent}`);
905
- }
906
- // Agent type requires a sandbox with tool use — cannot run locally
907
- if (agentData.type === 'agent') {
908
- throw new errors_1.CliError('Agent type cannot be run locally.\n\n' +
909
- 'Agent type requires a sandbox environment with tool use capabilities.\n\n' +
910
- 'Use server execution instead:\n' +
911
- ` orchagent call ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
912
- }
913
- // Check for dependencies (orchestrator agents)
914
- if (agentData.dependencies && agentData.dependencies.length > 0) {
915
- const depStatuses = await (0, spinner_1.withSpinner)('Checking dependencies...', async () => checkDependencies(resolved, agentData.dependencies), { successText: `Found ${agentData.dependencies.length} dependencies` });
916
- let choice;
917
- if (options.withDeps) {
918
- // Auto-download deps without prompting
919
- choice = 'local';
920
- }
921
- else {
922
- choice = await promptUserForDeps(depStatuses);
923
- }
924
- if (choice === 'cancel') {
925
- process.stderr.write('\nCancelled.\n');
926
- process.exit(0);
927
- }
928
- if (choice === 'server') {
929
- process.stderr.write(`\nUse server execution instead:\n`);
930
- process.stderr.write(` orch call ${org}/${parsed.agent}@${parsed.version} --input '{...}'\n\n`);
931
- process.exit(0);
979
+ }
980
+ const detected = await (0, llm_1.detectLlmKey)(providersToCheck, resolved);
981
+ if (detected) {
982
+ llmKey = detected.key;
983
+ llmProvider = detected.provider;
984
+ }
985
+ }
986
+ let llmCredentials;
987
+ if (llmKey && llmProvider) {
988
+ llmCredentials = {
989
+ api_key: llmKey,
990
+ provider: llmProvider,
991
+ ...(options.model && { model: options.model }),
992
+ };
993
+ }
994
+ else if (agentMeta.type === 'prompt') {
995
+ const searchedProviders = effectiveProvider ? [effectiveProvider] : supportedProviders;
996
+ const providerList = searchedProviders.join(', ');
997
+ process.stderr.write(`Warning: No LLM key found for provider(s): ${providerList}\n` +
998
+ `Set an env var (e.g., OPENAI_API_KEY), run 'orchagent keys add <provider>', use --key, or configure in web dashboard\n\n`);
999
+ }
1000
+ if (options.skills) {
1001
+ headers['X-OrchAgent-Skills'] = options.skills;
1002
+ }
1003
+ if (options.skillsOnly) {
1004
+ headers['X-OrchAgent-Skills-Only'] = options.skillsOnly;
1005
+ }
1006
+ if (options.noSkills) {
1007
+ headers['X-OrchAgent-No-Skills'] = 'true';
1008
+ }
1009
+ let body;
1010
+ let sourceLabel;
1011
+ const filePaths = [
1012
+ ...(options.file ?? []),
1013
+ ...(file ? [file] : []),
1014
+ ];
1015
+ if (options.data) {
1016
+ if (filePaths.length > 0 || options.metadata) {
1017
+ throw new errors_1.CliError('Cannot use --data with file uploads or --metadata.');
1018
+ }
1019
+ const resolvedBody = await resolveJsonBody(options.data);
1020
+ warnIfLocalPathReference(resolvedBody);
1021
+ if (llmCredentials) {
1022
+ const bodyObj = JSON.parse(resolvedBody);
1023
+ bodyObj.llm_credentials = llmCredentials;
1024
+ body = JSON.stringify(bodyObj);
1025
+ }
1026
+ else {
1027
+ body = resolvedBody;
1028
+ }
1029
+ headers['Content-Type'] = 'application/json';
1030
+ }
1031
+ else if ((filePaths.length > 0 || options.metadata) && agentMeta.type === 'prompt') {
1032
+ const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1033
+ let bodyObj = {};
1034
+ if (options.metadata) {
1035
+ try {
1036
+ bodyObj = JSON.parse(options.metadata);
932
1037
  }
933
- // choice === 'local' - download dependencies
934
- await downloadDependenciesRecursively(resolved, depStatuses);
935
- }
936
- // Check if user is overriding locked skills
937
- const agentSkillsLocked = agentData.skills_locked;
938
- if (agentSkillsLocked && (options.noSkills || options.skillsOnly)) {
939
- const readline = await Promise.resolve().then(() => __importStar(require('readline')));
940
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
941
- const answer = await new Promise(resolve => {
942
- rl.question(`\nWarning: Author locked skills for this agent.\n` +
943
- `Default skills: ${agentData.default_skills?.join(', ') || '(none)'}\n` +
944
- `Override anyway? [y/N] `, resolve);
945
- });
946
- rl.close();
947
- if (answer.toLowerCase() !== 'y') {
948
- process.stderr.write('Aborted. Running with author\'s locked skills.\n');
949
- options.noSkills = false;
950
- options.skillsOnly = undefined;
1038
+ catch {
1039
+ throw new errors_1.CliError('--metadata must be valid JSON.');
951
1040
  }
952
1041
  }
953
- // Save locally
954
- const agentDir = await saveAgentLocally(org, parsed.agent, agentData);
955
- process.stderr.write(`\nAgent saved to: ${agentDir}\n`);
956
- if (agentData.type === 'tool') {
957
- // Check if this agent has a bundle available for local execution
958
- if (agentData.has_bundle) {
959
- if (options.downloadOnly) {
960
- process.stdout.write(`\nTool has bundle available for local execution.\n`);
961
- process.stdout.write(`Run with: orch run ${org}/${parsed.agent} [args...]\n`);
962
- return;
963
- }
964
- // Execute the bundle-based tool locally
965
- await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, options.input);
966
- return;
1042
+ if (filePaths.length === 1) {
1043
+ const fileContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1044
+ bodyObj[fieldName] = fileContent;
1045
+ sourceLabel = filePaths[0];
1046
+ }
1047
+ else if (filePaths.length > 1) {
1048
+ const allContents = {};
1049
+ for (const fp of filePaths) {
1050
+ allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
967
1051
  }
968
- // Check for pip/source-based local execution (legacy)
969
- if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
970
- if (options.downloadOnly) {
971
- process.stdout.write(`\nTool ready for local execution.\n`);
972
- process.stdout.write(`Run with: orch run ${org}/${parsed.agent} [args...]\n`);
973
- return;
974
- }
975
- // Execute the tool locally
976
- await executeTool(agentData, args);
977
- return;
1052
+ const firstContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1053
+ bodyObj[fieldName] = firstContent;
1054
+ bodyObj.files = allContents;
1055
+ sourceLabel = `${filePaths.length} files`;
1056
+ }
1057
+ if (llmCredentials) {
1058
+ bodyObj.llm_credentials = llmCredentials;
1059
+ }
1060
+ body = JSON.stringify(bodyObj);
1061
+ headers['Content-Type'] = 'application/json';
1062
+ }
1063
+ else if (filePaths.length > 0 || options.metadata) {
1064
+ let metadata = options.metadata;
1065
+ if (llmCredentials) {
1066
+ const metaObj = metadata ? JSON.parse(metadata) : {};
1067
+ metaObj.llm_credentials = llmCredentials;
1068
+ metadata = JSON.stringify(metaObj);
1069
+ }
1070
+ const multipart = await buildMultipartBody(filePaths, metadata);
1071
+ body = multipart.body;
1072
+ sourceLabel = multipart.sourceLabel;
1073
+ }
1074
+ else if (llmCredentials) {
1075
+ body = JSON.stringify({ llm_credentials: llmCredentials });
1076
+ headers['Content-Type'] = 'application/json';
1077
+ }
1078
+ else {
1079
+ const multipart = await buildMultipartBody(undefined, options.metadata);
1080
+ body = multipart.body;
1081
+ sourceLabel = multipart.sourceLabel;
1082
+ }
1083
+ const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
1084
+ const spinner = options.json ? null : (0, spinner_1.createSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
1085
+ spinner?.start();
1086
+ let response;
1087
+ try {
1088
+ response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
1089
+ method: 'POST',
1090
+ headers,
1091
+ body,
1092
+ });
1093
+ }
1094
+ catch (err) {
1095
+ spinner?.fail(`Run failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
1096
+ throw err;
1097
+ }
1098
+ if (!response.ok) {
1099
+ const text = await response.text();
1100
+ let payload;
1101
+ try {
1102
+ payload = JSON.parse(text);
1103
+ }
1104
+ catch {
1105
+ payload = text;
1106
+ }
1107
+ const errorCode = typeof payload === 'object' && payload
1108
+ ? payload.error?.code
1109
+ : undefined;
1110
+ if (response.status === 402 || errorCode === 'INSUFFICIENT_CREDITS') {
1111
+ spinner?.fail('Insufficient credits');
1112
+ let errorMessage = 'Insufficient credits to run this agent.\n\n';
1113
+ if (pricingInfo?.price_cents) {
1114
+ errorMessage += `This agent costs $${(pricingInfo.price_cents / 100).toFixed(2)} per call.\n\n`;
978
1115
  }
979
- // Fallback: agent doesn't support local execution
980
- process.stdout.write(`\nThis is a tool-based agent that runs on the server.\n`);
981
- process.stdout.write(`\nUse: orch call ${org}/${parsed.agent}@${parsed.version} --input '{...}'\n`);
982
- return;
1116
+ errorMessage +=
1117
+ 'Add credits:\n' +
1118
+ ' orch billing add 5\n' +
1119
+ ' orch billing balance # check current balance\n';
1120
+ throw new errors_1.CliError(errorMessage, errors_1.ExitCodes.PERMISSION_DENIED);
983
1121
  }
984
- if (options.downloadOnly) {
985
- process.stdout.write(`\nAgent downloaded. Run with:\n`);
986
- process.stdout.write(` orchagent run ${org}/${parsed.agent}@${parsed.version} --input '{...}'\n`);
987
- return;
1122
+ if (errorCode === 'LLM_KEY_REQUIRED') {
1123
+ spinner?.fail('LLM key required');
1124
+ throw new errors_1.CliError('This public agent requires you to provide an LLM key.\n' +
1125
+ 'Use --key <key> --provider <provider> or set OPENAI_API_KEY/ANTHROPIC_API_KEY env var.');
1126
+ }
1127
+ if (errorCode === 'LLM_RATE_LIMITED') {
1128
+ const rateLimitMsg = typeof payload === 'object' && payload
1129
+ ? payload.error?.message || 'Rate limit exceeded'
1130
+ : 'Rate limit exceeded';
1131
+ spinner?.fail('Rate limited by LLM provider');
1132
+ throw new errors_1.CliError(rateLimitMsg + '\n\n' +
1133
+ 'This is the LLM provider\'s rate limit on your API key, not an OrchAgent limit.\n' +
1134
+ 'To switch providers: orch run <agent> --provider <gemini|anthropic|openai>', errors_1.ExitCodes.RATE_LIMITED);
988
1135
  }
989
- // For prompt-based agents, execute locally
990
- if (!options.input) {
991
- process.stdout.write(`\nPrompt-based agent ready.\n`);
992
- process.stdout.write(`Run with: orchagent run ${org}/${parsed.agent}@${parsed.version} --input '{...}'\n`);
1136
+ const message = typeof payload === 'object' && payload
1137
+ ? payload.error
1138
+ ?.message ||
1139
+ payload.message ||
1140
+ response.statusText
1141
+ : response.statusText;
1142
+ spinner?.fail(`Run failed: ${message}`);
1143
+ throw new errors_1.CliError(message);
1144
+ }
1145
+ spinner?.succeed(`Ran ${org}/${parsed.agent}@${parsed.version}`);
1146
+ if (!options.json && (0, pricing_1.isPaidAgent)(agentMeta) && pricingInfo?.price_cents && pricingInfo.price_cents > 0) {
1147
+ process.stderr.write(`\nCost: $${(pricingInfo.price_cents / 100).toFixed(2)} USD\n`);
1148
+ }
1149
+ const inputType = filePaths.length > 0
1150
+ ? 'file'
1151
+ : options.data
1152
+ ? 'json'
1153
+ : sourceLabel === 'stdin'
1154
+ ? 'stdin'
1155
+ : sourceLabel === 'metadata'
1156
+ ? 'metadata'
1157
+ : 'empty';
1158
+ await (0, analytics_1.track)('cli_run', {
1159
+ agent: `${org}/${parsed.agent}@${parsed.version}`,
1160
+ input_type: inputType,
1161
+ mode: 'cloud',
1162
+ });
1163
+ if (options.output) {
1164
+ const buffer = Buffer.from(await response.arrayBuffer());
1165
+ await promises_1.default.writeFile(options.output, buffer);
1166
+ process.stdout.write(`Saved response to ${options.output}\n`);
1167
+ return;
1168
+ }
1169
+ const text = await response.text();
1170
+ let payload;
1171
+ try {
1172
+ payload = JSON.parse(text);
1173
+ }
1174
+ catch {
1175
+ payload = text;
1176
+ }
1177
+ if (options.json) {
1178
+ if (typeof payload === 'string') {
1179
+ process.stdout.write(`${payload}\n`);
993
1180
  return;
994
1181
  }
995
- // Parse input
996
- let inputData;
1182
+ (0, output_1.printJson)(payload);
1183
+ return;
1184
+ }
1185
+ if (typeof payload === 'string') {
1186
+ process.stdout.write(`${payload}\n`);
1187
+ return;
1188
+ }
1189
+ (0, output_1.printJson)(payload);
1190
+ }
1191
+ // ─── Local execution path ───────────────────────────────────────────────────
1192
+ async function executeLocal(agentRef, args, options) {
1193
+ // Merge --data alias into --input
1194
+ if (options.data && !options.input) {
1195
+ options.input = options.data;
1196
+ }
1197
+ // Handle --here and --path shortcuts
1198
+ if (options.here) {
1199
+ options.input = JSON.stringify({ path: process.cwd() });
1200
+ }
1201
+ else if (options.path) {
1202
+ options.input = JSON.stringify({ path: options.path });
1203
+ }
1204
+ if (options.model && options.provider) {
1205
+ const modelLower = options.model.toLowerCase();
1206
+ const providerPatterns = {
1207
+ openai: /^(gpt-|o1-|o3-|davinci|text-)/,
1208
+ anthropic: /^claude-/,
1209
+ gemini: /^gemini-/,
1210
+ ollama: /^(llama|mistral|deepseek|phi|qwen)/,
1211
+ };
1212
+ const expectedPattern = providerPatterns[options.provider];
1213
+ if (expectedPattern && !expectedPattern.test(modelLower)) {
1214
+ process.stderr.write(`Warning: Model '${options.model}' may not be a ${options.provider} model.\n\n`);
1215
+ }
1216
+ }
1217
+ const resolved = await (0, config_1.getResolvedConfig)();
1218
+ const parsed = parseAgentRef(agentRef);
1219
+ const configFile = await (0, config_1.loadConfig)();
1220
+ const org = parsed.org ?? configFile.workspace ?? resolved.defaultOrg;
1221
+ if (!org) {
1222
+ throw new errors_1.CliError('Missing org. Use org/agent format.');
1223
+ }
1224
+ // Download agent definition with spinner
1225
+ const agentData = await (0, spinner_1.withSpinner)(`Downloading ${org}/${parsed.agent}@${parsed.version}...`, async () => {
997
1226
  try {
998
- inputData = JSON.parse(options.input);
1227
+ return await downloadAgent(resolved, org, parsed.agent, parsed.version);
999
1228
  }
1000
- catch {
1001
- throw new errors_1.CliError('Invalid JSON input');
1002
- }
1003
- // Handle skill composition
1004
- let skillPrompts = [];
1005
- if (!options.noSkills) {
1006
- const skillRefs = [];
1007
- if (options.skillsOnly) {
1008
- // Use only the specified skills (ignore defaults)
1009
- skillRefs.push(...options.skillsOnly.split(',').map((s) => s.trim()));
1229
+ catch (err) {
1230
+ const agentMeta = await (0, api_1.getPublicAgent)(resolved, org, parsed.agent, parsed.version);
1231
+ return {
1232
+ type: agentMeta.type || 'tool',
1233
+ name: agentMeta.name,
1234
+ version: agentMeta.version,
1235
+ description: agentMeta.description || undefined,
1236
+ supported_providers: agentMeta.supported_providers || ['any'],
1237
+ };
1238
+ }
1239
+ }, { successText: `Downloaded ${org}/${parsed.agent}@${parsed.version}` });
1240
+ // Skills cannot be run directly
1241
+ if (agentData.type === 'skill') {
1242
+ throw new errors_1.CliError('Skills cannot be run directly.\n\n' +
1243
+ 'Skills are instructions meant to be injected into AI agent contexts.\n\n' +
1244
+ 'Options:\n' +
1245
+ ` Install for AI tools: orchagent skill install ${org}/${parsed.agent}\n` +
1246
+ ` Use with an agent: orchagent run <agent> --skills ${org}/${parsed.agent}`);
1247
+ }
1248
+ // Agent type requires a sandbox — cannot run locally
1249
+ if (agentData.type === 'agent') {
1250
+ throw new errors_1.CliError('Agent type cannot be run locally.\n\n' +
1251
+ 'Agent type requires a sandbox environment with tool use capabilities.\n\n' +
1252
+ 'Remove the --local flag to run in the cloud:\n' +
1253
+ ` orch run ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
1254
+ }
1255
+ // Check for dependencies (orchestrator agents)
1256
+ if (agentData.dependencies && agentData.dependencies.length > 0) {
1257
+ const depStatuses = await (0, spinner_1.withSpinner)('Checking dependencies...', async () => checkDependencies(resolved, agentData.dependencies), { successText: `Found ${agentData.dependencies.length} dependencies` });
1258
+ let choice;
1259
+ if (options.withDeps) {
1260
+ choice = 'local';
1261
+ }
1262
+ else {
1263
+ choice = await promptUserForDeps(depStatuses);
1264
+ }
1265
+ if (choice === 'cancel') {
1266
+ process.stderr.write('\nCancelled.\n');
1267
+ process.exit(0);
1268
+ }
1269
+ if (choice === 'server') {
1270
+ process.stderr.write(`\nRun without --local for server execution:\n`);
1271
+ process.stderr.write(` orch run ${org}/${parsed.agent}@${parsed.version} --data '{...}'\n\n`);
1272
+ process.exit(0);
1273
+ }
1274
+ await downloadDependenciesRecursively(resolved, depStatuses);
1275
+ }
1276
+ // Check if user is overriding locked skills
1277
+ const agentSkillsLocked = agentData.skills_locked;
1278
+ if (agentSkillsLocked && (options.noSkills || options.skillsOnly)) {
1279
+ const readline = await Promise.resolve().then(() => __importStar(require('readline')));
1280
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
1281
+ const answer = await new Promise(resolve => {
1282
+ rl.question(`\nWarning: Author locked skills for this agent.\n` +
1283
+ `Default skills: ${agentData.default_skills?.join(', ') || '(none)'}\n` +
1284
+ `Override anyway? [y/N] `, resolve);
1285
+ });
1286
+ rl.close();
1287
+ if (answer.toLowerCase() !== 'y') {
1288
+ process.stderr.write('Aborted. Running with author\'s locked skills.\n');
1289
+ options.noSkills = false;
1290
+ options.skillsOnly = undefined;
1291
+ }
1292
+ }
1293
+ // Save locally
1294
+ const agentDir = await saveAgentLocally(org, parsed.agent, agentData);
1295
+ process.stderr.write(`\nAgent saved to: ${agentDir}\n`);
1296
+ if (agentData.type === 'tool') {
1297
+ if (agentData.has_bundle) {
1298
+ if (options.downloadOnly) {
1299
+ process.stdout.write(`\nTool has bundle available for local execution.\n`);
1300
+ process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
1301
+ return;
1010
1302
  }
1011
- else {
1012
- // Start with agent's default skills (if any)
1013
- const defaultSkills = agentData.default_skills || [];
1014
- skillRefs.push(...defaultSkills);
1015
- // Add any additional skills specified via --skills
1016
- if (options.skills) {
1017
- skillRefs.push(...options.skills.split(',').map((s) => s.trim()));
1018
- }
1303
+ await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, options.input);
1304
+ return;
1305
+ }
1306
+ if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
1307
+ if (options.downloadOnly) {
1308
+ process.stdout.write(`\nTool ready for local execution.\n`);
1309
+ process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
1310
+ return;
1019
1311
  }
1020
- if (skillRefs.length > 0) {
1021
- skillPrompts = await (0, spinner_1.withSpinner)(`Loading ${skillRefs.length} skill(s)...`, async () => loadSkillPrompts(resolved, skillRefs, org), { successText: `Loaded ${skillRefs.length} skill(s)` });
1312
+ await executeTool(agentData, args);
1313
+ return;
1314
+ }
1315
+ // Fallback: agent doesn't support local execution
1316
+ process.stdout.write(`\nThis is a tool-based agent that runs on the server.\n`);
1317
+ process.stdout.write(`\nRun without --local: orch run ${org}/${parsed.agent}@${parsed.version} --data '{...}'\n`);
1318
+ return;
1319
+ }
1320
+ if (options.downloadOnly) {
1321
+ process.stdout.write(`\nAgent downloaded. Run with:\n`);
1322
+ process.stdout.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1323
+ return;
1324
+ }
1325
+ // For prompt-based agents, execute locally
1326
+ if (!options.input) {
1327
+ process.stdout.write(`\nPrompt-based agent ready.\n`);
1328
+ process.stdout.write(`Run with: orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1329
+ return;
1330
+ }
1331
+ let inputData;
1332
+ try {
1333
+ inputData = JSON.parse(options.input);
1334
+ }
1335
+ catch {
1336
+ throw new errors_1.CliError('Invalid JSON input');
1337
+ }
1338
+ // Handle skill composition
1339
+ let skillPrompts = [];
1340
+ if (!options.noSkills) {
1341
+ const skillRefs = [];
1342
+ if (options.skillsOnly) {
1343
+ skillRefs.push(...options.skillsOnly.split(',').map((s) => s.trim()));
1344
+ }
1345
+ else {
1346
+ const defaultSkills = agentData.default_skills || [];
1347
+ skillRefs.push(...defaultSkills);
1348
+ if (options.skills) {
1349
+ skillRefs.push(...options.skills.split(',').map((s) => s.trim()));
1022
1350
  }
1023
1351
  }
1024
- // Execute locally (the spinner is inside executePromptLocally)
1025
- const result = await executePromptLocally(agentData, inputData, skillPrompts, resolved, options.provider, options.model);
1026
- if (options.json) {
1027
- (0, output_1.printJson)(result);
1352
+ if (skillRefs.length > 0) {
1353
+ skillPrompts = await (0, spinner_1.withSpinner)(`Loading ${skillRefs.length} skill(s)...`, async () => loadSkillPrompts(resolved, skillRefs, org), { successText: `Loaded ${skillRefs.length} skill(s)` });
1354
+ }
1355
+ }
1356
+ const result = await executePromptLocally(agentData, inputData, skillPrompts, resolved, options.provider, options.model);
1357
+ (0, output_1.printJson)(result);
1358
+ }
1359
+ function registerRunCommand(program) {
1360
+ program
1361
+ .command('run <agent> [file]')
1362
+ .description('Run an agent (cloud by default, --local for local execution)')
1363
+ .option('--local', 'Run locally instead of on the server')
1364
+ .option('--data <json>', 'JSON payload (string or @file, @- for stdin)')
1365
+ .option('--input <json>', 'Alias for --data')
1366
+ .option('--json', 'Output raw JSON')
1367
+ .option('--provider <provider>', 'LLM provider (openai, anthropic, gemini, ollama)')
1368
+ .option('--model <model>', 'LLM model to use (overrides agent default)')
1369
+ .option('--key <key>', 'LLM API key (overrides env vars)')
1370
+ .option('--skills <skills>', 'Add skills (comma-separated)')
1371
+ .option('--skills-only <skills>', 'Use only these skills')
1372
+ .option('--no-skills', 'Ignore default skills')
1373
+ // Cloud-only options
1374
+ .option('--endpoint <endpoint>', 'Override agent endpoint (cloud only)')
1375
+ .option('--tenant <tenant>', 'Tenant identifier for multi-tenant callers (cloud only)')
1376
+ .option('--output <file>', 'Save response body to a file (cloud only)')
1377
+ .option('--file <path...>', 'File(s) to upload (cloud only, can specify multiple)')
1378
+ .option('--file-field <field>', 'Schema field name for file content (cloud only)')
1379
+ .option('--metadata <json>', 'JSON metadata to send with files (cloud only)')
1380
+ // Local-only options
1381
+ .option('--download-only', 'Just download the agent, do not execute (local only)')
1382
+ .option('--with-deps', 'Automatically download all dependencies (local only)')
1383
+ .option('--here', 'Scan current directory (local only)')
1384
+ .option('--path <dir>', 'Shorthand for --data \'{"path": "<dir>"}\' (local only)')
1385
+ .addHelpText('after', `
1386
+ Examples:
1387
+ Cloud execution (default):
1388
+ orch run orchagent/leak-finder --data '{"repo_url": "https://github.com/org/repo"}'
1389
+ orch run orchagent/invoice-scanner invoice.pdf
1390
+ orch run orchagent/useeffect-checker --file src/App.tsx
1391
+ cat input.json | orch run acme/agent --data @-
1392
+ orch run acme/image-processor photo.jpg --output result.png
1393
+
1394
+ Local execution (--local):
1395
+ orch run orchagent/leak-finder --local --data '{"path": "."}'
1396
+ orch run joe/summarizer --local --data '{"text": "Hello world"}'
1397
+ orch run orchagent/leak-finder --local --download-only
1398
+
1399
+ Paid Agents:
1400
+ Paid agents charge per call and deduct from your prepaid credits.
1401
+ Check your balance: orch billing balance
1402
+ Add credits: orch billing add 5
1403
+
1404
+ Same-author calls are FREE - you won't be charged for calling your own agents.
1405
+
1406
+ File handling (cloud):
1407
+ For prompt agents, file content is read and sent as JSON mapped to the agent's
1408
+ input schema. Use --file-field to specify the field name (auto-detected by default).
1409
+ For tools, files are uploaded as multipart form data.
1410
+
1411
+ Important: Remote agents cannot access your local filesystem. If your --data payload
1412
+ contains keys like 'path', 'directory', 'file', etc., those values will be interpreted
1413
+ by the server, not your local machine. To use local files, use --local or --file.
1414
+ `)
1415
+ .action(async (agentRef, file, options) => {
1416
+ if (options.local) {
1417
+ // Local execution: file arg becomes first positional arg
1418
+ const args = file ? [file] : [];
1419
+ await executeLocal(agentRef, args, options);
1028
1420
  }
1029
1421
  else {
1030
- (0, output_1.printJson)(result);
1422
+ await executeCloud(agentRef, file, options);
1031
1423
  }
1032
1424
  });
1033
1425
  }