@swarmify/agents-cli 1.5.13 → 1.5.15

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.
Files changed (66) hide show
  1. package/README.md +93 -213
  2. package/dist/index.js +1194 -173
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/agents.d.ts.map +1 -1
  5. package/dist/lib/agents.js +19 -2
  6. package/dist/lib/agents.js.map +1 -1
  7. package/dist/lib/artifact-actions.d.ts +22 -0
  8. package/dist/lib/artifact-actions.d.ts.map +1 -0
  9. package/dist/lib/artifact-actions.js +55 -0
  10. package/dist/lib/artifact-actions.js.map +1 -0
  11. package/dist/lib/commands.d.ts.map +1 -1
  12. package/dist/lib/commands.js +3 -5
  13. package/dist/lib/commands.js.map +1 -1
  14. package/dist/lib/daemon.d.ts +22 -0
  15. package/dist/lib/daemon.d.ts.map +1 -0
  16. package/dist/lib/daemon.js +303 -0
  17. package/dist/lib/daemon.js.map +1 -0
  18. package/dist/lib/drive-server.d.ts +9 -0
  19. package/dist/lib/drive-server.d.ts.map +1 -0
  20. package/dist/lib/drive-server.js +217 -0
  21. package/dist/lib/drive-server.js.map +1 -0
  22. package/dist/lib/drives.d.ts +34 -0
  23. package/dist/lib/drives.d.ts.map +1 -0
  24. package/dist/lib/drives.js +267 -0
  25. package/dist/lib/drives.js.map +1 -0
  26. package/dist/lib/instructions.d.ts +31 -0
  27. package/dist/lib/instructions.d.ts.map +1 -0
  28. package/dist/lib/instructions.js +161 -0
  29. package/dist/lib/instructions.js.map +1 -0
  30. package/dist/lib/jobs.d.ts +53 -0
  31. package/dist/lib/jobs.d.ts.map +1 -0
  32. package/dist/lib/jobs.js +242 -0
  33. package/dist/lib/jobs.js.map +1 -0
  34. package/dist/lib/runner.d.ts +12 -0
  35. package/dist/lib/runner.d.ts.map +1 -0
  36. package/dist/lib/runner.js +266 -0
  37. package/dist/lib/runner.js.map +1 -0
  38. package/dist/lib/sandbox.d.ts +10 -0
  39. package/dist/lib/sandbox.d.ts.map +1 -0
  40. package/dist/lib/sandbox.js +166 -0
  41. package/dist/lib/sandbox.js.map +1 -0
  42. package/dist/lib/scheduler.d.ts +18 -0
  43. package/dist/lib/scheduler.d.ts.map +1 -0
  44. package/dist/lib/scheduler.js +64 -0
  45. package/dist/lib/scheduler.js.map +1 -0
  46. package/dist/lib/shims.d.ts +32 -0
  47. package/dist/lib/shims.d.ts.map +1 -0
  48. package/dist/lib/shims.js +175 -0
  49. package/dist/lib/shims.js.map +1 -0
  50. package/dist/lib/state.d.ts +5 -0
  51. package/dist/lib/state.d.ts.map +1 -1
  52. package/dist/lib/state.js +35 -0
  53. package/dist/lib/state.js.map +1 -1
  54. package/dist/lib/template.d.ts +24 -0
  55. package/dist/lib/template.d.ts.map +1 -0
  56. package/dist/lib/template.js +57 -0
  57. package/dist/lib/template.js.map +1 -0
  58. package/dist/lib/types.d.ts +11 -0
  59. package/dist/lib/types.d.ts.map +1 -1
  60. package/dist/lib/types.js.map +1 -1
  61. package/dist/lib/versions.d.ts +79 -0
  62. package/dist/lib/versions.d.ts.map +1 -0
  63. package/dist/lib/versions.js +289 -0
  64. package/dist/lib/versions.js.map +1 -0
  65. package/package.json +8 -5
  66. package/scripts/postinstall.js +72 -0
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ function isPromptCancelled(err) {
16
16
  err.message.includes('force closed') ||
17
17
  err.message.includes('User force closed'));
18
18
  }
19
- import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, SKILLS_CAPABLE_AGENTS, HOOKS_CAPABLE_AGENTS, getAllCliStates, isCliInstalled, getCliVersion, isMcpRegistered, registerMcp, unregisterMcp, listInstalledMcpsWithScope, promoteMcpToUser, } from './lib/agents.js';
19
+ import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, SKILLS_CAPABLE_AGENTS, HOOKS_CAPABLE_AGENTS, getAllCliStates, getCliVersion, isMcpRegistered, registerMcp, unregisterMcp, listInstalledMcpsWithScope, promoteMcpToUser, } from './lib/agents.js';
20
20
  import { readManifest, writeManifest, createDefaultManifest, MANIFEST_FILENAME, } from './lib/manifest.js';
21
21
  import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
22
22
  import { SCOPE_PRIORITIES, DEFAULT_SYSTEM_REPO } from './lib/types.js';
@@ -24,8 +24,11 @@ import { cloneRepo, parseSource } from './lib/git.js';
24
24
  import { discoverCommands, resolveCommandSource, installCommand, uninstallCommand, listInstalledCommandsWithScope, promoteCommandToUser, commandExists, commandContentMatches, } from './lib/commands.js';
25
25
  import { discoverHooksFromRepo, installHooks, listInstalledHooksWithScope, promoteHookToUser, removeHook, hookExists, hookContentMatches, getSourceHookEntry, } from './lib/hooks.js';
26
26
  import { discoverSkillsFromRepo, installSkill, uninstallSkill, listInstalledSkillsWithScope, promoteSkillToUser, getSkillInfo, getSkillRules, skillExists, skillContentMatches, } from './lib/skills.js';
27
+ import { discoverInstructionsFromRepo, resolveInstructionsSource, installInstructions, uninstallInstructions, listInstalledInstructionsWithScope, promoteInstructionsToUser, instructionsExists, instructionsContentMatches, getInstructionsContent, } from './lib/instructions.js';
27
28
  import { DEFAULT_REGISTRIES } from './lib/types.js';
28
29
  import { search as searchRegistries, getRegistries, setRegistry, removeRegistry, resolvePackage, } from './lib/registry.js';
30
+ import { parseAgentSpec, installVersion, removeVersion, removeAllVersions, listInstalledVersions, getGlobalDefault, setGlobalDefault, isVersionInstalled, } from './lib/versions.js';
31
+ import { createShim, removeShim, shimExists, isShimsInPath, getPathSetupInstructions, getShimsDir, } from './lib/shims.js';
29
32
  const program = new Command();
