@orchagent/cli 0.3.49 → 0.3.52

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.
@@ -36,11 +36,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.isKeyedFileArg = isKeyedFileArg;
40
+ exports.readKeyedFiles = readKeyedFiles;
41
+ exports.mountDirectory = mountDirectory;
42
+ exports.buildInjectedPayload = buildInjectedPayload;
39
43
  exports.registerRunCommand = registerRunCommand;
40
44
  const promises_1 = __importDefault(require("fs/promises"));
41
45
  const path_1 = __importDefault(require("path"));
42
46
  const os_1 = __importDefault(require("os"));
43
47
  const child_process_1 = require("child_process");
48
+ const chalk_1 = __importDefault(require("chalk"));
44
49
  const config_1 = require("../lib/config");
45
50
  const api_1 = require("../lib/api");
46
51
  const errors_1 = require("../lib/errors");
@@ -49,6 +54,7 @@ const spinner_1 = require("../lib/spinner");
49
54
  const llm_1 = require("../lib/llm");
50
55
  const analytics_1 = require("../lib/analytics");
51
56
  const pricing_1 = require("../lib/pricing");
57
+ const package_json_1 = __importDefault(require("../../package.json"));
52
58
  const DEFAULT_VERSION = 'latest';
53
59
  const AGENTS_DIR = path_1.default.join(os_1.default.homedir(), '.orchagent', 'agents');
54
60
  // Local execution environment variables
@@ -219,6 +225,164 @@ async function resolveJsonBody(input) {
219
225
  throw (0, errors_1.jsonInputError)('data');
220
226
  }
221
227
  }
228
+ // ─── Keyed file & mount helpers ──────────────────────────────────────────────
229
+ const KEYED_FILE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
230
+ function isKeyedFileArg(arg) {
231
+ const eqIndex = arg.indexOf('=');
232
+ if (eqIndex <= 0)
233
+ return null;
234
+ const key = arg.slice(0, eqIndex);
235
+ const filePath = arg.slice(eqIndex + 1);
236
+ if (!KEYED_FILE_KEY_RE.test(key))
237
+ return null;
238
+ if (!filePath)
239
+ return null;
240
+ return { key, filePath };
241
+ }
242
+ async function readKeyedFiles(args) {
243
+ const result = {};
244
+ for (const arg of args) {
245
+ const parsed = isKeyedFileArg(arg);
246
+ if (!parsed)
247
+ continue;
248
+ const resolved = path_1.default.resolve(parsed.filePath);
249
+ let stat;
250
+ try {
251
+ stat = await promises_1.default.stat(resolved);
252
+ }
253
+ catch {
254
+ throw new errors_1.CliError(`File not found: ${parsed.filePath}`);
255
+ }
256
+ if (!stat.isFile()) {
257
+ throw new errors_1.CliError(`Not a file: ${parsed.filePath}`);
258
+ }
259
+ result[parsed.key] = await promises_1.default.readFile(resolved, 'utf-8');
260
+ }
261
+ return result;
262
+ }
263
+ const MOUNT_SKIP_DIRS = new Set([
264
+ 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build',
265
+ '.next', 'target', '.cache', '.tox', 'coverage', '__snapshots__',
266
+ ]);
267
+ const MOUNT_MAX_DEPTH = 15;
268
+ const MOUNT_MAX_FILES = 500;
269
+ async function mountDirectory(dirPath) {
270
+ const resolved = path_1.default.resolve(dirPath);
271
+ let stat;
272
+ try {
273
+ stat = await promises_1.default.stat(resolved);
274
+ }
275
+ catch {
276
+ throw new errors_1.CliError(`Directory not found: ${dirPath}`);
277
+ }
278
+ if (!stat.isDirectory()) {
279
+ throw new errors_1.CliError(`Not a directory: ${dirPath}`);
280
+ }
281
+ const result = {};
282
+ let fileCount = 0;
283
+ async function walk(currentPath, relativePath, depth) {
284
+ if (depth > MOUNT_MAX_DEPTH)
285
+ return;
286
+ let names;
287
+ try {
288
+ names = await promises_1.default.readdir(currentPath);
289
+ }
290
+ catch {
291
+ return;
292
+ }
293
+ for (const name of names) {
294
+ if (name.startsWith('.'))
295
+ continue;
296
+ if (MOUNT_SKIP_DIRS.has(name))
297
+ continue;
298
+ const fullPath = path_1.default.join(currentPath, name);
299
+ const relPath = relativePath ? `${relativePath}/${name}` : name;
300
+ // Stat the entry (also skips symlinks)
301
+ let entryStat;
302
+ try {
303
+ entryStat = await promises_1.default.lstat(fullPath);
304
+ if (entryStat.isSymbolicLink())
305
+ continue;
306
+ }
307
+ catch {
308
+ continue;
309
+ }
310
+ if (entryStat.isDirectory()) {
311
+ await walk(fullPath, relPath, depth + 1);
312
+ }
313
+ else if (entryStat.isFile()) {
314
+ if (fileCount >= MOUNT_MAX_FILES) {
315
+ throw new errors_1.CliError(`Mount exceeds ${MOUNT_MAX_FILES} files. Use a more specific path or fewer files.`);
316
+ }
317
+ try {
318
+ const content = await promises_1.default.readFile(fullPath, 'utf-8');
319
+ result[relPath] = content;
320
+ fileCount++;
321
+ }
322
+ catch {
323
+ // Skip binary/unreadable files silently
324
+ }
325
+ }
326
+ }
327
+ }
328
+ await walk(resolved, '', 0);
329
+ return result;
330
+ }
331
+ const INJECT_MAX_BYTES = 4 * 1024 * 1024; // 4MB
332
+ async function buildInjectedPayload(options) {
333
+ let merged = {};
334
+ // 1. Start with --data
335
+ if (options.dataOption) {
336
+ const resolved = await resolveJsonBody(options.dataOption);
337
+ merged = JSON.parse(resolved);
338
+ }
339
+ let totalBytes = 0;
340
+ // 2. Overlay keyed --file entries
341
+ if (options.fileArgs && options.fileArgs.length > 0) {
342
+ const keyedFiles = await readKeyedFiles(options.fileArgs);
343
+ for (const [key, content] of Object.entries(keyedFiles)) {
344
+ totalBytes += Buffer.byteLength(content, 'utf-8');
345
+ merged[key] = content;
346
+ }
347
+ }
348
+ // 3. Overlay --mount entries
349
+ if (options.mountArgs && options.mountArgs.length > 0) {
350
+ for (const mountArg of options.mountArgs) {
351
+ const eqIndex = mountArg.indexOf('=');
352
+ if (eqIndex <= 0) {
353
+ throw new errors_1.CliError(`Invalid --mount format: ${mountArg}. Use --mount field=dir`);
354
+ }
355
+ const field = mountArg.slice(0, eqIndex);
356
+ const dirPath = mountArg.slice(eqIndex + 1);
357
+ if (!KEYED_FILE_KEY_RE.test(field)) {
358
+ throw new errors_1.CliError(`Invalid mount field name: ${field}. Must be a valid identifier.`);
359
+ }
360
+ const fileMap = await mountDirectory(dirPath);
361
+ for (const content of Object.values(fileMap)) {
362
+ totalBytes += Buffer.byteLength(content, 'utf-8');
363
+ }
364
+ merged[field] = fileMap;
365
+ }
366
+ }
367
+ // 4. Enforce size limit
368
+ if (totalBytes > INJECT_MAX_BYTES) {
369
+ throw new errors_1.CliError(`File content exceeds 4MB limit (${(totalBytes / 1024 / 1024).toFixed(1)}MB). ` +
370
+ `Use a more specific path or fewer files.`);
371
+ }
372
+ // 5. Inject llm_credentials
373
+ if (options.llmCredentials) {
374
+ merged.llm_credentials = options.llmCredentials;
375
+ }
376
+ const parts = [];
377
+ if (options.fileArgs && options.fileArgs.length > 0) {
378
+ parts.push(`${options.fileArgs.length} file(s)`);
379
+ }
380
+ if (options.mountArgs && options.mountArgs.length > 0) {
381
+ parts.push(`${options.mountArgs.length} mount(s)`);
382
+ }
383
+ const sourceLabel = parts.join(' + ');
384
+ return { body: JSON.stringify(merged), sourceLabel };
385
+ }
222
386
  // ─── Local execution helpers ────────────────────────────────────────────────
