@orchagent/cli 0.3.48 → 0.3.51

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");
@@ -219,6 +224,164 @@ async function resolveJsonBody(input) {
219
224
  throw (0, errors_1.jsonInputError)('data');
220
225
  }
221
226
  }
227
+ // ─── Keyed file & mount helpers ──────────────────────────────────────────────
228
+ const KEYED_FILE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
229
+ function isKeyedFileArg(arg) {
230
+ const eqIndex = arg.indexOf('=');
231
+ if (eqIndex <= 0)
232
+ return null;
233
+ const key = arg.slice(0, eqIndex);
234
+ const filePath = arg.slice(eqIndex + 1);
235
+ if (!KEYED_FILE_KEY_RE.test(key))
236
+ return null;
237
+ if (!filePath)
238
+ return null;
239
+ return { key, filePath };
240
+ }
241
+ async function readKeyedFiles(args) {
242
+ const result = {};
243
+ for (const arg of args) {
244
+ const parsed = isKeyedFileArg(arg);
245
+ if (!parsed)
246
+ continue;
247
+ const resolved = path_1.default.resolve(parsed.filePath);
248
+ let stat;
249
+ try {
250
+ stat = await promises_1.default.stat(resolved);
251
+ }
252
+ catch {
253
+ throw new errors_1.CliError(`File not found: ${parsed.filePath}`);
254
+ }
255
+ if (!stat.isFile()) {
256
+ throw new errors_1.CliError(`Not a file: ${parsed.filePath}`);
257
+ }
258
+ result[parsed.key] = await promises_1.default.readFile(resolved, 'utf-8');
259
+ }
260
+ return result;
261
+ }
262
+ const MOUNT_SKIP_DIRS = new Set([
263
+ 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build',
264
+ '.next', 'target', '.cache', '.tox', 'coverage', '__snapshots__',
265
+ ]);
266
+ const MOUNT_MAX_DEPTH = 15;
267
+ const MOUNT_MAX_FILES = 500;
268
+ async function mountDirectory(dirPath) {
269
+ const resolved = path_1.default.resolve(dirPath);
270
+ let stat;
271
+ try {
272
+ stat = await promises_1.default.stat(resolved);
273
+ }
274
+ catch {
275
+ throw new errors_1.CliError(`Directory not found: ${dirPath}`);
276
+ }
277
+ if (!stat.isDirectory()) {
278
+ throw new errors_1.CliError(`Not a directory: ${dirPath}`);
279
+ }
280
+ const result = {};
281
+ let fileCount = 0;
282
+ async function walk(currentPath, relativePath, depth) {
283
+ if (depth > MOUNT_MAX_DEPTH)
284
+ return;
285
+ let names;
286
+ try {
287
+ names = await promises_1.default.readdir(currentPath);
288
+ }
289
+ catch {
290
+ return;
291
+ }
292
+ for (const name of names) {
293
+ if (name.startsWith('.'))
294
+ continue;
295
+ if (MOUNT_SKIP_DIRS.has(name))
296
+ continue;
297
+ const fullPath = path_1.default.join(currentPath, name);
298
+ const relPath = relativePath ? `${relativePath}/${name}` : name;
299
+ // Stat the entry (also skips symlinks)
300
+ let entryStat;
301
+ try {
302
+ entryStat = await promises_1.default.lstat(fullPath);
303
+ if (entryStat.isSymbolicLink())
304
+ continue;
305
+ }
306
+ catch {
307
+ continue;
308
+ }
309
+ if (entryStat.isDirectory()) {
310
+ await walk(fullPath, relPath, depth + 1);
311
+ }
312
+ else if (entryStat.isFile()) {
313
+ if (fileCount >= MOUNT_MAX_FILES) {
314
+ throw new errors_1.CliError(`Mount exceeds ${MOUNT_MAX_FILES} files. Use a more specific path or fewer files.`);
315
+ }
316
+ try {
317
+ const content = await promises_1.default.readFile(fullPath, 'utf-8');
318
+ result[relPath] = content;
319
+ fileCount++;
320
+ }
321
+ catch {
322
+ // Skip binary/unreadable files silently
323
+ }
324
+ }
325
+ }
326
+ }
327
+ await walk(resolved, '', 0);
328
+ return result;
329
+ }
330
+ const INJECT_MAX_BYTES = 4 * 1024 * 1024; // 4MB
331
+ async function buildInjectedPayload(options) {
332
+ let merged = {};
333
+ // 1. Start with --data
334
+ if (options.dataOption) {
335
+ const resolved = await resolveJsonBody(options.dataOption);
336
+ merged = JSON.parse(resolved);
337
+ }
338
+ let totalBytes = 0;
339
+ // 2. Overlay keyed --file entries
340
+ if (options.fileArgs && options.fileArgs.length > 0) {
341
+ const keyedFiles = await readKeyedFiles(options.fileArgs);
342
+ for (const [key, content] of Object.entries(keyedFiles)) {
343
+ totalBytes += Buffer.byteLength(content, 'utf-8');
344
+ merged[key] = content;
345
+ }
346
+ }
347
+ // 3. Overlay --mount entries
348
+ if (options.mountArgs && options.mountArgs.length > 0) {
349
+ for (const mountArg of options.mountArgs) {
350
+ const eqIndex = mountArg.indexOf('=');
351
+ if (eqIndex <= 0) {
352
+ throw new errors_1.CliError(`Invalid --mount format: ${mountArg}. Use --mount field=dir`);
353
+ }
354
+ const field = mountArg.slice(0, eqIndex);
355
+ const dirPath = mountArg.slice(eqIndex + 1);
356
+ if (!KEYED_FILE_KEY_RE.test(field)) {
357
+ throw new errors_1.CliError(`Invalid mount field name: ${field}. Must be a valid identifier.`);
358
+ }
359
+ const fileMap = await mountDirectory(dirPath);
360
+ for (const content of Object.values(fileMap)) {
361
+ totalBytes += Buffer.byteLength(content, 'utf-8');
362
+ }
363
+ merged[field] = fileMap;
364
+ }
365
+ }
366
+ // 4. Enforce size limit
367
+ if (totalBytes > INJECT_MAX_BYTES) {
368
+ throw new errors_1.CliError(`File content exceeds 4MB limit (${(totalBytes / 1024 / 1024).toFixed(1)}MB). ` +
369
+ `Use a more specific path or fewer files.`);
370
+ }
371
+ // 5. Inject llm_credentials
372
+ if (options.llmCredentials) {
373
+ merged.llm_credentials = options.llmCredentials;
374
+ }
375
+ const parts = [];
376
+ if (options.fileArgs && options.fileArgs.length > 0) {
377
+ parts.push(`${options.fileArgs.length} file(s)`);
378
+ }
379
+ if (options.mountArgs && options.mountArgs.length > 0) {
380
+ parts.push(`${options.mountArgs.length} mount(s)`);
381
+ }
382
+ const sourceLabel = parts.join(' + ');
383
+ return { body: JSON.stringify(merged), sourceLabel };
384
+ }
222
385
  // ─── Local execution helpers ────────────────────────────────────────────────
