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