223
387
  async function downloadAgent(config, org, agent, version) {
224
388
  // Try public endpoint first
@@ -529,6 +693,183 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
529
693
  return await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
530
694
  }, { successText: `Completed with ${provider}` });
531
695
  }
696
+ // ─── Local agent-type execution ──────────────────────────────────────────────
697
+ const AGENT_RUNNER_SDK_PACKAGES = {
698
+ anthropic: 'anthropic',
699
+ openai: 'openai',
700
+ gemini: 'google-genai',
701
+ };
702
+ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, customTools, manifest, config, providerOverride, modelOverride) {
703
+ // 1. Check Python 3 available
704
+ try {
705
+ const { code } = await runCommand('python3', ['--version']);
706
+ if (code !== 0)
707
+ throw new Error();
708
+ }
709
+ catch {
710
+ throw new errors_1.CliError('Python 3 is required for local agent execution.\n' +
711
+ 'Install Python 3: https://python.org/downloads');
712
+ }
713
+ // 2. Detect LLM provider + key
714
+ const supportedProviders = manifest?.supported_providers || ['any'];
715
+ const providersToCheck = providerOverride
716
+ ? [providerOverride]
717
+ : supportedProviders;
718
+ const allProviders = await detectAllLlmKeys(providersToCheck, config);
719
+ if (allProviders.length === 0) {
720
+ const providers = providersToCheck.join(', ');
721
+ throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
722
+ `Set an environment variable (e.g., ANTHROPIC_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
723
+ }
724
+ const primary = allProviders[0];
725
+ const model = modelOverride || primary.model || (0, llm_1.getDefaultModel)(primary.provider);
726
+ const providerName = primary.provider;
727
+ const apiKeyEnvVar = llm_1.PROVIDER_ENV_VARS[providerName];
728
+ // 3. Check LLM SDK installed
729
+ const sdkPackage = AGENT_RUNNER_SDK_PACKAGES[providerName] || 'anthropic';
730
+ const sdkImportName = providerName === 'gemini' ? 'google.genai' : sdkPackage;
731
+ try {
732
+ const { code } = await runCommand('python3', ['-c', `import ${sdkImportName}`]);
733
+ if (code !== 0) {
734
+ process.stderr.write(`Installing ${sdkPackage} Python SDK...\n`);
735
+ const install = await runCommand('python3', ['-m', 'pip', 'install', '-q', sdkPackage]);
736
+ if (install.code !== 0) {
737
+ throw new errors_1.CliError(`Failed to install ${sdkPackage} SDK.\n` +
738
+ `Install manually: pip install ${sdkPackage}`);
739
+ }
740
+ }
741
+ }
742
+ catch (err) {
743
+ if (err instanceof errors_1.CliError)
744
+ throw err;
745
+ throw new errors_1.CliError(`Failed to check Python SDK: ${err}`);
746
+ }
747
+ // 4. Create temp directory with agent files
748
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-local-${Date.now()}`);
749
+ await promises_1.default.mkdir(tempDir, { recursive: true });
750
+ try {
751
+ // Copy agent_runner.py from resources
752
+ const runnerSource = path_1.default.join(__dirname, '..', 'resources', 'agent_runner.py');
753
+ // Also check alternate path for dev mode (running from src/)
754
+ let runnerContent;
755
+ try {
756
+ runnerContent = await promises_1.default.readFile(runnerSource, 'utf-8');
757
+ }
758
+ catch {
759
+ // Fallback for dev: try src/resources relative to the project
760
+ const altSource = path_1.default.join(__dirname, '..', '..', 'src', 'resources', 'agent_runner.py');
761
+ try {
762
+ runnerContent = await promises_1.default.readFile(altSource, 'utf-8');
763
+ }
764
+ catch {
765
+ throw new errors_1.CliError('Agent runner script not found. This is a packaging error.\n' +
766
+ 'Please reinstall the CLI: npm install -g @orchagent/cli');
767
+ }
768
+ }
769
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'agent_runner.py'), runnerContent);
770
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'prompt.md'), prompt);
771
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'input.json'), JSON.stringify(inputData, null, 2));
772
+ if (outputSchema) {
773
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'output_schema.json'), JSON.stringify(outputSchema));
774
+ }
775
+ if (customTools && customTools.length > 0) {
776
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'custom_tools.json'), JSON.stringify(customTools));
777
+ }
778
+ // 5. Set env vars
779
+ const subprocessEnv = { ...process.env };
780
+ subprocessEnv.LOCAL_MODE = '1';
781
+ subprocessEnv.LLM_PROVIDER = providerName;
782
+ subprocessEnv.LLM_MODEL = model;
783
+ if (apiKeyEnvVar && primary.apiKey) {
784
+ subprocessEnv[apiKeyEnvVar] = primary.apiKey;
785
+ }
786
+ // 6. Print warning and run
787
+ process.stderr.write(chalk_1.default.yellow('\nWarning: Local mode. Bash commands execute on your machine (no sandbox).\n\n'));
788
+ process.stderr.write(`Running with ${providerName} (${model})...\n`);
789
+ const maxTurns = 25;
790
+ const proc = (0, child_process_1.spawn)('python3', ['agent_runner.py', '--max-turns', String(maxTurns), '--verbose'], {
791
+ cwd: tempDir,
792
+ stdio: ['pipe', 'pipe', 'pipe'],
793
+ env: subprocessEnv,
794
+ });
795
+ proc.stdin.end();
796
+ let stdout = '';
797
+ let stderr = '';
798
+ proc.stdout?.on('data', (data) => {
799
+ stdout += data.toString();
800
+ });
801
+ let lastUsage = null;
802
+ proc.stderr?.on('data', (data) => {
803
+ const text = data.toString();
804
+ stderr += text;
805
+ // Filter out heartbeat dots and orchagent events, show human-readable lines
806
+ for (const line of text.split('\n')) {
807
+ if (line.startsWith('@@ORCHAGENT_EVENT:')) {
808
+ try {
809
+ const evt = JSON.parse(line.slice('@@ORCHAGENT_EVENT:'.length));
810
+ if (evt.usage)
811
+ lastUsage = evt.usage;
812
+ }
813
+ catch { /* ignore parse errors */ }
814
+ continue;
815
+ }
816
+ if (line.trim() === '.' || line.trim() === '')
817
+ continue;
818
+ process.stderr.write(line + '\n');
819
+ }
820
+ });
821
+ const exitCode = await new Promise((resolve) => {
822
+ proc.on('close', (code) => resolve(code ?? 1));
823
+ proc.on('error', (err) => {
824
+ process.stderr.write(`Error running agent: ${err.message}\n`);
825
+ resolve(1);
826
+ });
827
+ });
828
+ // Display token usage if available
829
+ const usage = lastUsage;
830
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
831
+ const total = (usage.input_tokens || 0) + (usage.output_tokens || 0);
832
+ process.stderr.write(chalk_1.default.gray(`${total.toLocaleString()} tokens (${(usage.input_tokens || 0).toLocaleString()} in, ${(usage.output_tokens || 0).toLocaleString()} out)\n`));
833
+ }
834
+ // 7. Parse and print result
835
+ if (stdout.trim()) {
836
+ try {
837
+ const result = JSON.parse(stdout.trim());
838
+ if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
839
+ throw new errors_1.CliError(`Agent error: ${result.error}`);
840
+ }
841
+ if (exitCode !== 0) {
842
+ (0, output_1.printJson)(result);
843
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
844
+ }
845
+ (0, output_1.printJson)(result);
846
+ }
847
+ catch (err) {
848
+ if (err instanceof errors_1.CliError)
849
+ throw err;
850
+ process.stdout.write(stdout);
851
+ if (exitCode !== 0) {
852
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
853
+ }
854
+ }
855
+ }
856
+ else if (exitCode !== 0) {
857
+ throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)\n\n` +
858
+ `Common causes:\n` +
859
+ ` - Missing LLM API key (check ${apiKeyEnvVar || 'API key env var'})\n` +
860
+ ` - Python SDK not installed (pip install ${sdkPackage})\n` +
861
+ ` - Syntax error in prompt.md\n`);
862
+ }
863
+ }
864
+ finally {
865
+ try {
866
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
867
+ }
868
+ catch {
869
+ // Ignore cleanup errors
870
+ }
871
+ }
872
+ }
532
873
  function parseSkillRef(value) {
533
874
  const [ref, versionPart] = value.split('@');
534
875
  const version = versionPart?.trim() || 'v1';
@@ -930,10 +1271,57 @@ async function executeLocalFromDir(dirPath, args, options) {
930
1271
  `Install with: orchagent skill install <org>/<skill>`);
931
1272
  }
932
1273
  if (agentType === 'agent') {
933
- throw new errors_1.CliError('Agent type cannot be run locally.\n\n' +
934
- 'Agent type requires a sandbox environment with tool use capabilities.\n' +
935
- 'Publish first, then run in the cloud:\n' +
936
- ' orch publish && orch run <org>/<agent> --data \'{"task": "..."}\'');
1274
+ // Read prompt.md
1275
+ const promptPath = path_1.default.join(resolved, 'prompt.md');
1276
+ let agentPrompt;
1277
+ try {
1278
+ agentPrompt = await promises_1.default.readFile(promptPath, 'utf-8');
1279
+ }
1280
+ catch {
1281
+ throw new errors_1.CliError(`No prompt.md found in ${resolved}`);
1282
+ }
1283
+ // Read schema.json for output schema
1284
+ let agentOutputSchema;
1285
+ try {
1286
+ const schemaRaw = await promises_1.default.readFile(path_1.default.join(resolved, 'schema.json'), 'utf-8');
1287
+ const schemas = JSON.parse(schemaRaw);
1288
+ agentOutputSchema = schemas.output;
1289
+ }
1290
+ catch {
1291
+ // Schema is optional
1292
+ }
1293
+ // Read custom_tools from manifest
1294
+ const customTools = manifest.custom_tools || undefined;
1295
+ // Check for keyed file/mount injection
1296
+ const agentFileArgs = options.file ?? [];
1297
+ const agentKeyedFiles = agentFileArgs.filter(a => isKeyedFileArg(a) !== null);
1298
+ const agentHasInjection = agentKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1299
+ if (!options.input && !agentHasInjection) {
1300
+ process.stderr.write(`Loaded local agent: ${manifest.name || path_1.default.basename(resolved)}\n\n`);
1301
+ process.stderr.write(`Run with input:\n`);
1302
+ process.stderr.write(` orch run ${dirPath} --local --data '{\"task\": \"...\"}'\n`);
1303
+ return;
1304
+ }
1305
+ let agentInputData;
1306
+ if (agentHasInjection) {
1307
+ const injected = await buildInjectedPayload({
1308
+ dataOption: options.input,
1309
+ fileArgs: agentKeyedFiles,
1310
+ mountArgs: options.mount,
1311
+ });
1312
+ agentInputData = JSON.parse(injected.body);
1313
+ }
1314
+ else {
1315
+ try {
1316
+ agentInputData = JSON.parse(options.input);
1317
+ }
1318
+ catch {
1319
+ throw new errors_1.CliError('Invalid JSON input');
1320
+ }
1321
+ }
1322
+ const config = await (0, config_1.getResolvedConfig)();
1323
+ await executeAgentLocally(resolved, agentPrompt, agentInputData, agentOutputSchema, customTools, manifest, config, options.provider, options.model);
1324
+ return;
937
1325
  }
938
1326
  if (agentType === 'prompt') {
939
1327
  // Read prompt.md
@@ -969,18 +1357,32 @@ async function executeLocalFromDir(dirPath, args, options) {
969
1357
  supported_providers: manifest.supported_providers || ['any'],
970
1358
  default_models: manifest.default_models,
971
1359
  };
972
- if (!options.input) {
1360
+ // Check for keyed file/mount injection
1361
+ const localFileArgs = options.file ?? [];
1362
+ const localKeyedFiles = localFileArgs.filter(a => isKeyedFileArg(a) !== null);
1363
+ const localHasInjection = localKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1364
+ if (!options.input && !localHasInjection) {
973
1365
  process.stderr.write(`Loaded local agent: ${agentData.name}\n\n`);
974
1366
  process.stderr.write(`Run with input:\n`);
975
1367
  process.stderr.write(` orch run ${dirPath} --local --data '{...}'\n`);
976
1368
  return;
977
1369
  }
978
1370
  let inputData;
979
- try {
980
- inputData = JSON.parse(options.input);
1371
+ if (localHasInjection) {
1372
+ const injected = await buildInjectedPayload({
1373
+ dataOption: options.input,
1374
+ fileArgs: localKeyedFiles,
1375
+ mountArgs: options.mount,
1376
+ });
1377
+ inputData = JSON.parse(injected.body);
981
1378
  }
982
- catch {
983
- throw new errors_1.CliError('Invalid JSON input');
1379
+ else {
1380
+ try {
1381
+ inputData = JSON.parse(options.input);
1382
+ }
1383
+ catch {
1384
+ throw new errors_1.CliError('Invalid JSON input');
1385
+ }
984
1386
  }
985
1387
  const config = await (0, config_1.getResolvedConfig)();
986
1388
  const result = await executePromptLocally(agentData, inputData, [], config, options.provider, options.model);
@@ -1034,8 +1436,20 @@ async function executeLocalFromDir(dirPath, args, options) {
1034
1436
  catch {
1035
1437
  // No requirements.txt
1036
1438
  }
1439
+ // Check for keyed file/mount injection (tool path)
1440
+ const toolFileArgs = options.file ?? [];
1441
+ const toolKeyedFiles = toolFileArgs.filter(a => isKeyedFileArg(a) !== null);
1442
+ const toolHasInjection = toolKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1037
1443
  let inputJson = '{}';
1038
- if (options.input) {
1444
+ if (toolHasInjection) {
1445
+ const injected = await buildInjectedPayload({
1446
+ dataOption: options.input,
1447
+ fileArgs: toolKeyedFiles,
1448
+ mountArgs: options.mount,
1449
+ });
1450
+ inputJson = injected.body;
1451
+ }
1452
+ else if (options.input) {
1039
1453
  try {
1040
1454
  JSON.parse(options.input);
1041
1455
  inputJson = options.input;
@@ -1103,6 +1517,35 @@ async function executeLocalFromDir(dirPath, args, options) {
1103
1517
  }
1104
1518
  }
1105
1519
  // ─── Cloud execution path ───────────────────────────────────────────────────
1520
+ function renderProgress(event) {
1521
+ switch (event.type) {
1522
+ case 'turn_start':
1523
+ process.stderr.write(chalk_1.default.gray(` Turn ${event.turn}/${event.max_turns}\n`));
1524
+ break;
1525
+ case 'tool_call': {
1526
+ const icon = event.tool === 'bash'
1527
+ ? '$'
1528
+ : event.tool === 'read_file'
1529
+ ? '>'
1530
+ : event.tool === 'write_file'
1531
+ ? '<'
1532
+ : '~';
1533
+ process.stderr.write(chalk_1.default.cyan(` ${icon} ${event.tool}`) +
1534
+ chalk_1.default.gray(` ${event.args_brief || ''}\n`));
1535
+ break;
1536
+ }
1537
+ case 'tool_result':
1538
+ if (event.status === 'error')
1539
+ process.stderr.write(chalk_1.default.yellow(` (error)\n`));
1540
+ break;
1541
+ case 'done':
1542
+ process.stderr.write(chalk_1.default.green(` Done\n`));
1543
+ break;
1544
+ case 'error':
1545
+ process.stderr.write(chalk_1.default.red(` Error: ${event.message}\n`));
1546
+ break;
1547
+ }
1548
+ }
1106
1549
  async function executeCloud(agentRef, file, options) {
1107
1550
  // Merge --input alias into --data
1108
1551
  const dataValue = options.data || options.input;
@@ -1173,6 +1616,7 @@ async function executeCloud(agentRef, file, options) {
1173
1616
  const endpoint = options.endpoint?.trim() || agentMeta.default_endpoint || 'analyze';
1174
1617
  const headers = {
1175
1618
  Authorization: `Bearer ${resolved.apiKey}`,
1619
+ 'X-CLI-Version': package_json_1.default.version,
1176
1620
  };
1177
1621
  if (options.tenant) {
1178
1622
  headers['X-OrchAgent-Tenant'] = options.tenant;
@@ -1253,31 +1697,127 @@ async function executeCloud(agentRef, file, options) {
1253
1697
  }
1254
1698
  let body;
1255
1699
  let sourceLabel;
1256
- const filePaths = [
1700
+ const allFileArgs = [
1257
1701
  ...(options.file ?? []),
1258
1702
  ...(file ? [file] : []),
1259
1703
  ];
1260
- if (options.data && options.metadata) {
1261
- throw new errors_1.CliError('Cannot use --data with --metadata. Use one or the other.');
1262
- }
1263
- if (options.data && filePaths.length > 0) {
1264
- // Merge file content into --data
1265
- const resolvedBody = await resolveJsonBody(options.data);
1266
- const bodyObj = JSON.parse(resolvedBody);
1267
- if (agentMeta.type === 'prompt') {
1704
+ // Partition --file args into keyed (key=path) vs unkeyed (plain path)
1705
+ const keyedFileArgs = allFileArgs.filter(a => isKeyedFileArg(a) !== null);
1706
+ const unkeyedFileArgs = allFileArgs.filter(a => isKeyedFileArg(a) === null);
1707
+ const hasKeyed = keyedFileArgs.length > 0;
1708
+ const hasMounts = (options.mount ?? []).length > 0;
1709
+ const hasInjection = hasKeyed || hasMounts;
1710
+ // Cannot mix keyed and unkeyed --file args
1711
+ if (hasInjection && unkeyedFileArgs.length > 0) {
1712
+ throw new errors_1.CliError('Cannot mix keyed --file (key=path) with unkeyed --file (path) in the same command.\n\n' +
1713
+ 'Use either:\n' +
1714
+ ' Keyed: --file code=./main.py --file config=./config.toml\n' +
1715
+ ' Unkeyed: --file ./main.py --file ./config.toml');
1716
+ }
1717
+ if (hasInjection) {
1718
+ // Route to JSON injection path
1719
+ const injected = await buildInjectedPayload({
1720
+ dataOption: options.data,
1721
+ fileArgs: keyedFileArgs,
1722
+ mountArgs: options.mount,
1723
+ llmCredentials,
1724
+ });
1725
+ body = injected.body;
1726
+ sourceLabel = injected.sourceLabel;
1727
+ headers['Content-Type'] = 'application/json';
1728
+ }
1729
+ else {
1730
+ // Existing body construction logic (unkeyed files only)
1731
+ const filePaths = unkeyedFileArgs;
1732
+ if (options.data && options.metadata) {
1733
+ throw new errors_1.CliError('Cannot use --data with --metadata. Use one or the other.');
1734
+ }
1735
+ if (options.data && filePaths.length > 0) {
1736
+ // Merge file content into --data
1737
+ const resolvedBody = await resolveJsonBody(options.data);
1738
+ const bodyObj = JSON.parse(resolvedBody);
1739
+ if (agentMeta.type === 'prompt') {
1740
+ const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1741
+ if (filePaths.length === 1) {
1742
+ await validateFilePath(filePaths[0]);
1743
+ bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1744
+ sourceLabel = filePaths[0];
1745
+ }
1746
+ else {
1747
+ const allContents = {};
1748
+ for (const fp of filePaths) {
1749
+ await validateFilePath(fp);
1750
+ allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
1751
+ }
1752
+ bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1753
+ bodyObj.files = allContents;
1754
+ sourceLabel = `${filePaths.length} files`;
1755
+ }
1756
+ // Auto-populate filename if schema has it and user didn't provide it
1757
+ if (filePaths.length >= 1 && bodyObj.filename === undefined) {
1758
+ const schema = agentMeta.input_schema;
1759
+ const schemaProps = schema?.properties;
1760
+ if (schemaProps?.filename) {
1761
+ bodyObj.filename = path_1.default.basename(filePaths[0]);
1762
+ }
1763
+ }
1764
+ applySchemaDefaults(bodyObj, agentMeta.input_schema);
1765
+ if (llmCredentials)
1766
+ bodyObj.llm_credentials = llmCredentials;
1767
+ body = JSON.stringify(bodyObj);
1768
+ headers['Content-Type'] = 'application/json';
1769
+ }
1770
+ else {
1771
+ // Tool agents: send files as multipart, --data as metadata
1772
+ let metadata = resolvedBody;
1773
+ if (llmCredentials) {
1774
+ const metaObj = JSON.parse(metadata);
1775
+ metaObj.llm_credentials = llmCredentials;
1776
+ metadata = JSON.stringify(metaObj);
1777
+ }
1778
+ const multipart = await buildMultipartBody(filePaths, metadata);
1779
+ body = multipart.body;
1780
+ sourceLabel = multipart.sourceLabel;
1781
+ }
1782
+ }
1783
+ else if (options.data) {
1784
+ const resolvedBody = await resolveJsonBody(options.data);
1785
+ warnIfLocalPathReference(resolvedBody);
1786
+ if (llmCredentials) {
1787
+ const bodyObj = JSON.parse(resolvedBody);
1788
+ bodyObj.llm_credentials = llmCredentials;
1789
+ body = JSON.stringify(bodyObj);
1790
+ }
1791
+ else {
1792
+ body = resolvedBody;
1793
+ }
1794
+ headers['Content-Type'] = 'application/json';
1795
+ }
1796
+ else if ((filePaths.length > 0 || options.metadata) && agentMeta.type === 'prompt') {
1268
1797
  const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1798
+ let bodyObj = {};
1799
+ if (options.metadata) {
1800
+ try {
1801
+ bodyObj = JSON.parse(options.metadata);
1802
+ }
1803
+ catch {
1804
+ throw new errors_1.CliError('--metadata must be valid JSON.');
1805
+ }
1806
+ }
1269
1807
  if (filePaths.length === 1) {
1270
1808
  await validateFilePath(filePaths[0]);
1271
- bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1809
+ const fileContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1810
+ bodyObj[fieldName] = fileContent;
1272
1811
  sourceLabel = filePaths[0];
1273
1812
  }
1274
- else {
1813
+ else if (filePaths.length > 1) {
1275
1814
  const allContents = {};
1276
1815
  for (const fp of filePaths) {
1277
1816
  await validateFilePath(fp);
1278
1817
  allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
1279
1818
  }
1280
- bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1819
+ const firstContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1820
+ bodyObj[fieldName] = firstContent;
1281
1821
  bodyObj.files = allContents;
1282
1822
  sourceLabel = `${filePaths.length} files`;
1283
1823
  }
@@ -1290,16 +1830,16 @@ async function executeCloud(agentRef, file, options) {
1290
1830
  }
1291
1831
  }
1292
1832
  applySchemaDefaults(bodyObj, agentMeta.input_schema);
1293
- if (llmCredentials)
1833
+ if (llmCredentials) {
1294
1834
  bodyObj.llm_credentials = llmCredentials;
1835
+ }
1295
1836
  body = JSON.stringify(bodyObj);
1296
1837
  headers['Content-Type'] = 'application/json';
1297
1838
  }
1298
- else {
1299
- // Tool agents: send files as multipart, --data as metadata
1300
- let metadata = resolvedBody;
1839
+ else if (filePaths.length > 0 || options.metadata) {
1840
+ let metadata = options.metadata;
1301
1841
  if (llmCredentials) {
1302
- const metaObj = JSON.parse(metadata);
1842
+ const metaObj = metadata ? JSON.parse(metadata) : {};
1303
1843
  metaObj.llm_credentials = llmCredentials;
1304
1844
  metadata = JSON.stringify(metaObj);
1305
1845
  }
@@ -1307,92 +1847,34 @@ async function executeCloud(agentRef, file, options) {
1307
1847
  body = multipart.body;
1308
1848
  sourceLabel = multipart.sourceLabel;
1309
1849
  }
1310
- }
1311
- else if (options.data) {
1312
- const resolvedBody = await resolveJsonBody(options.data);
1313
- warnIfLocalPathReference(resolvedBody);
1314
- if (llmCredentials) {
1315
- const bodyObj = JSON.parse(resolvedBody);
1316
- bodyObj.llm_credentials = llmCredentials;
1317
- body = JSON.stringify(bodyObj);
1850
+ else if (llmCredentials) {
1851
+ body = JSON.stringify({ llm_credentials: llmCredentials });
1852
+ headers['Content-Type'] = 'application/json';
1318
1853
  }
1319
1854
  else {
1320
- body = resolvedBody;
1321
- }
1322
- headers['Content-Type'] = 'application/json';
1323
- }
1324
- else if ((filePaths.length > 0 || options.metadata) && agentMeta.type === 'prompt') {
1325
- const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1326
- let bodyObj = {};
1327
- if (options.metadata) {
1328
- try {
1329
- bodyObj = JSON.parse(options.metadata);
1330
- }
1331
- catch {
1332
- throw new errors_1.CliError('--metadata must be valid JSON.');
1333
- }
1334
- }
1335
- if (filePaths.length === 1) {
1336
- await validateFilePath(filePaths[0]);
1337
- const fileContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1338
- bodyObj[fieldName] = fileContent;
1339
- sourceLabel = filePaths[0];
1340
- }
1341
- else if (filePaths.length > 1) {
1342
- const allContents = {};
1343
- for (const fp of filePaths) {
1344
- await validateFilePath(fp);
1345
- allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
1346
- }
1347
- const firstContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1348
- bodyObj[fieldName] = firstContent;
1349
- bodyObj.files = allContents;
1350
- sourceLabel = `${filePaths.length} files`;
1351
- }
1352
- // Auto-populate filename if schema has it and user didn't provide it
1353
- if (filePaths.length >= 1 && bodyObj.filename === undefined) {
1354
- const schema = agentMeta.input_schema;
1355
- const schemaProps = schema?.properties;
1356
- if (schemaProps?.filename) {
1357
- bodyObj.filename = path_1.default.basename(filePaths[0]);
1358
- }
1359
- }
1360
- applySchemaDefaults(bodyObj, agentMeta.input_schema);
1361
- if (llmCredentials) {
1362
- bodyObj.llm_credentials = llmCredentials;
1363
- }
1364
- body = JSON.stringify(bodyObj);
1365
- headers['Content-Type'] = 'application/json';
1366
- }
1367
- else if (filePaths.length > 0 || options.metadata) {
1368
- let metadata = options.metadata;
1369
- if (llmCredentials) {
1370
- const metaObj = metadata ? JSON.parse(metadata) : {};
1371
- metaObj.llm_credentials = llmCredentials;
1372
- metadata = JSON.stringify(metaObj);
1855
+ const multipart = await buildMultipartBody(undefined, options.metadata);
1856
+ body = multipart.body;
1857
+ sourceLabel = multipart.sourceLabel;
1373
1858
  }
1374
- const multipart = await buildMultipartBody(filePaths, metadata);
1375
- body = multipart.body;
1376
- sourceLabel = multipart.sourceLabel;
1377
- }
1378
- else if (llmCredentials) {
1379
- body = JSON.stringify({ llm_credentials: llmCredentials });
1380
- headers['Content-Type'] = 'application/json';
1381
- }
1382
- else {
1383
- const multipart = await buildMultipartBody(undefined, options.metadata);
1384
- body = multipart.body;
1385
- sourceLabel = multipart.sourceLabel;
1386
- }
1859
+ } // end of non-injection path
1387
1860
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
1861
+ // Enable SSE streaming for agent-type agents (unless --json or --no-stream or --output)
1862
+ const isAgentType = agentMeta.type === 'agent';
1863
+ const wantStream = isAgentType && !options.json && !options.noStream && !options.output;
1864
+ if (wantStream) {
1865
+ headers['Accept'] = 'text/event-stream';
1866
+ }
1388
1867
  const spinner = options.json ? null : (0, spinner_1.createSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
1389
1868
  spinner?.start();
1869
+ // Agent-type runs can take much longer; use 10 min timeout for streaming
1870
+ const timeoutMs = isAgentType ? 600000 : undefined;
1390
1871
  let response;
1391
1872
  try {
1392
1873
  response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
1393
1874
  method: 'POST',
1394
1875
  headers,
1395
1876
  body,
1877
+ ...(timeoutMs ? { timeoutMs } : {}),
1396
1878
  });
1397
1879
  }
1398
1880
  catch (err) {
@@ -1423,6 +1905,15 @@ async function executeCloud(agentRef, file, options) {
1423
1905
  ' orch billing balance # check current balance\n';
1424
1906
  throw new errors_1.CliError(errorMessage, errors_1.ExitCodes.PERMISSION_DENIED);
1425
1907
  }
1908
+ if (errorCode === 'CLI_VERSION_TOO_OLD') {
1909
+ spinner?.fail('CLI version too old');
1910
+ const minVersion = typeof payload === 'object' && payload
1911
+ ? payload.error?.min_version
1912
+ : undefined;
1913
+ throw new errors_1.CliError(`Your CLI version (${package_json_1.default.version}) is too old.\n\n` +
1914
+ (minVersion ? `Minimum required: ${minVersion}\n` : '') +
1915
+ 'Update with: npm update -g @orchagent/cli');
1916
+ }
1426
1917
  if (errorCode === 'LLM_KEY_REQUIRED') {
1427
1918
  spinner?.fail('LLM key required');
1428
1919
  throw new errors_1.CliError('This public agent requires you to provide an LLM key.\n' +
@@ -1452,19 +1943,94 @@ async function executeCloud(agentRef, file, options) {
1452
1943
  spinner?.fail(`Run failed: ${message}`);
1453
1944
  throw new errors_1.CliError(message);
1454
1945
  }
1946
+ // Handle SSE streaming response
1947
+ const contentType = response.headers?.get?.('content-type') || '';
1948
+ if (contentType.includes('text/event-stream') && response.body) {
1949
+ spinner?.stop();
1950
+ const { parseSSE } = await Promise.resolve().then(() => __importStar(require('../lib/sse.js')));
1951
+ let finalPayload = null;
1952
+ let hadError = false;
1953
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
1954
+ for await (const { event, data } of parseSSE(response.body)) {
1955
+ if (event === 'progress') {
1956
+ try {
1957
+ renderProgress(JSON.parse(data));
1958
+ }
1959
+ catch {
1960
+ // ignore malformed progress events
1961
+ }
1962
+ }
1963
+ else if (event === 'result') {
1964
+ try {
1965
+ finalPayload = JSON.parse(data);
1966
+ }
1967
+ catch {
1968
+ finalPayload = data;
1969
+ }
1970
+ }
1971
+ else if (event === 'error') {
1972
+ hadError = true;
1973
+ try {
1974
+ finalPayload = JSON.parse(data);
1975
+ }
1976
+ catch {
1977
+ finalPayload = data;
1978
+ }
1979
+ }
1980
+ }
1981
+ process.stderr.write('\n');
1982
+ await (0, analytics_1.track)('cli_run', {
1983
+ agent: `${org}/${parsed.agent}@${parsed.version}`,
1984
+ input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
1985
+ mode: 'cloud',
1986
+ streamed: true,
1987
+ });
1988
+ if (hadError) {
1989
+ const errMsg = typeof finalPayload === 'object' && finalPayload
1990
+ ? finalPayload.error?.message || 'Agent execution failed'
1991
+ : 'Agent execution failed';
1992
+ throw new errors_1.CliError(errMsg);
1993
+ }
1994
+ if (finalPayload !== null) {
1995
+ (0, output_1.printJson)(finalPayload);
1996
+ if (typeof finalPayload === 'object' && finalPayload !== null && 'metadata' in finalPayload) {
1997
+ const meta = finalPayload.metadata;
1998
+ if (meta) {
1999
+ const parts = [];
2000
+ if (typeof meta.processing_time_ms === 'number') {
2001
+ parts.push(`${(meta.processing_time_ms / 1000).toFixed(1)}s total`);
2002
+ }
2003
+ if (typeof meta.execution_time_ms === 'number') {
2004
+ parts.push(`${(meta.execution_time_ms / 1000).toFixed(1)}s execution`);
2005
+ }
2006
+ const usage = meta.usage;
2007
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
2008
+ const total = (usage.input_tokens || 0) + (usage.output_tokens || 0);
2009
+ parts.push(`${total.toLocaleString()} tokens (${(usage.input_tokens || 0).toLocaleString()} in, ${(usage.output_tokens || 0).toLocaleString()} out)`);
2010
+ }
2011
+ if (parts.length > 0) {
2012
+ process.stderr.write(chalk_1.default.gray(`${parts.join(' · ')}\n`));
2013
+ }
2014
+ }
2015
+ }
2016
+ }
2017
+ return;
2018
+ }
1455
2019
  spinner?.succeed(`Ran ${org}/${parsed.agent}@${parsed.version}`);
1456
2020
  if (!options.json && (0, pricing_1.isPaidAgent)(agentMeta) && pricingInfo?.price_cents && pricingInfo.price_cents > 0) {
1457
2021
  process.stderr.write(`\nCost: $${(pricingInfo.price_cents / 100).toFixed(2)} USD\n`);
1458
2022
  }
1459
- const inputType = filePaths.length > 0
1460
- ? 'file'
1461
- : options.data
1462
- ? 'json'
1463
- : sourceLabel === 'stdin'
1464
- ? 'stdin'
1465
- : sourceLabel === 'metadata'
1466
- ? 'metadata'
1467
- : 'empty';
2023
+ const inputType = hasInjection
2024
+ ? 'file_injection'
2025
+ : unkeyedFileArgs.length > 0
2026
+ ? 'file'
2027
+ : options.data
2028
+ ? 'json'
2029
+ : sourceLabel === 'stdin'
2030
+ ? 'stdin'
2031
+ : sourceLabel === 'metadata'
2032
+ ? 'metadata'
2033
+ : 'empty';
1468
2034
  await (0, analytics_1.track)('cli_run', {
1469
2035
  agent: `${org}/${parsed.agent}@${parsed.version}`,
1470
2036
  input_type: inputType,
@@ -1497,6 +2063,27 @@ async function executeCloud(agentRef, file, options) {
1497
2063
  return;
1498
2064
  }
1499
2065
  (0, output_1.printJson)(payload);
2066
+ // Display timing metadata on stderr (non-json mode only)
2067
+ if (typeof payload === 'object' && payload !== null && 'metadata' in payload) {
2068
+ const meta = payload.metadata;
2069
+ if (meta) {
2070
+ const parts = [];
2071
+ if (typeof meta.processing_time_ms === 'number') {
2072
+ parts.push(`${(meta.processing_time_ms / 1000).toFixed(1)}s total`);
2073
+ }
2074
+ if (typeof meta.execution_time_ms === 'number') {
2075
+ parts.push(`${(meta.execution_time_ms / 1000).toFixed(1)}s execution`);
2076
+ }
2077
+ const usage = meta.usage;
2078
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
2079
+ const total = (usage.input_tokens || 0) + (usage.output_tokens || 0);
2080
+ parts.push(`${total.toLocaleString()} tokens (${(usage.input_tokens || 0).toLocaleString()} in, ${(usage.output_tokens || 0).toLocaleString()} out)`);
2081
+ }
2082
+ if (parts.length > 0) {
2083
+ process.stderr.write(chalk_1.default.gray(`\n${parts.join(' · ')}\n`));
2084
+ }
2085
+ }
2086
+ }
1500
2087
  }
1501
2088
  // ─── Local execution path ───────────────────────────────────────────────────
1502
2089
  async function executeLocal(agentRef, args, options) {
@@ -1555,12 +2142,40 @@ async function executeLocal(agentRef, args, options) {
1555
2142
  ` Install for AI tools: orchagent skill install ${org}/${parsed.agent}\n` +
1556
2143
  ` Use with an agent: orchagent run <agent> --skills ${org}/${parsed.agent}`);
1557
2144
  }
1558
- // Agent type requires a sandbox cannot run locally
2145
+ // Agent type: execute locally with the agent runner
1559
2146
  if (agentData.type === 'agent') {
1560
- throw new errors_1.CliError('Agent type cannot be run locally.\n\n' +
1561
- 'Agent type requires a sandbox environment with tool use capabilities.\n\n' +
1562
- 'Remove the --local flag to run in the cloud:\n' +
1563
- ` orch run ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
2147
+ if (!agentData.prompt) {
2148
+ throw new errors_1.CliError('Agent prompt not available for local execution.\n\n' +
2149
+ 'This agent may have local download disabled.\n' +
2150
+ 'Remove the --local flag to run in the cloud:\n' +
2151
+ ` orch run ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
2152
+ }
2153
+ if (!options.input) {
2154
+ process.stderr.write(`\nAgent downloaded. Run with:\n`);
2155
+ process.stderr.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --data '{\"task\": \"...\"}'\n`);
2156
+ return;
2157
+ }
2158
+ let agentInputData;
2159
+ try {
2160
+ agentInputData = JSON.parse(options.input);
2161
+ }
2162
+ catch {
2163
+ throw new errors_1.CliError('Invalid JSON input');
2164
+ }
2165
+ // Write prompt to temp dir and run
2166
+ const tempAgentDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-${parsed.agent}-${Date.now()}`);
2167
+ await promises_1.default.mkdir(tempAgentDir, { recursive: true });
2168
+ try {
2169
+ await promises_1.default.writeFile(path_1.default.join(tempAgentDir, 'prompt.md'), agentData.prompt);
2170
+ await executeAgentLocally(tempAgentDir, agentData.prompt, agentInputData, agentData.output_schema, undefined, {}, resolved, options.provider, options.model);
2171
+ }
2172
+ finally {
2173
+ try {
2174
+ await promises_1.default.rm(tempAgentDir, { recursive: true, force: true });
2175
+ }
2176
+ catch { /* ignore */ }
2177
+ }
2178
+ return;
1564
2179
  }
1565
2180
  // Check for dependencies (orchestrator agents)
1566
2181
  if (agentData.dependencies && agentData.dependencies.length > 0) {
@@ -1610,7 +2225,20 @@ async function executeLocal(agentRef, args, options) {
1610
2225
  process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
1611
2226
  return;
1612
2227
  }
1613
- await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, options.input);
2228
+ // Pre-build injected payload for bundle agent if keyed files/mounts present
2229
+ const bundleFileArgs = options.file ?? [];
2230
+ const bundleKeyedFiles = bundleFileArgs.filter(a => isKeyedFileArg(a) !== null);
2231
+ const bundleHasInjection = bundleKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
2232
+ let bundleInput = options.input;
2233
+ if (bundleHasInjection) {
2234
+ const injected = await buildInjectedPayload({
2235
+ dataOption: options.input,
2236
+ fileArgs: bundleKeyedFiles,
2237
+ mountArgs: options.mount,
2238
+ });
2239
+ bundleInput = injected.body;
2240
+ }
2241
+ await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, bundleInput);
1614
2242
  return;
1615
2243
  }
1616
2244
  if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
@@ -1632,18 +2260,32 @@ async function executeLocal(agentRef, args, options) {
1632
2260
  process.stdout.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1633
2261
  return;
1634
2262
  }
2263
+ // Check for keyed file/mount injection
2264
+ const execLocalFileArgs = options.file ?? [];
2265
+ const execLocalKeyedFiles = execLocalFileArgs.filter(a => isKeyedFileArg(a) !== null);
2266
+ const execLocalHasInjection = execLocalKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1635
2267
  // For prompt-based agents, execute locally
1636
- if (!options.input) {
2268
+ if (!options.input && !execLocalHasInjection) {
1637
2269
  process.stdout.write(`\nPrompt-based agent ready.\n`);
1638
2270
  process.stdout.write(`Run with: orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1639