223
386
  async function downloadAgent(config, org, agent, version) {
224
387
  // Try public endpoint first
@@ -529,6 +692,169 @@ async function executePromptLocally(agentData, inputData, skillPrompts = [], con
529
692
  return await (0, llm_1.callLlm)(provider, key, model, prompt, agentData.output_schema);
530
693
  }, { successText: `Completed with ${provider}` });
531
694
  }
695
+ // ─── Local agent-type execution ──────────────────────────────────────────────
696
+ const AGENT_RUNNER_SDK_PACKAGES = {
697
+ anthropic: 'anthropic',
698
+ openai: 'openai',
699
+ gemini: 'google-genai',
700
+ };
701
+ async function executeAgentLocally(agentDir, prompt, inputData, outputSchema, customTools, manifest, config, providerOverride, modelOverride) {
702
+ // 1. Check Python 3 available
703
+ try {
704
+ const { code } = await runCommand('python3', ['--version']);
705
+ if (code !== 0)
706
+ throw new Error();
707
+ }
708
+ catch {
709
+ throw new errors_1.CliError('Python 3 is required for local agent execution.\n' +
710
+ 'Install Python 3: https://python.org/downloads');
711
+ }
712
+ // 2. Detect LLM provider + key
713
+ const supportedProviders = manifest?.supported_providers || ['any'];
714
+ const providersToCheck = providerOverride
715
+ ? [providerOverride]
716
+ : supportedProviders;
717
+ const allProviders = await detectAllLlmKeys(providersToCheck, config);
718
+ if (allProviders.length === 0) {
719
+ const providers = providersToCheck.join(', ');
720
+ throw new errors_1.CliError(`No LLM key found for: ${providers}\n` +
721
+ `Set an environment variable (e.g., ANTHROPIC_API_KEY), run 'orchagent keys add <provider>', or configure in web dashboard`);
722
+ }
723
+ const primary = allProviders[0];
724
+ const model = modelOverride || primary.model || (0, llm_1.getDefaultModel)(primary.provider);
725
+ const providerName = primary.provider;
726
+ const apiKeyEnvVar = llm_1.PROVIDER_ENV_VARS[providerName];
727
+ // 3. Check LLM SDK installed
728
+ const sdkPackage = AGENT_RUNNER_SDK_PACKAGES[providerName] || 'anthropic';
729
+ const sdkImportName = providerName === 'gemini' ? 'google.genai' : sdkPackage;
730
+ try {
731
+ const { code } = await runCommand('python3', ['-c', `import ${sdkImportName}`]);
732
+ if (code !== 0) {
733
+ process.stderr.write(`Installing ${sdkPackage} Python SDK...\n`);
734
+ const install = await runCommand('python3', ['-m', 'pip', 'install', '-q', sdkPackage]);
735
+ if (install.code !== 0) {
736
+ throw new errors_1.CliError(`Failed to install ${sdkPackage} SDK.\n` +
737
+ `Install manually: pip install ${sdkPackage}`);
738
+ }
739
+ }
740
+ }
741
+ catch (err) {
742
+ if (err instanceof errors_1.CliError)
743
+ throw err;
744
+ throw new errors_1.CliError(`Failed to check Python SDK: ${err}`);
745
+ }
746
+ // 4. Create temp directory with agent files
747
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-local-${Date.now()}`);
748
+ await promises_1.default.mkdir(tempDir, { recursive: true });
749
+ try {
750
+ // Copy agent_runner.py from resources
751
+ const runnerSource = path_1.default.join(__dirname, '..', 'resources', 'agent_runner.py');
752
+ // Also check alternate path for dev mode (running from src/)
753
+ let runnerContent;
754
+ try {
755
+ runnerContent = await promises_1.default.readFile(runnerSource, 'utf-8');
756
+ }
757
+ catch {
758
+ // Fallback for dev: try src/resources relative to the project
759
+ const altSource = path_1.default.join(__dirname, '..', '..', 'src', 'resources', 'agent_runner.py');
760
+ try {
761
+ runnerContent = await promises_1.default.readFile(altSource, 'utf-8');
762
+ }
763
+ catch {
764
+ throw new errors_1.CliError('Agent runner script not found. This is a packaging error.\n' +
765
+ 'Please reinstall the CLI: npm install -g @orchagent/cli');
766
+ }
767
+ }
768
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'agent_runner.py'), runnerContent);
769
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'prompt.md'), prompt);
770
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'input.json'), JSON.stringify(inputData, null, 2));
771
+ if (outputSchema) {
772
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'output_schema.json'), JSON.stringify(outputSchema));
773
+ }
774
+ if (customTools && customTools.length > 0) {
775
+ await promises_1.default.writeFile(path_1.default.join(tempDir, 'custom_tools.json'), JSON.stringify(customTools));
776
+ }
777
+ // 5. Set env vars
778
+ const subprocessEnv = { ...process.env };
779
+ subprocessEnv.LOCAL_MODE = '1';
780
+ subprocessEnv.LLM_PROVIDER = providerName;
781
+ subprocessEnv.LLM_MODEL = model;
782
+ if (apiKeyEnvVar && primary.apiKey) {
783
+ subprocessEnv[apiKeyEnvVar] = primary.apiKey;
784
+ }
785
+ // 6. Print warning and run
786
+ process.stderr.write(chalk_1.default.yellow('\nWarning: Local mode. Bash commands execute on your machine (no sandbox).\n\n'));
787
+ process.stderr.write(`Running with ${providerName} (${model})...\n`);
788
+ const maxTurns = 25;
789
+ const proc = (0, child_process_1.spawn)('python3', ['agent_runner.py', '--max-turns', String(maxTurns), '--verbose'], {
790
+ cwd: tempDir,
791
+ stdio: ['pipe', 'pipe', 'pipe'],
792
+ env: subprocessEnv,
793
+ });
794
+ proc.stdin.end();
795
+ let stdout = '';
796
+ let stderr = '';
797
+ proc.stdout?.on('data', (data) => {
798
+ stdout += data.toString();
799
+ });
800
+ proc.stderr?.on('data', (data) => {
801
+ const text = data.toString();
802
+ stderr += text;
803
+ // Filter out heartbeat dots and orchagent events, show human-readable lines
804
+ for (const line of text.split('\n')) {
805
+ if (line.startsWith('@@ORCHAGENT_EVENT:'))
806
+ continue;
807
+ if (line.trim() === '.' || line.trim() === '')
808
+ continue;
809
+ process.stderr.write(line + '\n');
810
+ }
811
+ });
812
+ const exitCode = await new Promise((resolve) => {
813
+ proc.on('close', (code) => resolve(code ?? 1));
814
+ proc.on('error', (err) => {
815
+ process.stderr.write(`Error running agent: ${err.message}\n`);
816
+ resolve(1);
817
+ });
818
+ });
819
+ // 7. Parse and print result
820
+ if (stdout.trim()) {
821
+ try {
822
+ const result = JSON.parse(stdout.trim());
823
+ if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
824
+ throw new errors_1.CliError(`Agent error: ${result.error}`);
825
+ }
826
+ if (exitCode !== 0) {
827
+ (0, output_1.printJson)(result);
828
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
829
+ }
830
+ (0, output_1.printJson)(result);
831
+ }
832
+ catch (err) {
833
+ if (err instanceof errors_1.CliError)
834
+ throw err;
835
+ process.stdout.write(stdout);
836
+ if (exitCode !== 0) {
837
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
838
+ }
839
+ }
840
+ }
841
+ else if (exitCode !== 0) {
842
+ throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)\n\n` +
843
+ `Common causes:\n` +
844
+ ` - Missing LLM API key (check ${apiKeyEnvVar || 'API key env var'})\n` +
845
+ ` - Python SDK not installed (pip install ${sdkPackage})\n` +
846
+ ` - Syntax error in prompt.md\n`);
847
+ }
848
+ }
849
+ finally {
850
+ try {
851
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
852
+ }
853
+ catch {
854
+ // Ignore cleanup errors
855
+ }
856
+ }
857
+ }
532
858
  function parseSkillRef(value) {
533
859
  const [ref, versionPart] = value.split('@');
534
860
  const version = versionPart?.trim() || 'v1';
@@ -930,10 +1256,57 @@ async function executeLocalFromDir(dirPath, args, options) {
930
1256
  `Install with: orchagent skill install <org>/<skill>`);
931
1257
  }
