@orchagent/cli 0.3.49 → 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.
- package/dist/commands/agent-keys.js +84 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/publish.js +42 -0
- package/dist/commands/run.js +744 -122
- package/dist/lib/api.js +12 -0
- package/dist/lib/errors.js +1 -0
- package/dist/lib/sse.js +41 -0
- package/package.json +3 -2
- package/src/resources/agent_runner.py +768 -0
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
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
|
-
|
|
980
|
-
|
|
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
|
-
|
|
983
|
-
|
|
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 (
|
|
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
|
|
1684
|
+
const allFileArgs = [
|
|
1257
1685
|
...(options.file ?? []),
|
|
1258
1686
|
...(file ? [file] : []),
|
|
1259
1687
|
];
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
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 =
|
|
1460
|
-
? '
|
|
1461
|
-
:
|
|
1462
|
-
? '
|
|
1463
|
-
:
|
|
1464
|
-
? '
|
|
1465
|
-
: sourceLabel === '
|
|
1466
|
-
? '
|
|
1467
|
-
: '
|
|
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
|
|
2110
|
+
// Agent type: execute locally with the agent runner
|
|
1559
2111
|
if (agentData.type === 'agent') {
|
|
1560
|
-
|
|
1561
|
-
'Agent
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
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
|
|
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.
|