2271
  return;
1640
2272
  }
1641
2273
  let inputData;
1642
- try {
1643
- inputData = JSON.parse(options.input);
2274
+ if (execLocalHasInjection) {
2275
+ const injected = await buildInjectedPayload({
2276
+ dataOption: options.input,
2277
+ fileArgs: execLocalKeyedFiles,
2278
+ mountArgs: options.mount,
2279
+ });
2280
+ inputData = JSON.parse(injected.body);
1644
2281
  }
1645
- catch {
1646
- throw new errors_1.CliError('Invalid JSON input');
2282
+ else {
2283
+ try {
2284
+ inputData = JSON.parse(options.input);
2285
+ }
2286
+ catch {
2287
+ throw new errors_1.CliError('Invalid JSON input');
2288
+ }
1647
2289
  }
1648
2290
  // Handle skill composition
1649
2291
  let skillPrompts = [];
@@ -1680,12 +2322,14 @@ function registerRunCommand(program) {
1680
2322
  .option('--skills <skills>', 'Add skills (comma-separated)')
1681
2323
  .option('--skills-only <skills>', 'Use only these skills')
1682
2324
  .option('--no-skills', 'Ignore default skills')
2325
+ .option('--no-stream', 'Disable real-time streaming for agent-type agents')
1683
2326
  // Cloud-only options
1684
2327
  .option('--endpoint <endpoint>', 'Override agent endpoint (cloud only)')
1685
2328
  .option('--tenant <tenant>', 'Tenant identifier for multi-tenant callers (cloud only)')
1686
2329
  .option('--output <file>', 'Save response body to a file (cloud only)')
1687
- .option('--file <path...>', 'File(s) to upload (cloud only, can specify multiple)')
2330
+ .option('--file <path...>', 'File(s) to upload or inject as keyed fields (key=path)')
1688
2331
  .option('--file-field <field>', 'Schema field name for file content (cloud only)')
2332
+ .option('--mount <field=dir...>', 'Mount a directory as a JSON field map (field=dir, can specify multiple)')
1689
2333
  .option('--metadata <json>', 'JSON metadata to send with files (cloud only)')
1690
2334
  // Local-only options
1691
2335
  .option('--download-only', 'Just download the agent, do not execute (local only)')
@@ -1701,6 +2345,15 @@ Examples:
1701
2345
  cat input.json | orch run acme/agent --data @-
1702
2346
  orch run acme/image-processor photo.jpg --output result.png
1703
2347
 
2348
+ Keyed file injection (--file key=path):
2349
+ orch run agent --file code=./src/lib.cairo
2350
+ orch run agent --data '{"filter": "test_add"}' --file code=./src/lib.cairo
2351
+ orch run agent --file config=./Scarb.toml --file code=./src/lib.cairo
2352
+
2353
+ Directory mount (--mount field=dir):
2354
+ orch run agent --mount source_files=./src/ --mount test_files=./tests/
2355
+ orch run agent --data '{"filter": "test_add"}' --mount src=./src/ --file config=./Scarb.toml
2356
+
1704
2357
  Local execution (--local):
1705
2358
  orch run orchagent/leak-finder --local --data '{"path": "."}'
1706
2359
  orch run joe/summarizer --local --data '{"text": "Hello world"}'
@@ -1718,6 +2371,10 @@ File handling (cloud):
1718
2371
  input schema. Use --file-field to specify the field name (auto-detected by default).
1719
2372
  For tools, files are uploaded as multipart form data.
1720
2373
 
2374
+ Use --file key=path to inject a file's content at a specific JSON field.
2375
+ Use --mount field=dir to inject a directory tree as a {path: content} map.
2376
+ These produce standard JSON payloads - no server changes needed.
2377
+
1721
2378
  Important: Remote agents cannot access your local filesystem. If your --data payload
1722
2379
  contains keys like 'path', 'directory', 'file', etc., those values will be interpreted
1723
2380
  by the server, not your local machine. To use local files, use --local or --file.