932
1258
  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": "..."}\'');
1259
+ // Read prompt.md
1260
+ const promptPath = path_1.default.join(resolved, 'prompt.md');
1261
+ let agentPrompt;
1262
+ try {
1263
+ agentPrompt = await promises_1.default.readFile(promptPath, 'utf-8');
1264
+ }
1265
+ catch {
1266
+ throw new errors_1.CliError(`No prompt.md found in ${resolved}`);
1267
+ }
1268
+ // Read schema.json for output schema
1269
+ let agentOutputSchema;
1270
+ try {
1271
+ const schemaRaw = await promises_1.default.readFile(path_1.default.join(resolved, 'schema.json'), 'utf-8');
1272
+ const schemas = JSON.parse(schemaRaw);
1273
+ agentOutputSchema = schemas.output;
1274
+ }
1275
+ catch {
1276
+ // Schema is optional
1277
+ }
1278
+ // Read custom_tools from manifest
1279
+ const customTools = manifest.custom_tools || undefined;
1280
+ // Check for keyed file/mount injection
1281
+ const agentFileArgs = options.file ?? [];
1282
+ const agentKeyedFiles = agentFileArgs.filter(a => isKeyedFileArg(a) !== null);
1283
+ const agentHasInjection = agentKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1284
+ if (!options.input && !agentHasInjection) {
1285
+ process.stderr.write(`Loaded local agent: ${manifest.name || path_1.default.basename(resolved)}\n\n`);
1286
+ process.stderr.write(`Run with input:\n`);
1287
+ process.stderr.write(` orch run ${dirPath} --local --data '{\"task\": \"...\"}'\n`);
1288
+ return;
1289
+ }
1290
+ let agentInputData;
1291
+ if (agentHasInjection) {
1292
+ const injected = await buildInjectedPayload({
1293
+ dataOption: options.input,
1294
+ fileArgs: agentKeyedFiles,
1295
+ mountArgs: options.mount,
1296
+ });
1297
+ agentInputData = JSON.parse(injected.body);
1298
+ }
1299
+ else {
1300
+ try {
1301
+ agentInputData = JSON.parse(options.input);
1302
+ }
1303
+ catch {
1304
+ throw new errors_1.CliError('Invalid JSON input');
1305
+ }
1306
+ }
1307
+ const config = await (0, config_1.getResolvedConfig)();
1308
+ await executeAgentLocally(resolved, agentPrompt, agentInputData, agentOutputSchema, customTools, manifest, config, options.provider, options.model);
1309
+ return;
937
1310
  }
