@orchagent/cli 0.2.2 → 0.2.3

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,165 @@ 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
+ await new Promise((resolve, reject) => {
394
+ proc.on('close', (code) => {
395
+ if (code !== 0) {
396
+ reject(new errors_1.CliError(`Agent exited with code ${code}`));
397
+ }
398
+ else {
399
+ resolve();
400
+ }
401
+ });
402
+ proc.on('error', (err) => {
403
+ reject(err);
404
+ });
405
+ });
406
+ // Parse and print the output
407
+ if (stdout.trim()) {
408
+ try {
409
+ const result = JSON.parse(stdout.trim());
410
+ (0, output_1.printJson)(result);
411
+ }
412
+ catch {
413
+ // Not JSON, print as-is
414
+ process.stdout.write(stdout);
415
+ }
416
+ }
417
+ }
418
+ finally {
419
+ // Clean up temp directory
420
+ try {
421
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
422
+ }
423
+ catch {
424
+ // Ignore cleanup errors
425
+ }
426
+ }
427
+ }
269
428
  async function saveAgentLocally(org, agent, agentData) {
270
429
  const agentDir = path_1.default.join(AGENTS_DIR, org, agent);
271
430
  await promises_1.default.mkdir(agentDir, { recursive: true });
@@ -351,7 +510,18 @@ function registerRunCommand(program) {
351
510
  const agentDir = await saveAgentLocally(org, parsed.agent, agentData);
352
511
  process.stderr.write(`\nAgent saved to: ${agentDir}\n`);
353
512
  if (agentData.type === 'code') {
354
- // Check if this agent supports local execution
513
+ // Check if this agent has a bundle available for local execution
514
+ if (agentData.has_bundle) {
515
+ if (options.downloadOnly) {
516
+ process.stdout.write(`\nCode agent has bundle available for local execution.\n`);
517
+ process.stdout.write(`Run with: orch run ${org}/${parsed.agent} [args...]\n`);
518
+ return;
519
+ }
520
+ // Execute the bundle-based code agent locally
521
+ await executeBundleAgent(resolved, org, parsed.agent, parsed.version, agentData, args);
522
+ return;
523
+ }
524
+ // Check for pip/source-based local execution (legacy)
355
525
  if (agentData.run_command && (agentData.source_url || agentData.pip_package)) {
356
526
  if (options.downloadOnly) {
357
527
  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.3",
4
4
  "description": "Command-line interface for the OrchAgent AI agent marketplace",
5
5
  "license": "MIT",
6
6
  "author": "OrchAgent <hello@orchagent.io>",