@orchagent/cli 0.3.86 → 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
@@ -686,24 +823,6 @@ function registerPublishCommand(program) {
686
823
  // Validate managed-loop specific fields + normalize loop payload
687
824
  let loopConfig;
688
825
  if (executionEngine === 'managed_loop') {
689
- if (manifest.custom_tools) {
690
- const reservedNames = new Set(['bash', 'read_file', 'write_file', 'list_files', 'submit_result']);
691
- const seenNames = new Set();
692
- for (const tool of manifest.custom_tools) {
693
- if (!tool.name || !tool.command) {
694
- throw new errors_1.CliError(`Invalid custom_tool: each tool must have 'name' and 'command' fields.\n` +
695
- `Found: ${JSON.stringify(tool)}`);
696
- }
697
- if (reservedNames.has(tool.name)) {
698
- throw new errors_1.CliError(`Custom tool '${tool.name}' conflicts with a built-in tool name.\n` +
699
- `Reserved names: ${[...reservedNames].join(', ')}`);
700
- }
701
- if (seenNames.has(tool.name)) {
702
- throw new errors_1.CliError(`Duplicate custom tool name: '${tool.name}'`);
703
- }
704
- seenNames.add(tool.name);
705
- }
706
- }
707
826
  if (manifest.max_turns !== undefined) {
708
827
  if (typeof manifest.max_turns !== 'number' || manifest.max_turns < 1 || manifest.max_turns > 50) {
709
828
  throw new errors_1.CliError('max_turns must be a number between 1 and 50');
@@ -722,6 +841,27 @@ function registerPublishCommand(program) {
722
841
  providedLoop.max_turns = 25;
723
842
  }
724
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
+ }
725
865
  if (!manifest.supported_providers) {
726
866
  manifest.supported_providers = ['anthropic'];
727
867
  }
@@ -870,7 +1010,7 @@ function registerPublishCommand(program) {
870
1010
  const schemaTypes = [inputSchema ? 'input' : null, outputSchema ? 'output' : null].filter(Boolean).join(' + ');
871
1011
  process.stderr.write(` ✓ schema.json found (${schemaTypes} schemas)\n`);
872
1012
  }
873
- 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;
874
1014
  process.stderr.write(` ✓ Custom tools: ${customToolCount}\n`);
875
1015
  process.stderr.write(` ✓ Max turns: ${loopConfig?.max_turns || manifest.max_turns || 25}\n`);
876
1016
  }
@@ -917,6 +1057,71 @@ function registerPublishCommand(program) {
917
1057
  if (manifest.required_secrets?.length) {
918
1058
  process.stderr.write(` Secrets: ${manifest.required_secrets.join(', ')}\n`);
919
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
+ }
920
1125
  process.stderr.write(`\nWould publish: ${preview.org_slug}/${manifest.name}@${preview.next_version}\n`);
921
1126
  if (shouldUploadBundle) {
922
1127
  const bundlePreview = await (0, bundle_1.previewBundle)(cwd, {
@@ -1012,6 +1217,8 @@ function registerPublishCommand(program) {
1012
1217
  default_skills: skillsFromFlag || manifest.default_skills,
1013
1218
  skills_locked: manifest.skills_locked || options.skillsLocked || undefined,
1014
1219
  allow_local_download: options.localDownload || false,
1220
+ // Environment pinning
1221
+ environment: manifest.environment,
1015
1222
  }, workspaceId);
1016
1223
  }
1017
1224
  catch (err) {
@@ -1185,8 +1392,17 @@ function registerPublishCommand(program) {
1185
1392
  }
1186
1393
  }
1187
1394
  if (result.service_key) {
1188
- process.stdout.write(`\nService key (save this - shown only once):\n`);
1395
+ process.stdout.write(`\nService key:\n`);
1189
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`);
1190
1406
  }
1191
1407
  // Show next-step CLI command based on run mode
1192
1408
  const runRef = `${org.slug}/${manifest.name}`;