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