@orchagent/cli 0.3.85 → 0.3.87

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.
@@ -42,17 +42,21 @@ exports.scanUndeclaredEnvVars = scanUndeclaredEnvVars;
42
42
  exports.scanReservedPort = scanReservedPort;
43
43
  exports.detectSdkCompatible = detectSdkCompatible;
44
44
  exports.checkDependencies = checkDependencies;
45
+ exports.batchPublish = batchPublish;
45
46
  exports.registerPublishCommand = registerPublishCommand;
46
47
  const promises_1 = __importDefault(require("fs/promises"));
47
48
  const path_1 = __importDefault(require("path"));
48
49
  const os_1 = __importDefault(require("os"));
49
50
  const yaml_1 = __importDefault(require("yaml"));
50
51
  const chalk_1 = __importDefault(require("chalk"));
52
+ const child_process_1 = require("child_process");
51
53
  const config_1 = require("../lib/config");
52
54
  const api_1 = require("../lib/api");
53
55
  const errors_1 = require("../lib/errors");
54
56
  const analytics_1 = require("../lib/analytics");
55
57
  const bundle_1 = require("../lib/bundle");
58
+ const key_store_1 = require("../lib/key-store");
59
+ const batch_publish_1 = require("../lib/batch-publish");
56
60
  /**
57
61
  * Extract template placeholders from a prompt template.
58
62
  * Matches double-brace patterns like {{variable}}.
@@ -442,6 +446,133 @@ async function checkDependencies(config, dependencies, publishingOrgSlug, worksp
442
446
  }
443
447
  }));
444
448
  }
449
+ /**
450
+ * Batch publish all agents found in subdirectories, in dependency order.
451
+ * Discovers orchagent.json/SKILL.md in immediate subdirectories,
452
+ * topologically sorts by manifest dependencies, and publishes leaf-first.
453
+ */
454
+ async function batchPublish(rootDir, options) {
455
+ process.stderr.write(`\nScanning for agents in ${rootDir}...\n`);
456
+ const agents = await (0, batch_publish_1.discoverAgents)(rootDir);
457
+ if (agents.length === 0) {
458
+ throw new errors_1.CliError('No agents found. Expected subdirectories with orchagent.json or SKILL.md files.\n\n' +
459
+ 'Example monorepo layout:\n' +
460
+ ' my-project/\n' +
461
+ ' leaf-tool/\n' +
462
+ ' orchagent.json\n' +
463
+ ' orchestrator/\n' +
464
+ ' orchagent.json\n' +
465
+ ' prompt.md\n\n' +
466
+ 'Run `orch publish --all` from the parent directory.');
467
+ }
468
+ const result = (0, batch_publish_1.topoSort)(agents);
469
+ if (!result.ok) {
470
+ throw new errors_1.CliError(`Circular dependency detected between: ${result.cycle.join(' → ')}\n\n` +
471
+ 'Break the cycle by removing a dependency in one of these agents\' orchagent.json manifest.dependencies.');
472
+ }
473
+ const sorted = result.sorted;
474
+ // Resolve org for display (best-effort)
475
+ let orgSlug;
476
+ try {
477
+ const config = await (0, config_1.getResolvedConfig)({}, options.profile);
478
+ const configFile = await (0, config_1.loadConfig)();
479
+ let workspaceId;
480
+ if (configFile.workspace && !options.profile) {
481
+ const { workspaces } = await (0, api_1.request)(config, 'GET', '/workspaces');
482
+ const ws = workspaces.find(w => w.slug === configFile.workspace);
483
+ if (ws)
484
+ workspaceId = ws.id;
485
+ }
486
+ const org = await (0, api_1.getOrg)(config, workspaceId);
487
+ orgSlug = org.slug;
488
+ }
489
+ catch {
490
+ // Non-critical — just won't show org prefix
491
+ }
492
+ const plan = (0, batch_publish_1.formatPublishPlan)(sorted, orgSlug);
493
+ process.stderr.write(plan);
494
+ if (options.dryRun) {
495
+ process.stderr.write(chalk_1.default.cyan('DRY RUN — running orch publish --dry-run in each directory:\n\n'));
496
+ }
497
+ // Build the CLI args to forward (exclude --all)
498
+ const forwardArgs = [];
499
+ if (options.profile)
500
+ forwardArgs.push('--profile', options.profile);
501
+ if (options.dryRun)
502
+ forwardArgs.push('--dry-run');
503
+ if (options.skills)
504
+ forwardArgs.push('--skills', options.skills);
505
+ if (options.skillsLocked)
506
+ forwardArgs.push('--skills-locked');
507
+ if (options.docker)
508
+ forwardArgs.push('--docker');
509
+ if (options.localDownload)
510
+ forwardArgs.push('--local-download');
511
+ if (options.requiredSecrets === false)
512
+ forwardArgs.push('--no-required-secrets');
513
+ const results = [];
514
+ for (let i = 0; i < sorted.length; i++) {
515
+ const agent = sorted[i];
516
+ const label = `[${i + 1}/${sorted.length}]`;
517
+ process.stderr.write(`${chalk_1.default.bold(label)} Publishing ${chalk_1.default.cyan(agent.name)} from ${agent.dirName}/...\n`);
518
+ // Spawn orch publish in the agent's directory
519
+ const spawnResult = (0, child_process_1.spawnSync)(process.argv[0], [process.argv[1], 'publish', ...forwardArgs], {
520
+ cwd: agent.dir,
521
+ stdio: ['inherit', 'inherit', 'inherit'],
522
+ env: process.env,
523
+ timeout: 120_000, // 2 minute timeout per agent
524
+ });
525
+ if (spawnResult.status === 0) {
526
+ results.push({ name: agent.name, dir: agent.dirName, success: true });
527
+ }
528
+ else {
529
+ const errMsg = spawnResult.error?.message || `exit code ${spawnResult.status}`;
530
+ results.push({ name: agent.name, dir: agent.dirName, success: false, error: errMsg });
531
+ // Stop on first failure — downstream agents depend on this one
532
+ process.stderr.write(chalk_1.default.red(`\n✗ Failed to publish ${agent.name}. Stopping — downstream agents may depend on it.\n`));
533
+ break;
534
+ }
535
+ if (i < sorted.length - 1) {
536
+ process.stderr.write('\n'); // Visual separator between publishes
537
+ }
538
+ }
539
+ // Summary
540
+ const succeeded = results.filter(r => r.success);
541
+ const failed = results.filter(r => !r.success);
542
+ const skipped = sorted.length - results.length;
543
+ process.stderr.write('\n' + chalk_1.default.bold('─'.repeat(50)) + '\n');
544
+ process.stderr.write(chalk_1.default.bold(`Batch publish summary:\n\n`));
545
+ for (const r of results) {
546
+ if (r.success) {
547
+ process.stderr.write(` ${chalk_1.default.green('✔')} ${r.name}\n`);
548
+ }
549
+ else {
550
+ process.stderr.write(` ${chalk_1.default.red('✗')} ${r.name} — ${r.error}\n`);
551
+ }
552
+ }
553
+ if (skipped > 0) {
554
+ const skippedAgents = sorted.slice(results.length);
555
+ for (const a of skippedAgents) {
556
+ process.stderr.write(` ${chalk_1.default.yellow('○')} ${a.name} (skipped)\n`);
557
+ }
558
+ }
559
+ process.stderr.write(`\n ${succeeded.length} succeeded`);
560
+ if (failed.length > 0)
561
+ process.stderr.write(`, ${chalk_1.default.red(`${failed.length} failed`)}`);
562
+ if (skipped > 0)
563
+ process.stderr.write(`, ${chalk_1.default.yellow(`${skipped} skipped`)}`);
564
+ process.stderr.write('\n');
565
+ await (0, analytics_1.track)('cli_publish_all', {
566
+ total: sorted.length,
567
+ succeeded: succeeded.length,
568
+ failed: failed.length,
569
+ skipped,
570
+ dry_run: options.dryRun || false,
571
+ });
572
+ if (failed.length > 0) {
573
+ throw new errors_1.CliError(`Batch publish failed: ${failed[0].name}`, errors_1.ExitCodes.GENERAL_ERROR);
574
+ }
575
+ }
445
576
  function registerPublishCommand(program) {
446
577
  program
447
578
  .command('publish')
@@ -454,12 +585,18 @@ function registerPublishCommand(program) {
454
585
  .option('--docker', 'Include Dockerfile for custom environment (builds E2B template)')
455
586
  .option('--local-download', 'Allow users to download and run locally (default: server-only)')
456
587
  .option('--no-required-secrets', 'Skip required_secrets check for tool/agent types')
588
+ .option('--all', 'Publish all agents in subdirectories (dependency order)')
457
589
  .action(async (options) => {
590
+ const cwd = process.cwd();
591
+ // --all: batch publish all agents in subdirectories
592
+ if (options.all) {
593
+ await batchPublish(cwd, options);
594
+ return;
595
+ }
458
596
  const skillsFromFlag = options.skills
459
597
  ? options.skills.split(',').map(s => s.trim()).filter(Boolean)
460
598
  : undefined;
461
599
  const config = await (0, config_1.getResolvedConfig)({}, options.profile);
462
- const cwd = process.cwd();
463
600
  // Resolve workspace context — if `orch workspace use` was called, publish
464
601
  // to that workspace instead of the personal org (F-5)
465
602
  // Skip workspace resolution when using a named profile — the global
@@ -476,7 +613,7 @@ function registerPublishCommand(program) {
476
613
  // Warn when publishing to the public showcase workspace
477
614
  if (ws.slug === 'orchagent-public' && !options.dryRun) {
478
615
  process.stderr.write(chalk_1.default.red.bold('\n PUBLIC WORKSPACE\n') +
479
- chalk_1.default.red(` Everything published to "${ws.slug}" is publicly visible on orchagent.io/explore.\n\n`));
616
+ chalk_1.default.red(` Everything published to "${ws.slug}" is publicly visible on orchagent.io.\n\n`));
480
617
  const readline = await Promise.resolve().then(() => __importStar(require('readline/promises')));
481
618
  const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
482
619
  const answer = await rl.question(chalk_1.default.red(' Continue? (y/N): '));
@@ -610,6 +747,11 @@ function registerPublishCommand(program) {
610
747
  if (runMode === 'always_on' && executionEngine === 'direct_llm') {
611
748
  throw new errors_1.CliError('run_mode=always_on requires runtime.command or loop configuration');
612
749
  }
750
+ if (manifest.timeout_seconds !== undefined) {
751
+ if (!Number.isInteger(manifest.timeout_seconds) || manifest.timeout_seconds <= 0) {
752
+ throw new errors_1.CliError('timeout_seconds must be a positive integer');
753
+ }
754
+ }
613
755
  // Warn about deprecated prompt field
614
756
  if (manifest.prompt) {
615
757
  process.stderr.write(chalk_1.default.yellow('Warning: "prompt" field in orchagent.json is ignored. Use prompt.md file instead.\n'));
@@ -681,24 +823,6 @@ function registerPublishCommand(program) {
681
823
  // Validate managed-loop specific fields + normalize loop payload
682
824
  let loopConfig;
683
825
  if (executionEngine === 'managed_loop') {
684
- if (manifest.custom_tools) {
685
- const reservedNames = new Set(['bash', 'read_file', 'write_file', 'list_files', 'submit_result']);
686
- const seenNames = new Set();
687
- for (const tool of manifest.custom_tools) {
688
- if (!tool.name || !tool.command) {
689
- throw new errors_1.CliError(`Invalid custom_tool: each tool must have 'name' and 'command' fields.\n` +
690
- `Found: ${JSON.stringify(tool)}`);
691
- }
692
- if (reservedNames.has(tool.name)) {
693
- throw new errors_1.CliError(`Custom tool '${tool.name}' conflicts with a built-in tool name.\n` +
694
- `Reserved names: ${[...reservedNames].join(', ')}`);
695
- }
696
- if (seenNames.has(tool.name)) {
697
- throw new errors_1.CliError(`Duplicate custom tool name: '${tool.name}'`);
698
- }
699
- seenNames.add(tool.name);
700
- }
701
- }
702
826
  if (manifest.max_turns !== undefined) {
703
827
  if (typeof manifest.max_turns !== 'number' || manifest.max_turns < 1 || manifest.max_turns > 50) {
704
828
  throw new errors_1.CliError('max_turns must be a number between 1 and 50');
@@ -717,6 +841,27 @@ function registerPublishCommand(program) {
717
841
  providedLoop.max_turns = 25;
718
842
  }
719
843
  loopConfig = providedLoop;
844
+ // Validate custom_tools from the merged loopConfig (covers both top-level
845
+ // manifest.custom_tools and loop.custom_tools placements — BUG-15)
846
+ const mergedTools = Array.isArray(loopConfig.custom_tools) ? loopConfig.custom_tools : [];
847
+ if (mergedTools.length > 0) {
848
+ const reservedNames = new Set(['bash', 'read_file', 'write_file', 'list_files', 'submit_result']);
849
+ const seenNames = new Set();
850
+ for (const tool of mergedTools) {
851
+ if (!tool.name || !tool.command) {
852
+ throw new errors_1.CliError(`Invalid custom_tool: each tool must have 'name' and 'command' fields.\n` +
853
+ `Found: ${JSON.stringify(tool)}`);
854
+ }
855
+ if (reservedNames.has(tool.name)) {
856
+ throw new errors_1.CliError(`Custom tool '${tool.name}' conflicts with a built-in tool name.\n` +
857
+ `Reserved names: ${[...reservedNames].join(', ')}`);
858
+ }
859
+ if (seenNames.has(tool.name)) {
860
+ throw new errors_1.CliError(`Duplicate custom tool name: '${tool.name}'`);
861
+ }
862
+ seenNames.add(tool.name);
863
+ }
864
+ }
720
865
  if (!manifest.supported_providers) {
721
866
  manifest.supported_providers = ['anthropic'];
722
867
  }
@@ -865,7 +1010,7 @@ function registerPublishCommand(program) {
865
1010
  const schemaTypes = [inputSchema ? 'input' : null, outputSchema ? 'output' : null].filter(Boolean).join(' + ');
866
1011
  process.stderr.write(` ✓ schema.json found (${schemaTypes} schemas)\n`);
867
1012
  }
868
- const customToolCount = manifest.custom_tools?.length || Number(Array.isArray(loopConfig?.custom_tools) ? loopConfig.custom_tools.length : 0);
1013
+ const customToolCount = Array.isArray(loopConfig?.custom_tools) ? loopConfig.custom_tools.length : 0;
869
1014
  process.stderr.write(` ✓ Custom tools: ${customToolCount}\n`);
870
1015
  process.stderr.write(` ✓ Max turns: ${loopConfig?.max_turns || manifest.max_turns || 25}\n`);
871
1016
  }
@@ -912,6 +1057,71 @@ function registerPublishCommand(program) {
912
1057
  if (manifest.required_secrets?.length) {
913
1058
  process.stderr.write(` Secrets: ${manifest.required_secrets.join(', ')}\n`);
914
1059
  }
1060
+ if (manifest.environment) {
1061
+ const envParts = [];
1062
+ if (manifest.environment.python_version)
1063
+ envParts.push(`Python ${manifest.environment.python_version}`);
1064
+ if (manifest.environment.node_version)
1065
+ envParts.push(`Node ${manifest.environment.node_version}`);
1066
+ if (manifest.environment.pip_flags)
1067
+ envParts.push(`pip: ${manifest.environment.pip_flags}`);
1068
+ if (manifest.environment.npm_flags)
1069
+ envParts.push(`npm: ${manifest.environment.npm_flags}`);
1070
+ if (envParts.length) {
1071
+ process.stderr.write(` Environment: ${envParts.join(', ')}\n`);
1072
+ }
1073
+ }
1074
+ // Server-side validation (BUG-11: dry-run missed server-side checks)
1075
+ process.stderr.write(`\nServer validation...\n`);
1076
+ try {
1077
+ const validation = await (0, api_1.validateAgentPublish)(config, {
1078
+ name: manifest.name,
1079
+ type: canonicalType,
1080
+ run_mode: runMode,
1081
+ runtime: runtimeConfig,
1082
+ loop: loopConfig,
1083
+ callable,
1084
+ description: manifest.description,
1085
+ prompt,
1086
+ url: agentUrl,
1087
+ input_schema: inputSchema,
1088
+ output_schema: outputSchema,
1089
+ is_public: false,
1090
+ supported_providers: supportedProviders,
1091
+ default_models: manifest.default_models,
1092
+ timeout_seconds: manifest.timeout_seconds,
1093
+ run_command: manifest.run_command,
1094
+ sdk_compatible: sdkCompatible || undefined,
1095
+ manifest: manifest.manifest,
1096
+ required_secrets: manifest.required_secrets,
1097
+ default_skills: skillsFromFlag || manifest.default_skills,
1098
+ skills_locked: manifest.skills_locked || options.skillsLocked || undefined,
1099
+ allow_local_download: options.localDownload || false,
1100
+ environment: manifest.environment,
1101
+ }, workspaceId);
1102
+ if (validation.warnings?.length) {
1103
+ for (const warning of validation.warnings) {
1104
+ process.stderr.write(chalk_1.default.yellow(` ⚠ ${warning}\n`));
1105
+ }
1106
+ }
1107
+ if (!validation.valid) {
1108
+ for (const error of validation.errors) {
1109
+ process.stderr.write(chalk_1.default.red(` ✗ ${error}\n`));
1110
+ }
1111
+ process.stderr.write(chalk_1.default.red(`\nDry run failed: server-side validation found ${validation.errors.length} error(s)\n`));
1112
+ process.stderr.write('No changes made (dry run)\n');
1113
+ const err = new errors_1.CliError('Server-side validation failed', errors_1.ExitCodes.INVALID_INPUT);
1114
+ err.displayed = true;
1115
+ throw err;
1116
+ }
1117
+ process.stderr.write(` ✓ Server-side validation passed\n`);
1118
+ }
1119
+ catch (err) {
1120
+ if (err instanceof errors_1.CliError)
1121
+ throw err;
1122
+ // Network or auth errors — show warning but don't block dry-run
1123
+ process.stderr.write(chalk_1.default.yellow(` ⚠ Could not reach server for validation (offline?)\n`));
1124
+ }
915
1125
  process.stderr.write(`\nWould publish: ${preview.org_slug}/${manifest.name}@${preview.next_version}\n`);
916
1126
  if (shouldUploadBundle) {
917
1127
  const bundlePreview = await (0, bundle_1.previewBundle)(cwd, {
@@ -994,6 +1204,7 @@ function registerPublishCommand(program) {
994
1204
  is_public: false,
995
1205
  supported_providers: supportedProviders,
996
1206
  default_models: manifest.default_models,
1207
+ timeout_seconds: manifest.timeout_seconds,
997
1208
  // Local run fields for code runtime agents
998
1209
  source_url: manifest.source_url,
999
1210
  pip_package: manifest.pip_package,
@@ -1006,6 +1217,8 @@ function registerPublishCommand(program) {
1006
1217
  default_skills: skillsFromFlag || manifest.default_skills,
1007
1218
  skills_locked: manifest.skills_locked || options.skillsLocked || undefined,
1008
1219
  allow_local_download: options.localDownload || false,
1220
+ // Environment pinning
1221
+ environment: manifest.environment,
1009
1222
  }, workspaceId);
1010
1223
  }
1011
1224
  catch (err) {
@@ -1179,8 +1392,17 @@ function registerPublishCommand(program) {
1179
1392
  }
1180
1393
  }
1181
1394
  if (result.service_key) {
1182
- process.stdout.write(`\nService key (save this - shown only once):\n`);
1395
+ process.stdout.write(`\nService key:\n`);
1183
1396
  process.stdout.write(` ${result.service_key}\n`);
1397
+ try {
1398
+ const keyPrefix = result.service_key.substring(0, 12);
1399
+ const savedPath = await (0, key_store_1.saveServiceKey)(org.slug, manifest.name, assignedVersion, result.service_key, keyPrefix);
1400
+ process.stdout.write(` ${chalk_1.default.gray(`Saved to ${savedPath}`)}\n`);
1401
+ }
1402
+ catch {
1403
+ process.stdout.write(` ${chalk_1.default.yellow('Could not save key locally. Copy it now — it cannot be retrieved from the server.')}\n`);
1404
+ }
1405
+ process.stdout.write(` Retrieve later: ${chalk_1.default.cyan(`orch agent-keys list ${org.slug}/${manifest.name}`)}\n`);
1184
1406
  }
1185
1407
  // Show next-step CLI command based on run mode
1186
1408
  const runRef = `${org.slug}/${manifest.name}`;