938
1311
  if (agentType === 'prompt') {
939
1312
  // Read prompt.md
@@ -969,18 +1342,32 @@ async function executeLocalFromDir(dirPath, args, options) {
969
1342
  supported_providers: manifest.supported_providers || ['any'],
970
1343
  default_models: manifest.default_models,
971
1344
  };
972
- if (!options.input) {
1345
+ // Check for keyed file/mount injection
1346
+ const localFileArgs = options.file ?? [];
1347
+ const localKeyedFiles = localFileArgs.filter(a => isKeyedFileArg(a) !== null);
1348
+ const localHasInjection = localKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1349
+ if (!options.input && !localHasInjection) {
973
1350
  process.stderr.write(`Loaded local agent: ${agentData.name}\n\n`);
974
1351
  process.stderr.write(`Run with input:\n`);
975
1352
  process.stderr.write(` orch run ${dirPath} --local --data '{...}'\n`);
976
1353
  return;
977
1354
  }
978
1355
  let inputData;
979
- try {
980
- inputData = JSON.parse(options.input);
1356
+ if (localHasInjection) {
1357
+ const injected = await buildInjectedPayload({
1358
+ dataOption: options.input,
1359
+ fileArgs: localKeyedFiles,
1360
+ mountArgs: options.mount,
1361
+ });
1362
+ inputData = JSON.parse(injected.body);
981
1363
  }
982
- catch {
983
- throw new errors_1.CliError('Invalid JSON input');
1364
+ else {
1365
+ try {
1366
+ inputData = JSON.parse(options.input);
1367
+ }
1368
+ catch {
1369
+ throw new errors_1.CliError('Invalid JSON input');
1370
+ }
984
1371
  }
985
1372
  const config = await (0, config_1.getResolvedConfig)();
986
1373
  const result = await executePromptLocally(agentData, inputData, [], config, options.provider, options.model);
@@ -1034,8 +1421,20 @@ async function executeLocalFromDir(dirPath, args, options) {
1034
1421
  catch {
1035
1422
  // No requirements.txt
1036
1423
  }
1424
+ // Check for keyed file/mount injection (tool path)
1425
+ const toolFileArgs = options.file ?? [];
1426
+ const toolKeyedFiles = toolFileArgs.filter(a => isKeyedFileArg(a) !== null);
1427
+ const toolHasInjection = toolKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1037
1428
  let inputJson = '{}';
1038
- if (options.input) {
1429
+ if (toolHasInjection) {
1430
+ const injected = await buildInjectedPayload({
1431
+ dataOption: options.input,
1432
+ fileArgs: toolKeyedFiles,
1433
+ mountArgs: options.mount,
1434
+ });
1435
+ inputJson = injected.body;
1436
+ }
1437
+ else if (options.input) {
1039
1438
  try {
1040
1439
  JSON.parse(options.input);
1041
1440
  inputJson = options.input;
@@ -1103,6 +1502,35 @@ async function executeLocalFromDir(dirPath, args, options) {
1103
1502
  }
1104
1503
  }
1105
1504
  // ─── Cloud execution path ───────────────────────────────────────────────────
1505
+ function renderProgress(event) {
1506
+ switch (event.type) {
1507
+ case 'turn_start':
1508
+ process.stderr.write(chalk_1.default.gray(` Turn ${event.turn}/${event.max_turns}\n`));
1509
+ break;
1510
+ case 'tool_call': {
1511
+ const icon = event.tool === 'bash'
1512
+ ? '$'
1513
+ : event.tool === 'read_file'
1514
+ ? '>'
1515
+ : event.tool === 'write_file'
1516
+ ? '<'
1517
+ : '~';
1518
+ process.stderr.write(chalk_1.default.cyan(` ${icon} ${event.tool}`) +
1519
+ chalk_1.default.gray(` ${event.args_brief || ''}\n`));
1520
+ break;
1521
+ }
1522
+ case 'tool_result':
1523
+ if (event.status === 'error')
1524
+ process.stderr.write(chalk_1.default.yellow(` (error)\n`));
1525
+ break;
1526
+ case 'done':
1527
+ process.stderr.write(chalk_1.default.green(` Done\n`));
1528
+ break;
1529
+ case 'error':
1530
+ process.stderr.write(chalk_1.default.red(` Error: ${event.message}\n`));
1531
+ break;
1532
+ }
1533
+ }
1106
1534
  async function executeCloud(agentRef, file, options) {
1107
1535
  // Merge --input alias into --data
1108
1536
  const dataValue = options.data || options.input;
@@ -1253,31 +1681,127 @@ async function executeCloud(agentRef, file, options) {
1253
1681
  }
1254
1682
  let body;
1255
1683
  let sourceLabel;
1256
- const filePaths = [
1684
+ const allFileArgs = [
1257
1685
  ...(options.file ?? []),
1258
1686
  ...(file ? [file] : []),
1259
1687
  ];
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') {
1688
+ // Partition --file args into keyed (key=path) vs unkeyed (plain path)
1689
+ const keyedFileArgs = allFileArgs.filter(a => isKeyedFileArg(a) !== null);
1690
+ const unkeyedFileArgs = allFileArgs.filter(a => isKeyedFileArg(a) === null);
1691
+ const hasKeyed = keyedFileArgs.length > 0;
1692
+ const hasMounts = (options.mount ?? []).length > 0;
1693
+ const hasInjection = hasKeyed || hasMounts;
1694
+ // Cannot mix keyed and unkeyed --file args
1695
+ if (hasInjection && unkeyedFileArgs.length > 0) {
1696
+ throw new errors_1.CliError('Cannot mix keyed --file (key=path) with unkeyed --file (path) in the same command.\n\n' +
1697
+ 'Use either:\n' +
1698
+ ' Keyed: --file code=./main.py --file config=./config.toml\n' +
1699
+ ' Unkeyed: --file ./main.py --file ./config.toml');
1700
+ }
1701
+ if (hasInjection) {
1702
+ // Route to JSON injection path
1703
+ const injected = await buildInjectedPayload({
1704
+ dataOption: options.data,
1705
+ fileArgs: keyedFileArgs,
1706
+ mountArgs: options.mount,
1707
+ llmCredentials,
1708
+ });
1709
+ body = injected.body;
1710
+ sourceLabel = injected.sourceLabel;
1711
+ headers['Content-Type'] = 'application/json';
1712
+ }
1713
+ else {
1714
+ // Existing body construction logic (unkeyed files only)
1715
+ const filePaths = unkeyedFileArgs;
1716
+ if (options.data && options.metadata) {
1717
+ throw new errors_1.CliError('Cannot use --data with --metadata. Use one or the other.');
1718
+ }
1719
+ if (options.data && filePaths.length > 0) {
1720
+ // Merge file content into --data
1721
+ const resolvedBody = await resolveJsonBody(options.data);
1722
+ const bodyObj = JSON.parse(resolvedBody);
1723
+ if (agentMeta.type === 'prompt') {
1724
+ const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1725
+ if (filePaths.length === 1) {
1726
+ await validateFilePath(filePaths[0]);
1727
+ bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1728
+ sourceLabel = filePaths[0];
1729
+ }
1730
+ else {
1731
+ const allContents = {};
1732
+ for (const fp of filePaths) {
1733
+ await validateFilePath(fp);
1734
+ allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
1735
+ }
1736
+ bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1737
+ bodyObj.files = allContents;
1738
+ sourceLabel = `${filePaths.length} files`;
1739
+ }
1740
+ // Auto-populate filename if schema has it and user didn't provide it
1741
+ if (filePaths.length >= 1 && bodyObj.filename === undefined) {
1742
+ const schema = agentMeta.input_schema;
1743
+ const schemaProps = schema?.properties;
1744
+ if (schemaProps?.filename) {
1745
+ bodyObj.filename = path_1.default.basename(filePaths[0]);
1746
+ }
1747
+ }
1748
+ applySchemaDefaults(bodyObj, agentMeta.input_schema);
1749
+ if (llmCredentials)
1750
+ bodyObj.llm_credentials = llmCredentials;
1751
+ body = JSON.stringify(bodyObj);
1752
+ headers['Content-Type'] = 'application/json';
1753
+ }
1754
+ else {
1755
+ // Tool agents: send files as multipart, --data as metadata
1756
+ let metadata = resolvedBody;
1757
+ if (llmCredentials) {
1758
+ const metaObj = JSON.parse(metadata);
1759
+ metaObj.llm_credentials = llmCredentials;
1760
+ metadata = JSON.stringify(metaObj);
1761
+ }
1762
+ const multipart = await buildMultipartBody(filePaths, metadata);
1763
+ body = multipart.body;
1764
+ sourceLabel = multipart.sourceLabel;
1765
+ }
1766
+ }
1767
+ else if (options.data) {
1768
+ const resolvedBody = await resolveJsonBody(options.data);
1769
+ warnIfLocalPathReference(resolvedBody);
1770
+ if (llmCredentials) {
1771
+ const bodyObj = JSON.parse(resolvedBody);
1772
+ bodyObj.llm_credentials = llmCredentials;
1773
+ body = JSON.stringify(bodyObj);
1774
+ }
1775
+ else {
1776
+ body = resolvedBody;
1777
+ }
1778
+ headers['Content-Type'] = 'application/json';
1779
+ }
1780
+ else if ((filePaths.length > 0 || options.metadata) && agentMeta.type === 'prompt') {
1268
1781
  const fieldName = options.fileField || inferFileField(agentMeta.input_schema);
1782
+ let bodyObj = {};
1783
+ if (options.metadata) {
1784
+ try {
1785
+ bodyObj = JSON.parse(options.metadata);
1786
+ }
1787
+ catch {
1788
+ throw new errors_1.CliError('--metadata must be valid JSON.');
1789
+ }
1790
+ }
1269
1791
  if (filePaths.length === 1) {
1270
1792
  await validateFilePath(filePaths[0]);
1271
- bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1793
+ const fileContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1794
+ bodyObj[fieldName] = fileContent;
1272
1795
  sourceLabel = filePaths[0];
1273
1796
  }
1274
- else {
1797
+ else if (filePaths.length > 1) {
1275
1798
  const allContents = {};
1276
1799
  for (const fp of filePaths) {
1277
1800
  await validateFilePath(fp);
1278
1801
  allContents[path_1.default.basename(fp)] = await promises_1.default.readFile(fp, 'utf-8');
1279
1802
  }
1280
- bodyObj[fieldName] = await promises_1.default.readFile(filePaths[0], 'utf-8');
1803
+ const firstContent = await promises_1.default.readFile(filePaths[0], 'utf-8');
1804
+ bodyObj[fieldName] = firstContent;
1281
1805
  bodyObj.files = allContents;
1282
1806
  sourceLabel = `${filePaths.length} files`;
1283
1807
  }
@@ -1290,16 +1814,16 @@ async function executeCloud(agentRef, file, options) {
1290
1814
  }
1291
1815
  }
1292
1816
  applySchemaDefaults(bodyObj, agentMeta.input_schema);
1293
- if (llmCredentials)
1817
+ if (llmCredentials) {
1294
1818
  bodyObj.llm_credentials = llmCredentials;
1819
+ }
1295
1820
  body = JSON.stringify(bodyObj);
1296
1821
  headers['Content-Type'] = 'application/json';
1297
1822
  }
1298
- else {
1299
- // Tool agents: send files as multipart, --data as metadata
1300
- let metadata = resolvedBody;
1823
+ else if (filePaths.length > 0 || options.metadata) {
1824
+ let metadata = options.metadata;
1301
1825
  if (llmCredentials) {
1302
- const metaObj = JSON.parse(metadata);
1826
+ const metaObj = metadata ? JSON.parse(metadata) : {};
1303
1827
  metaObj.llm_credentials = llmCredentials;
1304
1828
  metadata = JSON.stringify(metaObj);
1305
1829
  }
@@ -1307,92 +1831,34 @@ async function executeCloud(agentRef, file, options) {
1307
1831
  body = multipart.body;
1308
1832
  sourceLabel = multipart.sourceLabel;
1309
1833
  }
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);
1834
+ else if (llmCredentials) {
1835
+ body = JSON.stringify({ llm_credentials: llmCredentials });
1836
+ headers['Content-Type'] = 'application/json';
1318
1837
  }
1319
1838
  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);
1839
+ const multipart = await buildMultipartBody(undefined, options.metadata);
1840
+ body = multipart.body;
1841
+ sourceLabel = multipart.sourceLabel;
1373
1842
  }
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
- }
1843
+ } // end of non-injection path
1387
1844
  const url = `${resolved.apiUrl.replace(/\/$/, '')}/${org}/${parsed.agent}/${parsed.version}/${endpoint}`;
1845
+ // Enable SSE streaming for agent-type agents (unless --json or --no-stream or --output)
1846
+ const isAgentType = agentMeta.type === 'agent';
1847
+ const wantStream = isAgentType && !options.json && !options.noStream && !options.output;
1848
+ if (wantStream) {
1849
+ headers['Accept'] = 'text/event-stream';
1850
+ }
1388
1851
  const spinner = options.json ? null : (0, spinner_1.createSpinner)(`Running ${org}/${parsed.agent}@${parsed.version}...`);
1389
1852
  spinner?.start();
1853
+ // Agent-type runs can take much longer; use 10 min timeout for streaming
1854
+ const timeoutMs = isAgentType ? 600000 : undefined;
1390
1855
  let response;
1391
1856
  try {
1392
1857
  response = await (0, api_1.safeFetchWithRetryForCalls)(url, {
1393
1858
  method: 'POST',
1394
1859
  headers,
1395
1860
  body,
1861
+ ...(timeoutMs ? { timeoutMs } : {}),
1396
1862
  });
1397
1863
  }
1398
1864
  catch (err) {
@@ -1452,19 +1918,89 @@ async function executeCloud(agentRef, file, options) {
1452
1918
  spinner?.fail(`Run failed: ${message}`);
1453
1919
  throw new errors_1.CliError(message);
1454
1920
  }
1921
+ // Handle SSE streaming response
1922
+ const contentType = response.headers?.get?.('content-type') || '';
1923
+ if (contentType.includes('text/event-stream') && response.body) {
1924
+ spinner?.stop();
1925
+ const { parseSSE } = await Promise.resolve().then(() => __importStar(require('../lib/sse.js')));
1926
+ let finalPayload = null;
1927
+ let hadError = false;
1928
+ process.stderr.write(chalk_1.default.gray(`\nStreaming ${org}/${parsed.agent}@${parsed.version}:\n`));
1929
+ for await (const { event, data } of parseSSE(response.body)) {
1930
+ if (event === 'progress') {
1931
+ try {
1932
+ renderProgress(JSON.parse(data));
1933
+ }
1934
+ catch {
1935
+ // ignore malformed progress events
1936
+ }
1937
+ }
1938
+ else if (event === 'result') {
1939
+ try {
1940
+ finalPayload = JSON.parse(data);
1941
+ }
1942
+ catch {
1943
+ finalPayload = data;
1944
+ }
1945
+ }
1946
+ else if (event === 'error') {
1947
+ hadError = true;
1948
+ try {
1949
+ finalPayload = JSON.parse(data);
1950
+ }
1951
+ catch {
1952
+ finalPayload = data;
1953
+ }
1954
+ }
1955
+ }
1956
+ process.stderr.write('\n');
1957
+ await (0, analytics_1.track)('cli_run', {
1958
+ agent: `${org}/${parsed.agent}@${parsed.version}`,
1959
+ input_type: hasInjection ? 'file_injection' : unkeyedFileArgs.length > 0 ? 'file' : options.data ? 'json' : 'empty',
1960
+ mode: 'cloud',
1961
+ streamed: true,
1962
+ });
1963
+ if (hadError) {
1964
+ const errMsg = typeof finalPayload === 'object' && finalPayload
1965
+ ? finalPayload.error?.message || 'Agent execution failed'
1966
+ : 'Agent execution failed';
1967
+ throw new errors_1.CliError(errMsg);
1968
+ }
1969
+ if (finalPayload !== null) {
1970
+ (0, output_1.printJson)(finalPayload);
1971
+ if (typeof finalPayload === 'object' && finalPayload !== null && 'metadata' in finalPayload) {
1972
+ const meta = finalPayload.metadata;
1973
+ if (meta) {
1974
+ const parts = [];
1975
+ if (typeof meta.processing_time_ms === 'number') {
1976
+ parts.push(`${(meta.processing_time_ms / 1000).toFixed(1)}s total`);
1977
+ }
1978
+ if (typeof meta.execution_time_ms === 'number') {
1979
+ parts.push(`${(meta.execution_time_ms / 1000).toFixed(1)}s execution`);
1980
+ }
1981
+ if (parts.length > 0) {
1982
+ process.stderr.write(chalk_1.default.gray(`${parts.join(' · ')}\n`));
1983
+ }
1984
+ }
1985
+ }
1986
+ }
1987
+ return;
1988
+ }
1455
1989
  spinner?.succeed(`Ran ${org}/${parsed.agent}@${parsed.version}`);
1456
1990
  if (!options.json && (0, pricing_1.isPaidAgent)(agentMeta) && pricingInfo?.price_cents && pricingInfo.price_cents > 0) {
1457
1991
  process.stderr.write(`\nCost: $${(pricingInfo.price_cents / 100).toFixed(2)} USD\n`);
1458
1992
  }
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';
1993
+ const inputType = hasInjection
1994
+ ? 'file_injection'
1995
+ : unkeyedFileArgs.length > 0
1996
+ ? 'file'
1997
+ : options.data
1998
+ ? 'json'
1999
+ : sourceLabel === 'stdin'
2000
+ ? 'stdin'
2001
+ : sourceLabel === 'metadata'
2002
+ ? 'metadata'
2003
+ : 'empty';
1468
2004
  await (0, analytics_1.track)('cli_run', {
1469
2005
  agent: `${org}/${parsed.agent}@${parsed.version}`,
1470
2006
  input_type: inputType,
@@ -1497,6 +2033,22 @@ async function executeCloud(agentRef, file, options) {
1497
2033
  return;
1498
2034
  }
1499
2035
  (0, output_1.printJson)(payload);
2036
+ // Display timing metadata on stderr (non-json mode only)
2037
+ if (typeof payload === 'object' && payload !== null && 'metadata' in payload) {
2038
+ const meta = payload.metadata;
2039
+ if (meta) {
2040
+ const parts = [];
2041
+ if (typeof meta.processing_time_ms === 'number') {
2042
+ parts.push(`${(meta.processing_time_ms / 1000).toFixed(1)}s total`);
2043
+ }
2044
+ if (typeof meta.execution_time_ms === 'number') {
2045
+ parts.push(`${(meta.execution_time_ms / 1000).toFixed(1)}s execution`);
2046
+ }
2047
+ if (parts.length > 0) {
2048
+ process.stderr.write(chalk_1.default.gray(`\n${parts.join(' · ')}\n`));
2049
+ }
2050
+ }
2051
+ }
1500
2052
  }
1501
2053
  // ─── Local execution path ───────────────────────────────────────────────────
1502
2054
  async function executeLocal(agentRef, args, options) {
@@ -1555,12 +2107,40 @@ async function executeLocal(agentRef, args, options) {
1555
2107
  ` Install for AI tools: orchagent skill install ${org}/${parsed.agent}\n` +
1556
2108
  ` Use with an agent: orchagent run <agent> --skills ${org}/${parsed.agent}`);
1557
2109
  }
1558
- // Agent type requires a sandbox cannot run locally
2110
+ // Agent type: execute locally with the agent runner
1559
2111
  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": "..."}'`);
2112
+ if (!agentData.prompt) {
2113
+ throw new errors_1.CliError('Agent prompt not available for local execution.\n\n' +
2114
+ 'This agent may have local download disabled.\n' +
2115
+ 'Remove the --local flag to run in the cloud:\n' +
2116
+ ` orch run ${org}/${parsed.agent}@${parsed.version} --data '{"task": "..."}'`);
2117
+ }
2118
+ if (!options.input) {
2119
+ process.stderr.write(`\nAgent downloaded. Run with:\n`);
2120
+ process.stderr.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --data '{\"task\": \"...\"}'\n`);
2121
+ return;
2122
+ }
2123
+ let agentInputData;
2124
+ try {
2125
+ agentInputData = JSON.parse(options.input);
2126
+ }
2127
+ catch {
2128
+ throw new errors_1.CliError('Invalid JSON input');
2129
+ }
2130
+ // Write prompt to temp dir and run
2131
+ const tempAgentDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-agent-${parsed.agent}-${Date.now()}`);
2132
+ await promises_1.default.mkdir(tempAgentDir, { recursive: true });
2133
+ try {
2134
+ await promises_1.default.writeFile(path_1.default.join(tempAgentDir, 'prompt.md'), agentData.prompt);
2135
+ await executeAgentLocally(tempAgentDir, agentData.prompt, agentInputData, agentData.output_schema, undefined, {}, resolved, options.provider, options.model);
2136
+ }
2137
+ finally {
2138
+ try {
2139
+ await promises_1.default.rm(tempAgentDir, { recursive: true, force: true });
2140
+ }
2141
+ catch { /* ignore */ }
2142
+ }
2143
+ return;
1564
2144
  }
1565
2145
  // Check for dependencies (orchestrator agents)
1566
2146
  if (agentData.dependencies && agentData.dependencies.length > 0) {
@@ -1610,7 +2190,20 @@ async function executeLocal(agentRef, args, options) {
1610
2190
  process.stdout.write(`Run with: orch run ${org}/${parsed.agent} --local [args...]\n`);
1611
2191
  return;
1612
2192
  }
1613
- await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, options.input);
2193
+ // Pre-build injected payload for bundle agent if keyed files/mounts present
2194
+ const bundleFileArgs = options.file ?? [];
2195
+ const bundleKeyedFiles = bundleFileArgs.filter(a => isKeyedFileArg(a) !== null);
2196
+ const bundleHasInjection = bundleKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
2197
+ let bundleInput = options.input;
2198
+ if (bundleHasInjection) {
2199
+ const injected = await buildInjectedPayload({
2200
+ dataOption: options.input,
2201
+ fileArgs: bundleKeyedFiles,
2202
+ mountArgs: options.mount,
2203
+ });
2204
+ bundleInput = injected.body;
2205
+ }
2206
+ await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args, bundleInput);
1614
2207
  return;
1615
2208
  }
1616
2209
  if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
@@ -1632,18 +2225,32 @@ async function executeLocal(agentRef, args, options) {
1632
2225
  process.stdout.write(` orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1633
2226
  return;
1634
2227
  }
2228
+ // Check for keyed file/mount injection
2229
+ const execLocalFileArgs = options.file ?? [];
2230
+ const execLocalKeyedFiles = execLocalFileArgs.filter(a => isKeyedFileArg(a) !== null);
2231
+ const execLocalHasInjection = execLocalKeyedFiles.length > 0 || (options.mount ?? []).length > 0;
1635
2232
  // For prompt-based agents, execute locally
1636
- if (!options.input) {
2233
+ if (!options.input && !execLocalHasInjection) {
1637
2234
  process.stdout.write(`\nPrompt-based agent ready.\n`);
1638
2235
  process.stdout.write(`Run with: orch run ${org}/${parsed.agent}@${parsed.version} --local --input '{...}'\n`);
1639
2236
  return;
1640
2237
  }
1641
2238
  let inputData;
1642
- try {
1643
- inputData = JSON.parse(options.input);
2239
+ if (execLocalHasInjection) {
2240
+ const injected = await buildInjectedPayload({
2241
+ dataOption: options.input,
2242
+ fileArgs: execLocalKeyedFiles,
2243
+ mountArgs: options.mount,
2244
+ });
2245
+ inputData = JSON.parse(injected.body);
1644
2246
  }
1645
- catch {
1646
- throw new errors_1.CliError('Invalid JSON input');
2247
+ else {
2248
+ try {
2249
+ inputData = JSON.parse(options.input);
2250
+ }
2251
+ catch {
2252
+ throw new errors_1.CliError('Invalid JSON input');
2253
+ }
1647
2254
  }
1648
2255
  // Handle skill composition
1649
2256
  let skillPrompts = [];
@@ -1680,12 +2287,14 @@ function registerRunCommand(program) {
1680
2287
  .option('--skills <skills>', 'Add skills (comma-separated)')
1681
2288
  .option('--skills-only <skills>', 'Use only these skills')
1682
2289
  .option('--no-skills', 'Ignore default skills')
2290
+ .option('--no-stream', 'Disable real-time streaming for agent-type agents')
1683
2291
  // Cloud-only options
1684
2292
  .option('--endpoint <endpoint>', 'Override agent endpoint (cloud only)')
1685
2293
  .option('--tenant <tenant>', 'Tenant identifier for multi-tenant callers (cloud only)')
1686
2294
  .option('--output <file>', 'Save response body to a file (cloud only)')
1687
- .option('--file <path...>', 'File(s) to upload (cloud only, can specify multiple)')
2295
+ .option('--file <path...>', 'File(s) to upload or inject as keyed fields (key=path)')
1688
2296
  .option('--file-field <field>', 'Schema field name for file content (cloud only)')
2297
+ .option('--mount <field=dir...>', 'Mount a directory as a JSON field map (field=dir, can specify multiple)')
1689
2298
  .option('--metadata <json>', 'JSON metadata to send with files (cloud only)')
1690
2299
  // Local-only options
1691
2300
  .option('--download-only', 'Just download the agent, do not execute (local only)')
@@ -1701,6 +2310,15 @@ Examples:
1701
2310
  cat input.json | orch run acme/agent --data @-
1702
2311
  orch run acme/image-processor photo.jpg --output result.png
1703
2312
 
2313
+ Keyed file injection (--file key=path):
2314
+ orch run agent --file code=./src/lib.cairo
2315
+ orch run agent --data '{"filter": "test_add"}' --file code=./src/lib.cairo
2316
+ orch run agent --file config=./Scarb.toml --file code=./src/lib.cairo
2317
+
2318
+ Directory mount (--mount field=dir):
2319
+ orch run agent --mount source_files=./src/ --mount test_files=./tests/
2320
+ orch run agent --data '{"filter": "test_add"}' --mount src=./src/ --file config=./Scarb.toml
2321
+
1704
2322
  Local execution (--local):
1705
2323
  orch run orchagent/leak-finder --local --data '{"path": "."}'
1706
2324
  orch run joe/summarizer --local --data '{"text": "Hello world"}'
@@ -1718,6 +2336,10 @@ File handling (cloud):
1718
2336
  input schema. Use --file-field to specify the field name (auto-detected by default).
1719
2337
  For tools, files are uploaded as multipart form data.
1720
2338
 
2339
+ Use --file key=path to inject a file's content at a specific JSON field.
2340
+ Use --mount field=dir to inject a directory tree as a {path: content} map.
2341
+ These produce standard JSON payloads - no server changes needed.
2342
+
1721
2343
  Important: Remote agents cannot access your local filesystem. If your --data payload
1722
2344
  contains keys like 'path', 'directory', 'file', etc., those values will be interpreted
1723
2345
  by the server, not your local machine. To use local files, use --local or --file.