@orchagent/cli 0.2.2 → 0.2.4

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.
@@ -266,6 +266,183 @@ async function executeCodeAgent(agentData, args) {
266
266
  process.exit(code);
267
267
  }
268
268
  }
269
+ async function unzipBundle(zipPath, destDir) {
270
+ // Use spawn with array arguments to avoid shell injection
271
+ return new Promise((resolve, reject) => {
272
+ const proc = (0, child_process_1.spawn)('unzip', ['-q', zipPath, '-d', destDir], {
273
+ stdio: ['ignore', 'pipe', 'pipe'],
274
+ });
275
+ let stderr = '';
276
+ proc.stderr?.on('data', (data) => {
277
+ stderr += data.toString();
278
+ });
279
+ proc.on('close', (code) => {
280
+ if (code !== 0) {
281
+ reject(new errors_1.CliError(`Failed to extract bundle: ${stderr || `exit code ${code}`}`));
282
+ }
283
+ else {
284
+ resolve();
285
+ }
286
+ });
287
+ proc.on('error', (err) => {
288
+ reject(new errors_1.CliError(`Failed to run unzip: ${err.message}. Make sure unzip is installed.`));
289
+ });
290
+ });
291
+ }
292
+ async function executeBundleAgent(config, org, agentName, version, agentData, args) {
293
+ // Create temp directory for the bundle
294
+ const tempDir = path_1.default.join(os_1.default.tmpdir(), `orchagent-${agentName}-${Date.now()}`);
295
+ await promises_1.default.mkdir(tempDir, { recursive: true });
296
+ const bundleZip = path_1.default.join(tempDir, 'bundle.zip');
297
+ const extractDir = path_1.default.join(tempDir, 'agent');
298
+ try {
299
+ // Download the bundle
300
+ process.stderr.write(`Downloading bundle...\n`);
301
+ const bundleBuffer = await (0, api_1.downloadCodeBundle)(config, org, agentName, version);
302
+ await promises_1.default.writeFile(bundleZip, bundleBuffer);
303
+ process.stderr.write(`Bundle downloaded (${bundleBuffer.length} bytes)\n`);
304
+ // Extract the bundle
305
+ await promises_1.default.mkdir(extractDir, { recursive: true });
306
+ process.stderr.write(`Extracting bundle...\n`);
307
+ await unzipBundle(bundleZip, extractDir);
308
+ // Check if requirements.txt exists and install dependencies
309
+ const requirementsPath = path_1.default.join(extractDir, 'requirements.txt');
310
+ try {
311
+ await promises_1.default.access(requirementsPath);
312
+ process.stderr.write(`Installing dependencies from requirements.txt...\n`);
313
+ const { code } = await runCommand('python3', ['-m', 'pip', 'install', '-q', '-r', requirementsPath]);
314
+ if (code !== 0) {
315
+ throw new errors_1.CliError('Failed to install dependencies from requirements.txt');
316
+ }
317
+ }
318
+ catch (err) {
319
+ if (err.code !== 'ENOENT') {
320
+ throw err;
321
+ }
322
+ // requirements.txt doesn't exist, skip installation
323
+ }
324
+ // Determine entrypoint
325
+ const entrypoint = agentData.entrypoint || 'sandbox_main.py';
326
+ const entrypointPath = path_1.default.join(extractDir, entrypoint);
327
+ // Verify entrypoint exists
328
+ try {
329
+ await promises_1.default.access(entrypointPath);
330
+ }
331
+ catch {
332
+ throw new errors_1.CliError(`Entrypoint not found: ${entrypoint}`);
333
+ }
334
+ // Build input JSON from args
335
+ // The first arg should be the input (file path or JSON string)
336
+ let inputJson = '{}';
337
+ if (args.length > 0) {
338
+ const firstArg = args[0];
339
+ // Check if it's a file path
340
+ try {
341
+ const stat = await promises_1.default.stat(firstArg);
342
+ if (stat.isFile()) {
343
+ // Read file content as input
344
+ const fileContent = await promises_1.default.readFile(firstArg, 'utf-8');
345
+ // Check if it's already JSON
346
+ try {
347
+ JSON.parse(fileContent);
348
+ inputJson = fileContent;
349
+ }
350
+ catch {
351
+ // Wrap as file_path in JSON
352
+ inputJson = JSON.stringify({ file_path: firstArg });
353
+ }
354
+ }
355
+ else if (stat.isDirectory()) {
356
+ // Pass directory path
357
+ inputJson = JSON.stringify({ directory: firstArg });
358
+ }
359
+ }
360
+ catch {
361
+ // Not a file, check if it's JSON
362
+ try {
363
+ JSON.parse(firstArg);
364
+ inputJson = firstArg;
365
+ }
366
+ catch {
367
+ // Treat as a simple string input
368
+ inputJson = JSON.stringify({ input: firstArg });
369
+ }
370
+ }
371
+ }
372
+ // Run the entrypoint with input via stdin
373
+ process.stderr.write(`\nRunning: python3 ${entrypoint}\n\n`);
374
+ const proc = (0, child_process_1.spawn)('python3', [entrypointPath], {
375
+ cwd: extractDir,
376
+ stdio: ['pipe', 'pipe', 'pipe'],
377
+ });
378
+ // Send input JSON via stdin
379
+ proc.stdin.write(inputJson);
380
+ proc.stdin.end();
381
+ // Collect output
382
+ let stdout = '';
383
+ let stderr = '';
384
+ proc.stdout?.on('data', (data) => {
385
+ const text = data.toString();
386
+ stdout += text;
387
+ });
388
+ proc.stderr?.on('data', (data) => {
389
+ const text = data.toString();
390
+ stderr += text;
391
+ process.stderr.write(text);
392
+ });
393
+ const exitCode = await new Promise((resolve) => {
394
+ proc.on('close', (code) => {
395
+ resolve(code ?? 1);
396
+ });
397
+ proc.on('error', (err) => {
398
+ process.stderr.write(`Error running agent: ${err.message}\n`);
399
+ resolve(1);
400
+ });
401
+ });
402
+ // Handle output - check for errors in stdout even on failure
403
+ if (stdout.trim()) {
404
+ try {
405
+ const result = JSON.parse(stdout.trim());
406
+ // Check if it's an error response
407
+ if (exitCode !== 0 && typeof result === 'object' && result !== null && 'error' in result) {
408
+ throw new errors_1.CliError(`Agent error: ${result.error}`);
409
+ }
410
+ if (exitCode !== 0) {
411
+ // Non-zero exit but output isn't an error object - show it and fail
412
+ (0, output_1.printJson)(result);
413
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
414
+ }
415
+ // Success - print result
416
+ (0, output_1.printJson)(result);
417
+ }
418
+ catch (err) {
419
+ if (err instanceof errors_1.CliError)
420
+ throw err;
421
+ // Not JSON, print as-is
422
+ process.stdout.write(stdout);
423
+ if (exitCode !== 0) {
424
+ throw new errors_1.CliError(`Agent exited with code ${exitCode}`);
425
+ }
426
+ }
427
+ }
428
+ else if (exitCode !== 0) {
429
+ // No stdout, check stderr
430
+ if (stderr.trim()) {
431
+ throw new errors_1.CliError(`Agent error: ${stderr.trim()}`);
432
+ }
433
+ throw new errors_1.CliError(`Agent exited with code ${exitCode} (no output)`);
434
+ }
435
+ }
436
+ finally {
437
+ // Clean up temp directory
438
+ try {
439
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
440
+ }
441
+ catch {
442
+ // Ignore cleanup errors
443
+ }
444
+ }
445
+ }
269
446
  async function saveAgentLocally(org, agent, agentData) {
270
447
  const agentDir = path_1.default.join(AGENTS_DIR, org, agent);
271
448
  await promises_1.default.mkdir(agentDir, { recursive: true });
@@ -351,7 +528,18 @@ function registerRunCommand(program) {
351
528
  const agentDir = await saveAgentLocally(org, parsed.agent, agentData);
352
529
  process.stderr.write(`\nAgent saved to: ${agentDir}\n`);
353
530
  if (agentData.type === 'code') {
354
- // Check if this agent supports local execution
531
+ // Check if this agent has a bundle available for local execution
532
+ if (agentData.has_bundle) {
533
+ if (options.downloadOnly) {
534
+ process.stdout.write(`\nCode agent has bundle available for local execution.\n`);
535
+ process.stdout.write(`Run with: orch run ${org}/${parsed.agent} [args...]\n`);
536
+ return;
537
+ }
538
+ // Execute the bundle-based code agent locally
539
+ await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args);
540
+ return;
541
+ }
542
+ // Check for pip/source-based local execution (legacy)
355
543
  if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
356
544
  if (options.downloadOnly) {
357
545
  process.stdout.write(`\nCode agent ready for local execution.\n`);
package/dist/lib/api.js CHANGED
@@ -47,6 +47,7 @@ exports.unstarAgent = unstarAgent;
47
47
  exports.forkAgent = forkAgent;
48
48
  exports.searchAgents = searchAgents;
49
49
  exports.fetchLlmKeys = fetchLlmKeys;
50
+ exports.downloadCodeBundle = downloadCodeBundle;
50
51
  exports.uploadCodeBundle = uploadCodeBundle;
51
52
  class ApiError extends Error {
52
53
  status;
@@ -151,6 +152,17 @@ async function fetchLlmKeys(config) {
151
152
  const result = await request(config, 'GET', '/llm-keys/export');
152
153
  return result.keys;
153
154
  }
155
+ /**
156
+ * Download a code bundle for local execution.
157
+ */
158
+ async function downloadCodeBundle(config, org, agent, version) {
159
+ const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/public/agents/${org}/${agent}/${version}/bundle`);
160
+ if (!response.ok) {
161
+ throw await parseError(response);
162
+ }
163
+ const arrayBuffer = await response.arrayBuffer();
164
+ return Buffer.from(arrayBuffer);
165
+ }
154
166
  /**
155
167
  * Upload a code bundle for a hosted code agent.
156
168
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Command-line interface for the OrchAgent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "OrchAgent <hello@orchagent.io>",