30
33
  /**
31
34
  * Ensure at least one scope is configured.
@@ -179,6 +182,10 @@ program
179
182
  agent: AGENTS[agentId],
180
183
  mcps: listInstalledMcpsWithScope(agentId, cwd),
181
184
  }));
185
+ const instructionsData = agentsToShow.map((agentId) => ({
186
+ agent: AGENTS[agentId],
187
+ instructions: listInstalledInstructionsWithScope(agentId, cwd),
188
+ }));
182
189
  const scopes = filterAgentId ? [] : getScopesByPriority();
183
190
  spinner.stop();
184
191
  // Helper to format MCP with version
@@ -249,6 +256,23 @@ program
249
256
  }
250
257
  }
251
258
  }
259
+ console.log(chalk.bold('\nInstalled Instructions\n'));
260
+ for (const { agent, instructions } of instructionsData) {
261
+ const userInstr = instructions.find((i) => i.scope === 'user' && i.exists);
262
+ const projectInstr = instructions.find((i) => i.scope === 'project' && i.exists);
263
+ if (!userInstr && !projectInstr) {
264
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
265
+ }
266
+ else {
267
+ console.log(` ${chalk.bold(agent.name)}:`);
268
+ if (userInstr) {
269
+ console.log(` ${chalk.gray('User:')} ${chalk.cyan(agent.instructionsFile)}`);
270
+ }
271
+ if (projectInstr) {
272
+ console.log(` ${chalk.gray('Project:')} ${chalk.yellow(agent.instructionsFile)}`);
273
+ }
274
+ }
275
+ }
252
276
  // Only show scopes when not filtering by agent
253
277
  if (!filterAgentId) {
254
278
  if (scopes.length > 0) {
@@ -364,6 +388,9 @@ program
364
388
  const allCommands = discoverCommands(localPath);
365
389
  const allSkills = discoverSkillsFromRepo(localPath);
366
390
  const discoveredHooks = discoverHooksFromRepo(localPath);
391
+ const allInstructions = discoverInstructionsFromRepo(localPath);
392
+ const allDiscoveredJobs = discoverJobsFromRepo(localPath);
393
+ const allDiscoveredDrives = discoverDrivesFromRepo(localPath);
367
394
  // Determine which agents to sync
368
395
  const cliStates = await getAllCliStates();
369
396
  let selectedAgents;
@@ -506,12 +533,51 @@ program
506
533
  }
507
534
  }
508
535
  }
536
+ // Process instructions
537
+ for (const instr of allInstructions) {
538
+ if (!selectedAgents.includes(instr.agentId))
539
+ continue;
540
+ const hasExisting = instructionsExists(instr.agentId, 'user');
541
+ if (!hasExisting) {
542
+ newItems.push({ type: 'instructions', name: AGENTS[instr.agentId].instructionsFile, agents: [instr.agentId], isNew: true });
543
+ }
544
+ else if (instructionsContentMatches(instr.agentId, instr.sourcePath, 'user')) {
545
+ upToDateItems.push({ type: 'instructions', name: AGENTS[instr.agentId].instructionsFile, agents: [instr.agentId], isNew: false });
546
+ }
547
+ else {
548
+ existingItems.push({ type: 'instructions', name: AGENTS[instr.agentId].instructionsFile, agents: [instr.agentId], isNew: false });
549
+ }
550
+ }
551
+ // Process jobs
552
+ for (const discoveredJob of allDiscoveredJobs) {
553
+ if (!jobExists(discoveredJob.name)) {
554
+ newItems.push({ type: 'job', name: discoveredJob.name, agents: [], isNew: true });
555
+ }
556
+ else if (jobContentMatches(discoveredJob.name, discoveredJob.path)) {
557
+ upToDateItems.push({ type: 'job', name: discoveredJob.name, agents: [], isNew: false });
558
+ }
559
+ else {
560
+ existingItems.push({ type: 'job', name: discoveredJob.name, agents: [], isNew: false });
561
+ }
562
+ }
563
+ // Process drives
564
+ for (const discoveredDrive of allDiscoveredDrives) {
565
+ if (!driveExists(discoveredDrive.name)) {
566
+ newItems.push({ type: 'drive', name: discoveredDrive.name, agents: [], isNew: true });
567
+ }
568
+ else if (driveContentMatches(discoveredDrive.name, discoveredDrive.path)) {
569
+ upToDateItems.push({ type: 'drive', name: discoveredDrive.name, agents: [], isNew: false });
570
+ }
571
+ else {
572
+ existingItems.push({ type: 'drive', name: discoveredDrive.name, agents: [], isNew: false });
573
+ }
574
+ }
509
575
  // Display overview
510
576
  console.log(chalk.bold('\nOverview\n'));
511
577
  const formatAgentList = (agents) => agents.map((id) => AGENTS[id].name).join(', ');
512
578
  if (newItems.length > 0) {
513
579
  console.log(chalk.green(' NEW (will install):\n'));
514
- const byType = { command: [], skill: [], hook: [], mcp: [] };
580
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
515
581
  for (const item of newItems)
516
582
  byType[item.type].push(item);
517
583
  if (byType.command.length > 0) {
@@ -538,11 +604,29 @@ program
538
604
  console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
539
605
  }
540
606
  }
607
+ if (byType.instructions.length > 0) {
608
+ console.log(` Instructions:`);
609
+ for (const item of byType.instructions) {
610
+ console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
611
+ }
612
+ }
613
+ if (byType.job.length > 0) {
614
+ console.log(` Jobs:`);
615
+ for (const item of byType.job) {
616
+ console.log(` ${chalk.cyan(item.name)}`);
617
+ }
618
+ }
619
+ if (byType.drive.length > 0) {
620
+ console.log(` Drives:`);
621
+ for (const item of byType.drive) {
622
+ console.log(` ${chalk.cyan(item.name)}`);
623
+ }
624
+ }
541
625
  console.log();
542
626
  }
543
627
  if (upToDateItems.length > 0) {
544
628
  console.log(chalk.gray(' UP TO DATE (no changes):\n'));
545
- const byType = { command: [], skill: [], hook: [], mcp: [] };
629
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
546
630
  for (const item of upToDateItems)
547
631
  byType[item.type].push(item);
548
632
  if (byType.command.length > 0) {
@@ -563,11 +647,29 @@ program
563
647
  console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
564
648
  }
565
649
  }
650
+ if (byType.instructions.length > 0) {
651
+ console.log(` Instructions:`);
652
+ for (const item of byType.instructions) {
653
+ console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
654
+ }
655
+ }
656
+ if (byType.job.length > 0) {
657
+ console.log(` Jobs:`);
658
+ for (const item of byType.job) {
659
+ console.log(` ${chalk.dim(item.name)}`);
660
+ }
661
+ }
662
+ if (byType.drive.length > 0) {
663
+ console.log(` Drives:`);
664
+ for (const item of byType.drive) {
665
+ console.log(` ${chalk.dim(item.name)}`);
666
+ }
667
+ }
566
668
  console.log();
567
669
  }
568
670
  if (existingItems.length > 0) {
569
671
  console.log(chalk.yellow(' EXISTING (conflicts):\n'));
570
- const byType = { command: [], skill: [], hook: [], mcp: [] };
672
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
571
673
  for (const item of existingItems)
572
674
  byType[item.type].push(item);
573
675
  if (byType.command.length > 0) {
@@ -594,6 +696,24 @@ program
594
696
  console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
595
697
  }
596
698
  }
699
+ if (byType.instructions.length > 0) {
700
+ console.log(` Instructions:`);
701
+ for (const item of byType.instructions) {
702
+ console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
703
+ }
704
+ }
705
+ if (byType.job.length > 0) {
706
+ console.log(` Jobs:`);
707
+ for (const item of byType.job) {
708
+ console.log(` ${chalk.yellow(item.name)}`);
709
+ }
710
+ }
711
+ if (byType.drive.length > 0) {
712
+ console.log(` Drives:`);
713
+ for (const item of byType.drive) {
714
+ console.log(` ${chalk.yellow(item.name)}`);
715
+ }
716
+ }
597
717
  console.log();
598
718
  }
599
719
  if (newItems.length === 0 && existingItems.length === 0) {
@@ -666,8 +786,8 @@ program
666
786
  }
667
787
  // Install new items (no conflicts)
668
788
  console.log();
669
- let installed = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
670
- let skipped = { commands: 0, skills: 0, hooks: 0, mcps: 0 };
789
+ let installed = { commands: 0, skills: 0, hooks: 0, mcps: 0, instructions: 0, jobs: 0, drives: 0 };
790
+ let skipped = { commands: 0, skills: 0, hooks: 0, mcps: 0, instructions: 0, jobs: 0, drives: 0 };
671
791
  // Install commands
672
792
  const cmdSpinner = ora('Installing commands...').start();
673
793
  for (const item of [...newItems, ...existingItems].filter((i) => i.type === 'command')) {
@@ -679,8 +799,13 @@ program
679
799
  for (const agentId of item.agents) {
680
800
  const sourcePath = resolveCommandSource(localPath, item.name, agentId);
681
801
  if (sourcePath) {
682
- installCommand(sourcePath, agentId, item.name, method);
683
- installed.commands++;
802
+ const result = installCommand(sourcePath, agentId, item.name, method);
803
+ if (result.error) {
804
+ console.log(chalk.yellow(`\n Warning: ${item.name} (${AGENTS[agentId].name}): ${result.error}`));
805
+ }
806
+ else {
807
+ installed.commands++;
808
+ }
684
809
  }
685
810
  }
686
811
  }
@@ -759,6 +884,104 @@ program
759
884
  mcpSpinner.info('No MCP servers to register');
760
885
  }
761
886
  }
887
+ // Install instructions
888
+ const instructionItems = [...newItems, ...existingItems].filter((i) => i.type === 'instructions');
889
+ if (instructionItems.length > 0) {
890
+ const instrSpinner = ora('Installing instructions...').start();
891
+ for (const item of instructionItems) {
892
+ const decision = item.isNew ? 'overwrite' : decisions.get(`instructions:${item.name}`);
893
+ if (decision === 'skip') {
894
+ skipped.instructions++;
895
+ continue;
896
+ }
897
+ for (const agentId of item.agents) {
898
+ const sourcePath = resolveInstructionsSource(localPath, agentId);
899
+ if (sourcePath) {
900
+ const result = installInstructions(sourcePath, agentId, method);
901
+ if (result.error) {
902
+ console.log(chalk.yellow(`\n Warning: ${item.name} (${AGENTS[agentId].name}): ${result.error}`));
903
+ }
904
+ else {
905
+ installed.instructions++;
906
+ }
907
+ }
908
+ }
909
+ }
910
+ if (skipped.instructions > 0) {
911
+ instrSpinner.succeed(`Installed ${installed.instructions} instructions (skipped ${skipped.instructions})`);
912
+ }
913
+ else if (installed.instructions > 0) {
914
+ instrSpinner.succeed(`Installed ${installed.instructions} instructions`);
915
+ }
916
+ else {
917
+ instrSpinner.info('No instructions to install');
918
+ }
919
+ }
920
+ // Install jobs
921
+ const jobItems = [...newItems, ...existingItems].filter((i) => i.type === 'job');
922
+ if (jobItems.length > 0) {
923
+ const jobSpinner = ora('Installing jobs...').start();
924
+ for (const item of jobItems) {
925
+ const decision = item.isNew ? 'overwrite' : decisions.get(`job:${item.name}`);
926
+ if (decision === 'skip') {
927
+ skipped.jobs++;
928
+ continue;
929
+ }
930
+ const discovered = allDiscoveredJobs.find((j) => j.name === item.name);
931
+ if (discovered) {
932
+ const result = installJobFromSource(discovered.path, discovered.name);
933
+ if (result.success) {
934
+ installed.jobs++;
935
+ }
936
+ else {
937
+ console.log(chalk.yellow(`\n Warning: job ${item.name}: ${result.error}`));
938
+ }
939
+ }
940
+ }
941
+ if (skipped.jobs > 0) {
942
+ jobSpinner.succeed(`Installed ${installed.jobs} jobs (skipped ${skipped.jobs})`);
943
+ }
944
+ else if (installed.jobs > 0) {
945
+ jobSpinner.succeed(`Installed ${installed.jobs} jobs`);
946
+ }
947
+ else {
948
+ jobSpinner.info('No jobs to install');
949
+ }
950
+ if (installed.jobs > 0 && isDaemonRunning()) {
951
+ signalDaemonReload();
952
+ }
953
+ }
954
+ // Install drives
955
+ const driveItems = [...newItems, ...existingItems].filter((i) => i.type === 'drive');
956
+ if (driveItems.length > 0) {
957
+ const driveSpinner = ora('Installing drives...').start();
958
+ for (const item of driveItems) {
959
+ const decision = item.isNew ? 'overwrite' : decisions.get(`drive:${item.name}`);
960
+ if (decision === 'skip') {
961
+ skipped.drives++;
962
+ continue;
963
+ }
964
+ const discovered = allDiscoveredDrives.find((d) => d.name === item.name);
965
+ if (discovered) {
966
+ const result = installDriveFromSource(discovered.path, discovered.name);
967
+ if (result.success) {
968
+ installed.drives++;
969
+ }
970
+ else {
971
+ console.log(chalk.yellow(`\n Warning: drive ${item.name}: ${result.error}`));
972
+ }
973
+ }
974
+ }
975
+ if (skipped.drives > 0) {
976
+ driveSpinner.succeed(`Installed ${installed.drives} drives (skipped ${skipped.drives})`);
977
+ }
978
+ else if (installed.drives > 0) {
979
+ driveSpinner.succeed(`Installed ${installed.drives} drives`);
980
+ }
981
+ else {
982
+ driveSpinner.info('No drives to install');
983
+ }
984
+ }
762
985
  // Sync CLI versions (user scope only)
763
986
  if (isUserScope && !options.skipClis && manifest?.clis) {
764
987
  const cliSpinner = ora('Checking CLI versions...').start();
@@ -852,6 +1075,44 @@ program
852
1075
  exported++;
853
1076
  }
854
1077
  }
1078
+ // Export MCP servers from installed agents
1079
+ console.log();
1080
+ let mcpExported = 0;
1081
+ const mcpByName = new Map();
1082
+ for (const agentId of MCP_CAPABLE_AGENTS) {
1083
+ if (!cliStates[agentId]?.installed)
1084
+ continue;
1085
+ const mcps = listInstalledMcpsWithScope(agentId);
1086
+ for (const mcp of mcps) {
1087
+ if (mcp.scope !== 'user')
1088
+ continue; // Only export user-scoped MCPs
1089
+ const existing = mcpByName.get(mcp.name);
1090
+ if (existing) {
1091
+ if (!existing.agents.includes(agentId)) {
1092
+ existing.agents.push(agentId);
1093
+ }
1094
+ }
1095
+ else {
1096
+ mcpByName.set(mcp.name, {
1097
+ command: mcp.command || '',
1098
+ agents: [agentId],
1099
+ });
1100
+ }
1101
+ }
1102
+ }
1103
+ if (mcpByName.size > 0) {
1104
+ manifest.mcp = manifest.mcp || {};
1105
+ for (const [name, config] of mcpByName) {
1106
+ manifest.mcp[name] = {
1107
+ command: config.command,
1108
+ transport: 'stdio',
1109
+ agents: config.agents,
1110
+ scope: 'user',
1111
+ };
1112
+ console.log(` ${chalk.green('+')} MCP: ${name} (${config.agents.map(id => AGENTS[id].name).join(', ')})`);
1113
+ mcpExported++;
1114
+ }
1115
+ }
855
1116
  if (!options.exportOnly) {
856
1117
  writeManifest(localPath, manifest);
857
1118
  console.log(chalk.bold(`\nUpdated ${MANIFEST_FILENAME}`));
@@ -952,8 +1213,13 @@ commandsCmd
952
1213
  continue;
953
1214
  const sourcePath = resolveCommandSource(localPath, command.name, agentId);
954
1215
  if (sourcePath) {
955
- installCommand(sourcePath, agentId, command.name, 'symlink');
956
- console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1216
+ const result = installCommand(sourcePath, agentId, command.name, 'symlink');
1217
+ if (result.error) {
1218
+ console.log(` ${chalk.yellow('!')} ${AGENTS[agentId].name}: ${result.error}`);
1219
+ }
1220
+ else {
1221
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1222
+ }
957
1223
  }
958
1224
  }
959
1225
  }
@@ -1427,6 +1693,152 @@ skillsCmd
1427
1693
  }
1428
1694
  });
1429
1695
  // =============================================================================
1696
+ // INSTRUCTIONS COMMANDS
1697
+ // =============================================================================
1698
+ const instructionsCmd = program
1699
+ .command('instructions')
1700
+ .alias('instr')
1701
+ .description('Manage agent instructions (CLAUDE.md, GEMINI.md, etc.)');
1702
+ instructionsCmd
1703
+ .command('list')
1704
+ .description('List installed instructions files')
1705
+ .option('-a, --agent <agent>', 'Filter by agent')
1706
+ .action(async (options) => {
1707
+ const cwd = process.cwd();
1708
+ const agents = options.agent
1709
+ ? [resolveAgentName(options.agent)].filter(Boolean)
1710
+ : ALL_AGENT_IDS;
1711
+ console.log(chalk.bold('Installed Instructions\n'));
1712
+ for (const agentId of agents) {
1713
+ const agent = AGENTS[agentId];
1714
+ const installed = listInstalledInstructionsWithScope(agentId, cwd);
1715
+ const userInstr = installed.find((i) => i.scope === 'user');
1716
+ const projectInstr = installed.find((i) => i.scope === 'project');
1717
+ const userStatus = userInstr?.exists ? chalk.green(agent.instructionsFile) : chalk.gray('none');
1718
+ const projectStatus = projectInstr?.exists ? chalk.yellow(agent.instructionsFile) : chalk.gray('none');
1719
+ console.log(` ${chalk.bold(agent.name)}:`);
1720
+ console.log(` ${chalk.gray('User:')} ${userStatus}`);
1721
+ console.log(` ${chalk.gray('Project:')} ${projectStatus}`);
1722
+ console.log();
1723
+ }
1724
+ });
1725
+ instructionsCmd
1726
+ .command('view [agent]')
1727
+ .alias('show')
1728
+ .description('View instructions content for an agent')
1729
+ .option('-s, --scope <scope>', 'Scope: user or project', 'user')
1730
+ .action(async (agentArg, options) => {
1731
+ const cwd = process.cwd();
1732
+ let agentId;
1733
+ if (agentArg) {
1734
+ agentId = resolveAgentName(agentArg) || undefined;
1735
+ if (!agentId) {
1736
+ console.log(chalk.red(`Unknown agent: ${agentArg}`));
1737
+ process.exit(1);
1738
+ }
1739
+ }
1740
+ else {
1741
+ const choices = ALL_AGENT_IDS.filter((id) => instructionsExists(id, 'user', cwd) || instructionsExists(id, 'project', cwd));
1742
+ if (choices.length === 0) {
1743
+ console.log(chalk.yellow('No instructions files found.'));
1744
+ return;
1745
+ }
1746
+ agentId = await select({
1747
+ message: 'Select agent:',
1748
+ choices: choices.map((id) => ({ name: AGENTS[id].name, value: id })),
1749
+ });
1750
+ }
1751
+ const scope = (options?.scope || 'user');
1752
+ const content = getInstructionsContent(agentId, scope, cwd);
1753
+ if (!content) {
1754
+ console.log(chalk.yellow(`No ${scope} instructions found for ${AGENTS[agentId].name}`));
1755
+ return;
1756
+ }
1757
+ console.log(chalk.bold(`\n${AGENTS[agentId].name} Instructions (${scope}):\n`));
1758
+ console.log(content);
1759
+ });
1760
+ instructionsCmd
1761
+ .command('diff [agent]')
1762
+ .description('Show differences between local and repo instructions')
1763
+ .action(async (agentArg) => {
1764
+ const cwd = process.cwd();
1765
+ const meta = readState();
1766
+ const scopes = getScopesByPriority();
1767
+ if (scopes.length === 0) {
1768
+ console.log(chalk.yellow('No repo configured. Run: agents repo add <source>'));
1769
+ return;
1770
+ }
1771
+ const agents = agentArg
1772
+ ? [resolveAgentName(agentArg)].filter(Boolean)
1773
+ : ALL_AGENT_IDS;
1774
+ const diff = await import('diff');
1775
+ for (const { name: scopeName, config } of scopes) {
1776
+ const localPath = getRepoLocalPath(config.source);
1777
+ const repoInstructions = discoverInstructionsFromRepo(localPath);
1778
+ for (const agentId of agents) {
1779
+ const repoInstr = repoInstructions.find((i) => i.agentId === agentId);
1780
+ if (!repoInstr)
1781
+ continue;
1782
+ const installedContent = getInstructionsContent(agentId, 'user', cwd);
1783
+ if (!installedContent) {
1784
+ console.log(`${chalk.bold(AGENTS[agentId].name)}: ${chalk.green('NEW')} (not installed)`);
1785
+ continue;
1786
+ }
1787
+ const repoContent = fs.readFileSync(repoInstr.sourcePath, 'utf-8');
1788
+ if (installedContent.trim() === repoContent.trim()) {
1789
+ console.log(`${chalk.bold(AGENTS[agentId].name)}: ${chalk.gray('up to date')}`);
1790
+ continue;
1791
+ }
1792
+ console.log(`${chalk.bold(AGENTS[agentId].name)}:`);
1793
+ const changes = diff.diffLines(installedContent, repoContent);
1794
+ for (const change of changes) {
1795
+ if (change.added) {
1796
+ process.stdout.write(chalk.green(change.value));
1797
+ }
1798
+ else if (change.removed) {
1799
+ process.stdout.write(chalk.red(change.value));
1800
+ }
1801
+ }
1802
+ console.log();
1803
+ }
1804
+ }
1805
+ });
1806
+ instructionsCmd
1807
+ .command('push <agent>')
1808
+ .description('Save project-scoped instructions to user scope')
1809
+ .action((agentArg) => {
1810
+ const cwd = process.cwd();
1811
+ const agentId = resolveAgentName(agentArg);
1812
+ if (!agentId) {
1813
+ console.log(chalk.red(`Unknown agent: ${agentArg}`));
1814
+ process.exit(1);
1815
+ }
1816
+ const result = promoteInstructionsToUser(agentId, cwd);
1817
+ if (result.success) {
1818
+ console.log(chalk.green(`Pushed ${AGENTS[agentId].instructionsFile} to user scope`));
1819
+ }
1820
+ else {
1821
+ console.log(chalk.red(result.error || 'Failed to push instructions'));
1822
+ }
1823
+ });
1824
+ instructionsCmd
1825
+ .command('remove <agent>')
1826
+ .description('Remove user-scoped instructions for an agent')
1827
+ .action((agentArg) => {
1828
+ const agentId = resolveAgentName(agentArg);
1829
+ if (!agentId) {
1830
+ console.log(chalk.red(`Unknown agent: ${agentArg}`));
1831
+ process.exit(1);
1832
+ }
1833
+ const result = uninstallInstructions(agentId);
1834
+ if (result) {
1835
+ console.log(chalk.green(`Removed ${AGENTS[agentId].instructionsFile}`));
1836
+ }
1837
+ else {
1838
+ console.log(chalk.yellow(`No instructions file found for ${AGENTS[agentId].name}`));
1839
+ }
1840
+ });
1841
+ // =============================================================================
1430
1842
  // MCP COMMANDS
1431
1843
  // =============================================================================
1432
1844
  const mcpCmd = program
@@ -1641,207 +2053,313 @@ mcpCmd
1641
2053
  }
1642
2054
  });
1643
2055
  // =============================================================================
1644
- // CLI COMMANDS
2056
+ // VERSION MANAGEMENT COMMANDS (add, remove, use, list, upgrade)
1645
2057
  // =============================================================================
1646
- const cliCmd = program
1647
- .command('cli')
1648
- .description('Manage agent CLIs');
1649
- cliCmd
1650
- .command('list')
1651
- .description('List installed agent CLIs')
1652
- .action(async () => {
1653
- const spinner = ora('Checking installed CLIs...').start();
1654
- const states = await getAllCliStates();
1655
- spinner.stop();
1656
- console.log(chalk.bold('Agent CLIs\n'));
1657
- for (const agentId of ALL_AGENT_IDS) {
1658
- const agent = AGENTS[agentId];
1659
- const state = states[agentId];
1660
- if (state?.installed) {
1661
- console.log(` ${agent.name.padEnd(14)} ${chalk.green(state.version || 'installed')}`);
1662
- if (state.path) {
1663
- console.log(` ${''.padEnd(14)} ${chalk.gray(state.path)}`);
1664
- }
2058
+ program
2059
+ .command('add <specs...>')
2060
+ .description('Install agent CLI(s). Examples: agents add claude@1.5.0, agents add claude codex')
2061
+ .option('-p, --project', 'Pin version in project manifest (.agents/agents.yaml)')
2062
+ .action(async (specs, options) => {
2063
+ const isProject = options.project;
2064
+ for (const spec of specs) {
2065
+ const parsed = parseAgentSpec(spec);
2066
+ if (!parsed) {
2067
+ console.log(chalk.red(`Invalid agent: ${spec}`));
2068
+ console.log(chalk.gray(`Format: <agent>[@version]. Available: ${ALL_AGENT_IDS.join(', ')}`));
2069
+ continue;
1665
2070
  }
1666
- else {
1667
- console.log(` ${agent.name.padEnd(14)} ${chalk.gray('not installed')}`);
1668
- }
1669
- }
1670
- });
1671
- cliCmd
1672
- .command('add <agents...>')
1673
- .description('Install agent CLI(s)')
1674
- .option('-v, --version <version>', 'Version to install', 'latest')
1675
- .option('--manifest-only', 'Only add to manifest, do not install')
1676
- .action(async (agents, options) => {
1677
- const validAgents = [];
1678
- for (const agent of agents) {
1679
- const agentId = agent.toLowerCase();
1680
- if (!AGENTS[agentId]) {
1681
- console.log(chalk.red(`Unknown agent: ${agent}`));
1682
- console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
1683
- return;
2071
+ const { agent, version } = parsed;
2072
+ const agentConfig = AGENTS[agent];
2073
+ if (!agentConfig.npmPackage) {
2074
+ console.log(chalk.yellow(`${agentConfig.name} has no npm package. Install manually.`));
2075
+ continue;
1684
2076
  }
1685
- validAgents.push(agentId);
1686
- }
1687
- const { exec } = await import('child_process');
1688
- const { promisify } = await import('util');
1689
- const execAsync = promisify(exec);
1690
- const version = options.version;
1691
- for (const agentId of validAgents) {
1692
- const agentConfig = AGENTS[agentId];
1693
- const pkg = agentConfig.npmPackage;
1694
- const installScript = agentConfig.installScript;
1695
- if (!options.manifestOnly) {
1696
- if (pkg) {
1697
- const spinner = ora(`Installing ${agentConfig.name}@${version}...`).start();
1698
- try {
1699
- await execAsync(`npm install -g ${pkg}@${version}`);
1700
- spinner.succeed(`Installed ${agentConfig.name}@${version}`);
1701
- }
1702
- catch (err) {
1703
- spinner.fail(`Failed to install ${agentConfig.name}`);
1704
- console.error(chalk.gray(err.message));
1705
- continue;
1706
- }
1707
- }
1708
- else if (installScript) {
1709
- const spinner = ora(`Installing ${agentConfig.name}...`).start();
1710
- try {
1711
- await execAsync(installScript, { shell: '/bin/bash' });
1712
- spinner.succeed(`Installed ${agentConfig.name}`);
2077
+ // Check if already installed
2078
+ if (isVersionInstalled(agent, version)) {
2079
+ console.log(chalk.gray(`${agentConfig.name}@${version} already installed`));
2080
+ }
2081
+ else {
2082
+ const spinner = ora(`Installing ${agentConfig.name}@${version}...`).start();
2083
+ const result = await installVersion(agent, version, (msg) => {
2084
+ spinner.text = msg;
2085
+ });
2086
+ if (result.success) {
2087
+ spinner.succeed(`Installed ${agentConfig.name}@${result.installedVersion}`);
2088
+ // Create shim if first install
2089
+ if (!shimExists(agent)) {
2090
+ createShim(agent);
2091
+ console.log(chalk.gray(` Created shim: ${getShimsDir()}/${agentConfig.cliCommand}`));
1713
2092
  }
1714
- catch (err) {
1715
- spinner.fail(`Failed to install ${agentConfig.name}`);
1716
- console.error(chalk.gray(err.message));
1717
- continue;
2093
+ // Check if shims in PATH
2094
+ if (!isShimsInPath()) {
2095
+ console.log();
2096
+ console.log(chalk.yellow('Shims directory not in PATH. Add it to use version switching:'));
2097
+ console.log(chalk.gray(getPathSetupInstructions()));
2098
+ console.log();
1718
2099
  }
1719
2100
  }
1720
2101
  else {
1721
- console.log(chalk.yellow(`${agentConfig.name} has no installer. Install manually.`));
2102
+ spinner.fail(`Failed to install ${agentConfig.name}@${version}`);
2103
+ console.error(chalk.gray(result.error || 'Unknown error'));
2104
+ continue;
1722
2105
  }
1723
2106
  }
2107
+ // Update project manifest if -p flag
2108
+ if (isProject) {
2109
+ const projectManifestDir = path.join(process.cwd(), '.agents');
2110
+ const projectManifestPath = path.join(projectManifestDir, 'agents.yaml');
2111
+ if (!fs.existsSync(projectManifestDir)) {
2112
+ fs.mkdirSync(projectManifestDir, { recursive: true });
2113
+ }
2114
+ const manifest = fs.existsSync(projectManifestPath)
2115
+ ? readManifest(process.cwd()) || createDefaultManifest()
2116
+ : createDefaultManifest();
2117
+ manifest.clis = manifest.clis || {};
2118
+ manifest.clis[agent] = {
2119
+ package: agentConfig.npmPackage,
2120
+ version: version === 'latest' ? (await getInstalledVersionForAgent(agent, version)) : version,
2121
+ };
2122
+ writeManifest(process.cwd(), manifest);
2123
+ console.log(chalk.green(` Pinned ${agentConfig.name}@${version} in .agents/agents.yaml`));
2124
+ }
1724
2125
  }
1725
- const source = await ensureSource();
1726
- const localPath = getRepoLocalPath(source);
1727
- const manifest = readManifest(localPath) || createDefaultManifest();
1728
- manifest.clis = manifest.clis || {};
1729
- for (const agentId of validAgents) {
1730
- const agentConfig = AGENTS[agentId];
1731
- manifest.clis[agentId] = {
1732
- package: agentConfig.npmPackage,
1733
- version: version,
1734
- };
1735
- }
1736
- writeManifest(localPath, manifest);
1737
- if (validAgents.length === 1) {
1738
- console.log(chalk.green(`Added ${AGENTS[validAgents[0]].name} to manifest`));
2126
+ });
2127
+ /**
2128
+ * Helper to get actual installed version for an agent.
2129
+ */
2130
+ async function getInstalledVersionForAgent(agent, requestedVersion) {
2131
+ const versions = listInstalledVersions(agent);
2132
+ if (versions.length > 0) {
2133
+ return versions[versions.length - 1];
1739
2134
  }
1740
- else {
1741
- console.log(chalk.green(`Added ${validAgents.length} agents to manifest`));
1742
- }
1743
- });
1744
- cliCmd
1745
- .command('remove <agents...>')
1746
- .description('Uninstall agent CLI(s)')
1747
- .option('--manifest-only', 'Only remove from manifest, do not uninstall')
1748
- .action(async (agents, options) => {
1749
- const validAgents = [];
1750
- for (const agent of agents) {
1751
- const agentId = agent.toLowerCase();
1752
- if (!AGENTS[agentId]) {
1753
- console.log(chalk.red(`Unknown agent: ${agent}`));
1754
- console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
1755
- return;
2135
+ return requestedVersion;
2136
+ }
2137
+ program
2138
+ .command('remove <specs...>')
2139
+ .description('Remove agent CLI version(s). Examples: agents remove claude@1.5.0, agents remove claude')
2140
+ .option('-p, --project', 'Also remove from project manifest')
2141
+ .action(async (specs, options) => {
2142
+ const isProject = options.project;
2143
+ for (const spec of specs) {
2144
+ const parsed = parseAgentSpec(spec);
2145
+ if (!parsed) {
2146
+ console.log(chalk.red(`Invalid agent: ${spec}`));
2147
+ console.log(chalk.gray(`Format: <agent>[@version]. Available: ${ALL_AGENT_IDS.join(', ')}`));
2148
+ continue;
1756
2149
  }
1757
- validAgents.push(agentId);
1758
- }
1759
- const { exec } = await import('child_process');
1760
- const { promisify } = await import('util');
1761
- const execAsync = promisify(exec);
1762
- for (const agentId of validAgents) {
1763
- const agentConfig = AGENTS[agentId];
1764
- const pkg = agentConfig.npmPackage;
1765
- if (!options.manifestOnly) {
1766
- if (!pkg) {
1767
- console.log(chalk.yellow(`${agentConfig.name} has no npm package.`));
2150
+ const { agent, version } = parsed;
2151
+ const agentConfig = AGENTS[agent];
2152
+ if (version === 'latest' || !spec.includes('@')) {
2153
+ // Remove all versions
2154
+ const versions = listInstalledVersions(agent);
2155
+ if (versions.length === 0) {
2156
+ console.log(chalk.gray(`No versions of ${agentConfig.name} installed`));
1768
2157
  }
1769
- else if (!(await isCliInstalled(agentId))) {
1770
- console.log(chalk.gray(`${agentConfig.name} is not installed`));
2158
+ else {
2159
+ const count = removeAllVersions(agent);
2160
+ removeShim(agent);
2161
+ console.log(chalk.green(`Removed ${count} version(s) of ${agentConfig.name}`));
2162
+ }
2163
+ }
2164
+ else {
2165
+ // Remove specific version
2166
+ if (!isVersionInstalled(agent, version)) {
2167
+ console.log(chalk.gray(`${agentConfig.name}@${version} not installed`));
1771
2168
  }
1772
2169
  else {
1773
- const spinner = ora(`Uninstalling ${agentConfig.name}...`).start();
1774
- try {
1775
- await execAsync(`npm uninstall -g ${pkg}`);
1776
- spinner.succeed(`Uninstalled ${agentConfig.name}`);
2170
+ removeVersion(agent, version);
2171
+ console.log(chalk.green(`Removed ${agentConfig.name}@${version}`));
2172
+ // Remove shim if no versions left
2173
+ const remaining = listInstalledVersions(agent);
2174
+ if (remaining.length === 0) {
2175
+ removeShim(agent);
1777
2176
  }
1778
- catch (err) {
1779
- spinner.fail(`Failed to uninstall ${agentConfig.name}`);
1780
- console.error(chalk.gray(err.message));
2177
+ }
2178
+ }
2179
+ // Update project manifest if -p flag
2180
+ if (isProject) {
2181
+ const projectManifestPath = path.join(process.cwd(), '.agents', 'agents.yaml');
2182
+ if (fs.existsSync(projectManifestPath)) {
2183
+ const manifest = readManifest(process.cwd());
2184
+ if (manifest?.clis?.[agent]) {
2185
+ delete manifest.clis[agent];
2186
+ writeManifest(process.cwd(), manifest);
2187
+ console.log(chalk.gray(` Removed from .agents/agents.yaml`));
1781
2188
  }
1782
2189
  }
1783
2190
  }
1784
2191
  }
1785
- const source = await ensureSource();
1786
- const localPath = getRepoLocalPath(source);
1787
- const manifest = readManifest(localPath);
1788
- let removed = 0;
1789
- for (const agentId of validAgents) {
1790
- if (manifest?.clis?.[agentId]) {
1791
- delete manifest.clis[agentId];
1792
- removed++;
2192
+ });
2193
+ program
2194
+ .command('use <spec>')
2195
+ .description('Set default agent version. Examples: agents use claude@1.5.0')
2196
+ .option('-p, --project', 'Set in project manifest instead of global default')
2197
+ .action(async (spec, options) => {
2198
+ const parsed = parseAgentSpec(spec);
2199
+ if (!parsed) {
2200
+ console.log(chalk.red(`Invalid agent: ${spec}`));
2201
+ console.log(chalk.gray(`Format: <agent>@<version>. Available: ${ALL_AGENT_IDS.join(', ')}`));
2202
+ return;
2203
+ }
2204
+ const { agent, version } = parsed;
2205
+ const agentConfig = AGENTS[agent];
2206
+ if (!spec.includes('@') || version === 'latest') {
2207
+ console.log(chalk.red('Please specify a version: agents use <agent>@<version>'));
2208
+ const versions = listInstalledVersions(agent);
2209
+ if (versions.length > 0) {
2210
+ console.log(chalk.gray(`Installed versions: ${versions.join(', ')}`));
1793
2211
  }
2212
+ return;
1794
2213
  }
1795
- if (removed > 0 && manifest) {
1796
- writeManifest(localPath, manifest);
1797
- if (removed === 1) {
1798
- console.log(chalk.green(`Removed ${AGENTS[validAgents[0]].name} from manifest`));
2214
+ if (!isVersionInstalled(agent, version)) {
2215
+ console.log(chalk.red(`${agentConfig.name}@${version} not installed`));
2216
+ console.log(chalk.gray(`Run: agents add ${agent}@${version}`));
2217
+ return;
2218
+ }
2219
+ if (options.project) {
2220
+ // Set in project manifest
2221
+ const projectManifestDir = path.join(process.cwd(), '.agents');
2222
+ const projectManifestPath = path.join(projectManifestDir, 'agents.yaml');
2223
+ if (!fs.existsSync(projectManifestDir)) {
2224
+ fs.mkdirSync(projectManifestDir, { recursive: true });
2225
+ }
2226
+ const manifest = fs.existsSync(projectManifestPath)
2227
+ ? readManifest(process.cwd()) || createDefaultManifest()
2228
+ : createDefaultManifest();
2229
+ manifest.clis = manifest.clis || {};
2230
+ manifest.clis[agent] = {
2231
+ package: agentConfig.npmPackage,
2232
+ version,
2233
+ };
2234
+ writeManifest(process.cwd(), manifest);
2235
+ console.log(chalk.green(`Set ${agentConfig.name}@${version} for this project`));
2236
+ }
2237
+ else {
2238
+ // Set global default
2239
+ setGlobalDefault(agent, version);
2240
+ console.log(chalk.green(`Set ${agentConfig.name}@${version} as global default`));
2241
+ }
2242
+ });
2243
+ program
2244
+ .command('list')
2245
+ .description('List installed agent CLI versions')
2246
+ .action(async () => {
2247
+ console.log(chalk.bold('Installed Agent CLIs\n'));
2248
+ let hasAny = false;
2249
+ for (const agentId of ALL_AGENT_IDS) {
2250
+ const agent = AGENTS[agentId];
2251
+ const versions = listInstalledVersions(agentId);
2252
+ const globalDefault = getGlobalDefault(agentId);
2253
+ if (versions.length > 0) {
2254
+ hasAny = true;
2255
+ console.log(` ${chalk.bold(agent.name)}`);
2256
+ for (const version of versions) {
2257
+ const isDefault = version === globalDefault;
2258
+ const marker = isDefault ? chalk.green(' (default)') : '';
2259
+ console.log(` ${version}${marker}`);
2260
+ }
2261
+ // Check for project override
2262
+ const projectVersion = getProjectVersionFromCwd(agentId);
2263
+ if (projectVersion && projectVersion !== globalDefault) {
2264
+ console.log(chalk.cyan(` -> ${projectVersion} (project)`));
2265
+ }
2266
+ console.log();
2267
+ }
2268
+ }
2269
+ if (!hasAny) {
2270
+ console.log(chalk.gray(' No agent CLIs installed.'));
2271
+ console.log(chalk.gray(' Run: agents add claude@latest'));
2272
+ console.log();
2273
+ }
2274
+ // Show shims path status
2275
+ if (hasAny) {
2276
+ const shimsDir = getShimsDir();
2277
+ if (isShimsInPath()) {
2278
+ console.log(chalk.gray(`Shims: ${shimsDir} (in PATH)`));
1799
2279
  }
1800
2280
  else {
1801
- console.log(chalk.green(`Removed ${removed} agents from manifest`));
2281
+ console.log(chalk.yellow(`Shims: ${shimsDir} (not in PATH)`));
2282
+ console.log(chalk.gray('Add to PATH for automatic version switching'));
1802
2283
  }
1803
2284
  }
1804
2285
  });
1805
- cliCmd
2286
+ /**
2287
+ * Helper to get project version from current working directory.
2288
+ */
2289
+ function getProjectVersionFromCwd(agent) {
2290
+ const manifestPath = path.join(process.cwd(), '.agents', 'agents.yaml');
2291
+ if (!fs.existsSync(manifestPath)) {
2292
+ return null;
2293
+ }
2294
+ try {
2295
+ const manifest = readManifest(process.cwd());
2296
+ return manifest?.clis?.[agent]?.version || null;
2297
+ }
2298
+ catch {
2299
+ return null;
2300
+ }
2301
+ }
2302
+ program
1806
2303
  .command('upgrade [agent]')
1807
- .description('Upgrade agent CLI(s) to version in manifest')
1808
- .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
1809
- .option('--latest', 'Upgrade to latest version (ignore manifest)')
2304
+ .description('Upgrade agent CLI(s) to latest version')
2305
+ .option('-p, --project', 'Upgrade to version in project manifest')
1810
2306
  .action(async (agent, options) => {
1811
- const scopeName = options.scope;
1812
- const scope = getScope(scopeName);
1813
- const localPath = scope ? getRepoLocalPath(scope.source) : null;
1814
- const manifest = localPath ? readManifest(localPath) : null;
1815
2307
  const agentsToUpgrade = agent
1816
2308
  ? [agent.toLowerCase()]
1817
- : ALL_AGENT_IDS.filter((id) => manifest?.clis?.[id] || options.latest);
2309
+ : ALL_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
1818
2310
  if (agentsToUpgrade.length === 0) {
1819
- console.log(chalk.yellow('No CLIs to upgrade. Add CLIs to manifest or use --latest'));
2311
+ console.log(chalk.yellow('No agent CLIs installed. Run: agents add <agent>@<version>'));
1820
2312
  return;
1821
2313
  }
1822
- const { exec } = await import('child_process');
1823
- const { promisify } = await import('util');
1824
- const execAsync = promisify(exec);
1825
2314
  for (const agentId of agentsToUpgrade) {
1826
2315
  const agentConfig = AGENTS[agentId];
1827
2316
  if (!agentConfig) {
1828
2317
  console.log(chalk.red(`Unknown agent: ${agentId}`));
1829
2318
  continue;
1830
2319
  }
1831
- const cliConfig = manifest?.clis?.[agentId];
1832
- const version = options.latest ? 'latest' : (cliConfig?.version || 'latest');
1833
- const pkg = cliConfig?.package || agentConfig.npmPackage;
1834
- const spinner = ora(`Upgrading ${agentConfig.name} to ${version}...`).start();
1835
- try {
1836
- await execAsync(`npm install -g ${pkg}@${version}`);
1837
- spinner.succeed(`${agentConfig.name} upgraded to ${version}`);
2320
+ // Determine target version
2321
+ let targetVersion = 'latest';
2322
+ if (options.project) {
2323
+ const projectVersion = getProjectVersionFromCwd(agentId);
2324
+ if (projectVersion) {
2325
+ targetVersion = projectVersion;
2326
+ }
1838
2327
  }
1839
- catch (err) {
2328
+ const spinner = ora(`Upgrading ${agentConfig.name} to ${targetVersion}...`).start();
2329
+ const result = await installVersion(agentId, targetVersion, (msg) => {
2330
+ spinner.text = msg;
2331
+ });
2332
+ if (result.success) {
2333
+ spinner.succeed(`Upgraded ${agentConfig.name} to ${result.installedVersion}`);
2334
+ // Update global default to new version
2335
+ setGlobalDefault(agentId, result.installedVersion);
2336
+ }
2337
+ else {
1840
2338
  spinner.fail(`Failed to upgrade ${agentConfig.name}`);
1841
- console.error(chalk.gray(err.message));
2339
+ console.error(chalk.gray(result.error || 'Unknown error'));
1842
2340
  }
1843
2341
  }
1844
2342
  });
2343
+ // Legacy alias for backwards compatibility
2344
+ program
2345
+ .command('cli')
2346
+ .description('(deprecated) Use: agents add, agents remove, agents list')
2347
+ .action(() => {
2348
+ console.log(chalk.yellow('The "cli" subcommand is deprecated.'));
2349
+ console.log();
2350
+ console.log('New commands:');
2351
+ console.log(' agents add <agent>@<version> Install agent CLI');
2352
+ console.log(' agents remove <agent>[@version] Remove agent CLI');
2353
+ console.log(' agents use <agent>@<version> Set default version');
2354
+ console.log(' agents list List installed versions');
2355
+ console.log(' agents upgrade [agent] Upgrade to latest');
2356
+ console.log();
2357
+ console.log('Examples:');
2358
+ console.log(' agents add claude@1.5.0');
2359
+ console.log(' agents add claude@1.5.0 -p Pin to project');
2360
+ console.log(' agents use claude@1.4.0');
2361
+ console.log(' agents remove claude@1.4.0');
2362
+ });
1845
2363
  // =============================================================================
1846
2364
  // REPO COMMANDS
1847
2365
  // =============================================================================
@@ -2189,11 +2707,11 @@ program
2189
2707
  }
2190
2708
  });
2191
2709
  // =============================================================================
2192
- // ADD COMMAND (unified package installation)
2710
+ // INSTALL COMMAND (unified package installation)
2193
2711
  // =============================================================================
2194
2712
  program
2195
- .command('add <identifier>')
2196
- .description('Add a package (mcp:name, skill:user/repo, or gh:user/repo)')
2713
+ .command('install <identifier>')
2714
+ .description('Install a package (mcp:name, skill:user/repo, or gh:user/repo)')
2197
2715
  .option('-a, --agents <list>', 'Comma-separated agents to install to')
2198
2716
  .action(async (identifier, options) => {
2199
2717
  const spinner = ora('Resolving package...').start();
@@ -2300,18 +2818,29 @@ program
2300
2818
  if (hasCommands) {
2301
2819
  console.log(chalk.bold('\nInstalling commands...'));
2302
2820
  let installed = 0;
2821
+ let failed = 0;
2303
2822
  for (const command of commands) {
2304
2823
  for (const agentId of agents) {
2305
2824
  if (!gitCliStates[agentId]?.installed && agentId !== 'cursor')
2306
2825
  continue;
2307
2826
  const sourcePath = resolveCommandSource(localPath, command.name, agentId);
2308
2827
  if (sourcePath) {
2309
- installCommand(sourcePath, agentId, command.name, 'symlink');
2310
- installed++;
2828
+ const result = installCommand(sourcePath, agentId, command.name, 'symlink');
2829
+ if (result.error) {
2830
+ failed++;
2831
+ }
2832
+ else {
2833
+ installed++;
2834
+ }
2311
2835
  }
2312
2836
  }
2313
2837
  }
2314
- console.log(` Installed ${installed} command instances`);
2838
+ if (failed > 0) {
2839
+ console.log(` Installed ${installed} command instances (${failed} failed)`);
2840
+ }
2841
+ else {
2842
+ console.log(` Installed ${installed} command instances`);
2843
+ }
2315
2844
  }
2316
2845
  // Install skills
2317
2846
  if (hasSkills) {
@@ -2344,7 +2873,7 @@ program
2344
2873
  });
2345
2874
  // Self-upgrade command
2346
2875
  program
2347
- .command('upgrade')
2876
+ .command('self-upgrade')
2348
2877
  .description('Upgrade agents-cli to the latest version')
2349
2878
  .action(async () => {
2350
2879
  const spinner = ora('Checking for updates...').start();
@@ -2406,6 +2935,498 @@ program
2406
2935
  process.exit(1);
2407
2936
  }
2408
2937
  });
2938
+ // =============================================================================
2939
+ // DAEMON COMMANDS
2940
+ // =============================================================================
2941
+ import { startDaemon, stopDaemon, isDaemonRunning, readDaemonPid, readDaemonLog, runDaemon, signalDaemonReload, } from './lib/daemon.js';
2942
+ import { listJobs as listAllJobs, readJob, validateJob, writeJob, setJobEnabled, getLatestRun, getRunDir, discoverJobsFromRepo, jobExists, jobContentMatches, installJobFromSource, } from './lib/jobs.js';
2943
+ import { executeJob } from './lib/runner.js';
2944
+ import { JobScheduler } from './lib/scheduler.js';
2945
+ const daemonCmd = program.command('daemon').description('Manage the jobs daemon');
2946
+ daemonCmd
2947
+ .command('start')
2948
+ .description('Start the daemon')
2949
+ .action(() => {
2950
+ const result = startDaemon();
2951
+ if (result.method === 'already-running') {
2952
+ console.log(chalk.yellow(`Daemon already running (PID: ${result.pid})`));
2953
+ }
2954
+ else {
2955
+ console.log(chalk.green(`Daemon started (PID: ${result.pid}, method: ${result.method})`));
2956
+ }
2957
+ });
2958
+ daemonCmd
2959
+ .command('stop')
2960
+ .description('Stop the daemon')
2961
+ .action(() => {
2962
+ if (!isDaemonRunning()) {
2963
+ console.log(chalk.yellow('Daemon is not running'));
2964
+ return;
2965
+ }
2966
+ stopDaemon();
2967
+ console.log(chalk.green('Daemon stopped'));
2968
+ });
2969
+ daemonCmd
2970
+ .command('status')
2971
+ .description('Show daemon status')
2972
+ .action(() => {
2973
+ const running = isDaemonRunning();
2974
+ const pid = readDaemonPid();
2975
+ console.log(chalk.bold('Daemon Status\n'));
2976
+ console.log(` Status: ${running ? chalk.green('running') : chalk.gray('stopped')}`);
2977
+ if (pid)
2978
+ console.log(` PID: ${pid}`);
2979
+ const jobs = listAllJobs();
2980
+ const enabled = jobs.filter((j) => j.enabled);
2981
+ console.log(` Jobs: ${enabled.length} enabled / ${jobs.length} total`);
2982
+ if (running && enabled.length > 0) {
2983
+ const scheduler = new JobScheduler(async () => { });
2984
+ scheduler.loadAll();
2985
+ const scheduled = scheduler.listScheduled();
2986
+ console.log(chalk.bold('\n Scheduled Jobs\n'));
2987
+ for (const job of scheduled) {
2988
+ const next = job.nextRun ? job.nextRun.toLocaleString() : 'unknown';
2989
+ console.log(` ${chalk.cyan(job.name.padEnd(24))} next: ${chalk.gray(next)}`);
2990
+ }
2991
+ scheduler.stopAll();
2992
+ }
2993
+ });
2994
+ daemonCmd
2995
+ .command('logs')
2996
+ .description('Show daemon logs')
2997
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
2998
+ .option('-f, --follow', 'Follow log output')
2999
+ .action(async (options) => {
3000
+ if (options.follow) {
3001
+ const { exec: execCb } = await import('child_process');
3002
+ const { getAgentsDir } = await import('./lib/state.js');
3003
+ const logPath = path.join(getAgentsDir(), 'daemon.log');
3004
+ const child = execCb(`tail -f "${logPath}"`);
3005
+ child.stdout?.pipe(process.stdout);
3006
+ child.stderr?.pipe(process.stderr);
3007
+ child.on('exit', () => process.exit(0));
3008
+ process.on('SIGINT', () => { child.kill(); process.exit(0); });
3009
+ return;
3010
+ }
3011
+ const lines = parseInt(options.lines, 10);
3012
+ const output = readDaemonLog(lines);
3013
+ if (output) {
3014
+ console.log(output);
3015
+ }
3016
+ else {
3017
+ console.log(chalk.gray('No daemon logs'));
3018
+ }
3019
+ });
3020
+ daemonCmd
3021
+ .command('_run')
3022
+ .description('Run daemon in foreground (internal)')
3023
+ .action(async () => {
3024
+ await runDaemon();
3025
+ });
3026
+ // =============================================================================
3027
+ // JOBS COMMANDS
3028
+ // =============================================================================
3029
+ const jobsCmd = program.command('jobs').description('Manage scheduled jobs');
3030
+ jobsCmd
3031
+ .command('list')
3032
+ .description('List all jobs')
3033
+ .action(() => {
3034
+ const jobs = listAllJobs();
3035
+ if (jobs.length === 0) {
3036
+ console.log(chalk.gray('No jobs configured'));
3037
+ console.log(chalk.gray(' Add a job: agents jobs add <path-to-job.yml>'));
3038
+ return;
3039
+ }
3040
+ const scheduler = new JobScheduler(async () => { });
3041
+ scheduler.loadAll();
3042
+ console.log(chalk.bold('Scheduled Jobs\n'));
3043
+ const header = ` ${'Name'.padEnd(24)} ${'Agent'.padEnd(10)} ${'Schedule'.padEnd(20)} ${'Enabled'.padEnd(10)} ${'Next Run'.padEnd(24)} ${'Last Status'}`;
3044
+ console.log(chalk.gray(header));
3045
+ console.log(chalk.gray(' ' + '-'.repeat(110)));
3046
+ for (const job of jobs) {
3047
+ const nextRun = scheduler.getNextRun(job.name);
3048
+ const nextStr = nextRun ? nextRun.toLocaleString() : '-';
3049
+ const latestRun = getLatestRun(job.name);
3050
+ const lastStatus = latestRun?.status || '-';
3051
+ const enabledStr = job.enabled ? chalk.green('yes') : chalk.gray('no');
3052
+ const statusColor = lastStatus === 'completed' ? chalk.green : lastStatus === 'failed' ? chalk.red : lastStatus === 'timeout' ? chalk.yellow : chalk.gray;
3053
+ console.log(` ${chalk.cyan(job.name.padEnd(24))} ${job.agent.padEnd(10)} ${job.schedule.padEnd(20)} ${enabledStr.padEnd(10 + 10)} ${chalk.gray(nextStr.padEnd(24))} ${statusColor(lastStatus)}`);
3054
+ }
3055
+ scheduler.stopAll();
3056
+ console.log();
3057
+ });
3058
+ jobsCmd
3059
+ .command('add <path>')
3060
+ .description('Add a job from a YAML file')
3061
+ .action(async (filePath) => {
3062
+ const resolved = path.resolve(filePath);
3063
+ if (!fs.existsSync(resolved)) {
3064
+ console.log(chalk.red(`File not found: ${resolved}`));
3065
+ process.exit(1);
3066
+ }
3067
+ const content = fs.readFileSync(resolved, 'utf-8');
3068
+ let parsed;
3069
+ try {
3070
+ const yamlMod = await import('yaml');
3071
+ parsed = yamlMod.parse(content);
3072
+ }
3073
+ catch (err) {
3074
+ console.log(chalk.red(`Invalid YAML: ${err.message}`));
3075
+ process.exit(1);
3076
+ }
3077
+ const name = parsed.name || path.basename(resolved).replace(/\.ya?ml$/, '');
3078
+ parsed.name = name;
3079
+ const errors = validateJob(parsed);
3080
+ if (errors.length > 0) {
3081
+ console.log(chalk.red('Validation errors:'));
3082
+ for (const err of errors) {
3083
+ console.log(chalk.red(` - ${err}`));
3084
+ }
3085
+ process.exit(1);
3086
+ }
3087
+ const config = {
3088
+ mode: 'plan',
3089
+ effort: 'default',
3090
+ timeout: '30m',
3091
+ enabled: true,
3092
+ ...parsed,
3093
+ };
3094
+ writeJob(config);
3095
+ console.log(chalk.green(`Job '${name}' added`));
3096
+ if (isDaemonRunning()) {
3097
+ signalDaemonReload();
3098
+ console.log(chalk.gray('Daemon reloaded'));
3099
+ }
3100
+ });
3101
+ jobsCmd
3102
+ .command('run <name>')
3103
+ .description('Run a job immediately in the foreground')
3104
+ .action(async (name) => {
3105
+ const job = readJob(name);
3106
+ if (!job) {
3107
+ console.log(chalk.red(`Job '${name}' not found`));
3108
+ process.exit(1);
3109
+ }
3110
+ console.log(chalk.bold(`Running job '${name}' (agent: ${job.agent}, mode: ${job.mode})\n`));
3111
+ const spinner = ora('Executing...').start();
3112
+ try {
3113
+ const result = await executeJob(job);
3114
+ if (result.meta.status === 'completed') {
3115
+ spinner.succeed(`Job completed (exit code: ${result.meta.exitCode})`);
3116
+ }
3117
+ else if (result.meta.status === 'timeout') {
3118
+ spinner.warn(`Job timed out after ${job.timeout}`);
3119
+ }
3120
+ else {
3121
+ spinner.fail(`Job failed (exit code: ${result.meta.exitCode})`);
3122
+ }
3123
+ console.log(chalk.gray(` Run: ${result.meta.runId}`));
3124
+ console.log(chalk.gray(` Log: ${getRunDir(name, result.meta.runId)}/stdout.log`));
3125
+ if (result.reportPath) {
3126
+ console.log(chalk.bold('\nReport:\n'));
3127
+ console.log(fs.readFileSync(result.reportPath, 'utf-8'));
3128
+ }
3129
+ }
3130
+ catch (err) {
3131
+ spinner.fail('Execution failed');
3132
+ console.error(chalk.red(err.message));
3133
+ process.exit(1);
3134
+ }
3135
+ });
3136
+ jobsCmd
3137
+ .command('logs <name>')
3138
+ .description('Show stdout from the latest (or specific) run')
3139
+ .option('-r, --run <runId>', 'Specific run ID')
3140
+ .action((name, options) => {
3141
+ let runId = options.run;
3142
+ if (!runId) {
3143
+ const latest = getLatestRun(name);
3144
+ if (!latest) {
3145
+ console.log(chalk.yellow(`No runs found for job '${name}'`));
3146
+ return;
3147
+ }
3148
+ runId = latest.runId;
3149
+ }
3150
+ const logPath = path.join(getRunDir(name, runId), 'stdout.log');
3151
+ if (!fs.existsSync(logPath)) {
3152
+ console.log(chalk.yellow(`Log not found: ${logPath}`));
3153
+ return;
3154
+ }
3155
+ console.log(chalk.gray(`Run: ${runId}\n`));
3156
+ console.log(fs.readFileSync(logPath, 'utf-8'));
3157
+ });
3158
+ jobsCmd
3159
+ .command('report <name>')
3160
+ .description('Show report from the latest (or specific) run')
3161
+ .option('-r, --run <runId>', 'Specific run ID')
3162
+ .action((name, options) => {
3163
+ let runId = options.run;
3164
+ if (!runId) {
3165
+ const latest = getLatestRun(name);
3166
+ if (!latest) {
3167
+ console.log(chalk.yellow(`No runs found for job '${name}'`));
3168
+ return;
3169
+ }
3170
+ runId = latest.runId;
3171
+ }
3172
+ const reportPath = path.join(getRunDir(name, runId), 'report.md');
3173
+ if (!fs.existsSync(reportPath)) {
3174
+ console.log(chalk.yellow(`No report found for run ${runId}`));
3175
+ console.log(chalk.gray(` Reports are extracted from agent output on completion`));
3176
+ return;
3177
+ }
3178
+ console.log(chalk.gray(`Run: ${runId}\n`));
3179
+ console.log(fs.readFileSync(reportPath, 'utf-8'));
3180
+ });
3181
+ jobsCmd
3182
+ .command('enable <name>')
3183
+ .description('Enable a job')
3184
+ .action((name) => {
3185
+ try {
3186
+ setJobEnabled(name, true);
3187
+ console.log(chalk.green(`Job '${name}' enabled`));
3188
+ if (isDaemonRunning()) {
3189
+ signalDaemonReload();
3190
+ console.log(chalk.gray('Daemon reloaded'));
3191
+ }
3192
+ }
3193
+ catch (err) {
3194
+ console.log(chalk.red(err.message));
3195
+ process.exit(1);
3196
+ }
3197
+ });
3198
+ jobsCmd
3199
+ .command('disable <name>')
3200
+ .description('Disable a job')
3201
+ .action((name) => {
3202
+ try {
3203
+ setJobEnabled(name, false);
3204
+ console.log(chalk.green(`Job '${name}' disabled`));
3205
+ if (isDaemonRunning()) {
3206
+ signalDaemonReload();
3207
+ console.log(chalk.gray('Daemon reloaded'));
3208
+ }
3209
+ }
3210
+ catch (err) {
3211
+ console.log(chalk.red(err.message));
3212
+ process.exit(1);
3213
+ }
3214
+ });
3215
+ // =============================================================================
3216
+ // DRIVE COMMANDS
3217
+ // =============================================================================
3218
+ import { createDrive, readDrive, listDrives as listAllDrives, deleteDrive, updateDriveFrontmatter, driveExists, discoverDrivesFromRepo, installDriveFromSource, driveContentMatches, } from './lib/drives.js';
3219
+ import { runDriveServer } from './lib/drive-server.js';
3220
+ const driveCmd = program.command('drive').description('Manage context drives');
3221
+ driveCmd
3222
+ .command('create <name>')
3223
+ .description('Create a new empty drive')
3224
+ .option('-d, --description <desc>', 'Drive description')
3225
+ .option('-p, --project <path>', 'Link to a project directory')
3226
+ .action((name, options) => {
3227
+ try {
3228
+ const filePath = createDrive(name, options.description);
3229
+ if (options.project) {
3230
+ updateDriveFrontmatter(name, { project: path.resolve(options.project) });
3231
+ }
3232
+ console.log(chalk.green(`Drive '${name}' created`));
3233
+ console.log(chalk.gray(` ${filePath}`));
3234
+ }
3235
+ catch (err) {
3236
+ console.log(chalk.red(err.message));
3237
+ process.exit(1);
3238
+ }
3239
+ });
3240
+ driveCmd
3241
+ .command('list')
3242
+ .description('List all drives')
3243
+ .action(() => {
3244
+ const drives = listAllDrives();
3245
+ if (drives.length === 0) {
3246
+ console.log(chalk.gray('No drives configured'));
3247
+ console.log(chalk.gray(' Create a drive: agents drive create <name>'));
3248
+ return;
3249
+ }
3250
+ console.log(chalk.bold('Context Drives\n'));
3251
+ const header = ` ${'Name'.padEnd(24)} ${'Description'.padEnd(40)} ${'Project'}`;
3252
+ console.log(chalk.gray(header));
3253
+ console.log(chalk.gray(' ' + '-'.repeat(90)));
3254
+ for (const drive of drives) {
3255
+ const desc = (drive.description || '-').slice(0, 38);
3256
+ const proj = drive.project || '-';
3257
+ console.log(` ${chalk.cyan(drive.name.padEnd(24))} ${desc.padEnd(40)} ${chalk.gray(proj)}`);
3258
+ }
3259
+ console.log();
3260
+ });
3261
+ driveCmd
3262
+ .command('info <name>')
3263
+ .description('Show drive metadata and content preview')
3264
+ .action((name) => {
3265
+ const drive = readDrive(name);
3266
+ if (!drive) {
3267
+ console.log(chalk.red(`Drive '${name}' not found`));
3268
+ process.exit(1);
3269
+ }
3270
+ console.log(chalk.bold(`Drive: ${drive.frontmatter.name}\n`));
3271
+ if (drive.frontmatter.description) {
3272
+ console.log(chalk.gray(` Description: ${drive.frontmatter.description}`));
3273
+ }
3274
+ if (drive.frontmatter.project) {
3275
+ console.log(chalk.gray(` Project: ${drive.frontmatter.project}`));
3276
+ }
3277
+ if (drive.frontmatter.repo) {
3278
+ console.log(chalk.gray(` Repo: ${drive.frontmatter.repo}`));
3279
+ }
3280
+ if (drive.frontmatter.updated) {
3281
+ console.log(chalk.gray(` Updated: ${drive.frontmatter.updated}`));
3282
+ }
3283
+ console.log(chalk.gray(` Path: ${drive.path}`));
3284
+ const content = drive.content.trim();
3285
+ if (content) {
3286
+ console.log(chalk.bold('\nContent:\n'));
3287
+ const lines = content.split('\n');
3288
+ const preview = lines.slice(0, 30);
3289
+ for (const line of preview) {
3290
+ console.log(` ${line}`);
3291
+ }
3292
+ if (lines.length > 30) {
3293
+ console.log(chalk.gray(`\n ... ${lines.length - 30} more lines`));
3294
+ }
3295
+ }
3296
+ console.log();
3297
+ });
3298
+ driveCmd
3299
+ .command('edit <name>')
3300
+ .description('Open drive in $EDITOR')
3301
+ .action((name) => {
3302
+ const drive = readDrive(name);
3303
+ if (!drive) {
3304
+ console.log(chalk.red(`Drive '${name}' not found`));
3305
+ process.exit(1);
3306
+ }
3307
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
3308
+ const { execSync } = require('child_process');
3309
+ try {
3310
+ execSync(`${editor} "${drive.path}"`, { stdio: 'inherit' });
3311
+ }
3312
+ catch {
3313
+ console.log(chalk.red(`Failed to open editor: ${editor}`));
3314
+ process.exit(1);
3315
+ }
3316
+ });
3317
+ driveCmd
3318
+ .command('delete <name>')
3319
+ .description('Delete a drive')
3320
+ .action(async (name) => {
3321
+ if (!driveExists(name)) {
3322
+ console.log(chalk.red(`Drive '${name}' not found`));
3323
+ process.exit(1);
3324
+ }
3325
+ try {
3326
+ const answer = await confirm({ message: `Delete drive '${name}'?` });
3327
+ if (!answer)
3328
+ return;
3329
+ }
3330
+ catch (err) {
3331
+ if (isPromptCancelled(err))
3332
+ return;
3333
+ throw err;
3334
+ }
3335
+ deleteDrive(name);
3336
+ console.log(chalk.green(`Drive '${name}' deleted`));
3337
+ });
3338
+ driveCmd
3339
+ .command('link <name> <path>')
3340
+ .description('Link a drive to a project directory')
3341
+ .action((name, projectPath) => {
3342
+ if (!driveExists(name)) {
3343
+ console.log(chalk.red(`Drive '${name}' not found`));
3344
+ process.exit(1);
3345
+ }
3346
+ const resolved = path.resolve(projectPath);
3347
+ if (!fs.existsSync(resolved)) {
3348
+ console.log(chalk.red(`Directory not found: ${resolved}`));
3349
+ process.exit(1);
3350
+ }
3351
+ updateDriveFrontmatter(name, { project: resolved });
3352
+ console.log(chalk.green(`Drive '${name}' linked to ${resolved}`));
3353
+ });
3354
+ driveCmd
3355
+ .command('sync')
3356
+ .description('Sync drives with .agents repo')
3357
+ .action(async () => {
3358
+ const source = await ensureSource();
3359
+ const repoPath = getRepoLocalPath(source);
3360
+ const discovered = discoverDrivesFromRepo(repoPath);
3361
+ if (discovered.length === 0) {
3362
+ console.log(chalk.gray('No drives found in repo'));
3363
+ return;
3364
+ }
3365
+ let installed = 0;
3366
+ let skipped = 0;
3367
+ for (const d of discovered) {
3368
+ if (driveExists(d.name) && driveContentMatches(d.name, d.path)) {
3369
+ skipped++;
3370
+ continue;
3371
+ }
3372
+ const result = installDriveFromSource(d.path, d.name);
3373
+ if (result.success) {
3374
+ installed++;
3375
+ console.log(chalk.green(` + ${d.name}`));
3376
+ }
3377
+ else {
3378
+ console.log(chalk.red(` x ${d.name}: ${result.error}`));
3379
+ }
3380
+ }
3381
+ if (installed === 0 && skipped > 0) {
3382
+ console.log(chalk.gray(`All ${skipped} drives up to date`));
3383
+ }
3384
+ else if (installed > 0) {
3385
+ console.log(chalk.green(`\nSynced ${installed} drive(s)`));
3386
+ }
3387
+ });
3388
+ driveCmd
3389
+ .command('generate <name>')
3390
+ .description('Run a drive generation job now')
3391
+ .action(async (name) => {
3392
+ if (!driveExists(name)) {
3393
+ console.log(chalk.red(`Drive '${name}' not found`));
3394
+ process.exit(1);
3395
+ }
3396
+ const jobName = `update-drive-${name}`;
3397
+ const job = readJob(jobName);
3398
+ if (!job) {
3399
+ console.log(chalk.red(`No generation job found for drive '${name}'`));
3400
+ console.log(chalk.gray(` Expected job name: ${jobName}`));
3401
+ console.log(chalk.gray(` Create one at: ~/.agents/jobs/${jobName}.yml`));
3402
+ process.exit(1);
3403
+ }
3404
+ console.log(chalk.bold(`Generating drive '${name}' (job: ${jobName})\n`));
3405
+ const spinner = ora('Executing...').start();
3406
+ try {
3407
+ const result = await executeJob(job);
3408
+ if (result.meta.status === 'completed') {
3409
+ spinner.succeed(`Drive '${name}' updated`);
3410
+ }
3411
+ else if (result.meta.status === 'timeout') {
3412
+ spinner.warn(`Generation timed out after ${job.timeout}`);
3413
+ }
3414
+ else {
3415
+ spinner.fail(`Generation failed (exit code: ${result.meta.exitCode})`);
3416
+ }
3417
+ }
3418
+ catch (err) {
3419
+ spinner.fail('Generation failed');
3420
+ console.error(chalk.red(err.message));
3421
+ process.exit(1);
3422
+ }
3423
+ });
3424
+ driveCmd
3425
+ .command('serve')
3426
+ .description('Start the drive MCP server (stdio)')
3427
+ .action(async () => {
3428
+ await runDriveServer();
3429
+ });
2409
3430
  async function showWhatsNew(fromVersion, toVersion) {
2410
3431
  try {
2411
3432
  // Fetch changelog from npm package