@swarmify/agents-cli 1.5.14 → 1.5.16

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 (50) hide show
  1. package/README.md +86 -291
  2. package/dist/index.js +879 -197
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/daemon.d.ts +22 -0
  5. package/dist/lib/daemon.d.ts.map +1 -0
  6. package/dist/lib/daemon.js +303 -0
  7. package/dist/lib/daemon.js.map +1 -0
  8. package/dist/lib/drive-server.d.ts +9 -0
  9. package/dist/lib/drive-server.d.ts.map +1 -0
  10. package/dist/lib/drive-server.js +217 -0
  11. package/dist/lib/drive-server.js.map +1 -0
  12. package/dist/lib/drives.d.ts +34 -0
  13. package/dist/lib/drives.d.ts.map +1 -0
  14. package/dist/lib/drives.js +267 -0
  15. package/dist/lib/drives.js.map +1 -0
  16. package/dist/lib/jobs.d.ts +53 -0
  17. package/dist/lib/jobs.d.ts.map +1 -0
  18. package/dist/lib/jobs.js +242 -0
  19. package/dist/lib/jobs.js.map +1 -0
  20. package/dist/lib/manifest.js +1 -1
  21. package/dist/lib/manifest.js.map +1 -1
  22. package/dist/lib/runner.d.ts +12 -0
  23. package/dist/lib/runner.d.ts.map +1 -0
  24. package/dist/lib/runner.js +266 -0
  25. package/dist/lib/runner.js.map +1 -0
  26. package/dist/lib/sandbox.d.ts +10 -0
  27. package/dist/lib/sandbox.d.ts.map +1 -0
  28. package/dist/lib/sandbox.js +166 -0
  29. package/dist/lib/sandbox.js.map +1 -0
  30. package/dist/lib/scheduler.d.ts +18 -0
  31. package/dist/lib/scheduler.d.ts.map +1 -0
  32. package/dist/lib/scheduler.js +64 -0
  33. package/dist/lib/scheduler.js.map +1 -0
  34. package/dist/lib/shims.d.ts +32 -0
  35. package/dist/lib/shims.d.ts.map +1 -0
  36. package/dist/lib/shims.js +181 -0
  37. package/dist/lib/shims.js.map +1 -0
  38. package/dist/lib/state.d.ts +5 -0
  39. package/dist/lib/state.d.ts.map +1 -1
  40. package/dist/lib/state.js +35 -0
  41. package/dist/lib/state.js.map +1 -1
  42. package/dist/lib/types.d.ts +10 -0
  43. package/dist/lib/types.d.ts.map +1 -1
  44. package/dist/lib/types.js.map +1 -1
  45. package/dist/lib/versions.d.ts +84 -0
  46. package/dist/lib/versions.d.ts.map +1 -0
  47. package/dist/lib/versions.js +297 -0
  48. package/dist/lib/versions.js.map +1 -0
  49. package/package.json +8 -5
  50. package/scripts/postinstall.js +72 -0
package/dist/index.js CHANGED
@@ -16,9 +16,9 @@ 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
- import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
21
+ import { readState, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
22
22
  import { SCOPE_PRIORITIES, DEFAULT_SYSTEM_REPO } from './lib/types.js';
23
23
  import { cloneRepo, parseSource } from './lib/git.js';
24
24
  import { discoverCommands, resolveCommandSource, installCommand, uninstallCommand, listInstalledCommandsWithScope, promoteCommandToUser, commandExists, commandContentMatches, } from './lib/commands.js';
@@ -27,6 +27,8 @@ import { discoverSkillsFromRepo, installSkill, uninstallSkill, listInstalledSkil
27
27
  import { discoverInstructionsFromRepo, resolveInstructionsSource, installInstructions, uninstallInstructions, listInstalledInstructionsWithScope, promoteInstructionsToUser, instructionsExists, instructionsContentMatches, getInstructionsContent, } from './lib/instructions.js';
28
28
  import { DEFAULT_REGISTRIES } from './lib/types.js';
29
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';
30
32
  const program = new Command();
31
33
  /**
32
34
  * Ensure at least one scope is configured.
@@ -387,6 +389,8 @@ program
387
389
  const allSkills = discoverSkillsFromRepo(localPath);
388
390
  const discoveredHooks = discoverHooksFromRepo(localPath);
389
391
  const allInstructions = discoverInstructionsFromRepo(localPath);
392
+ const allDiscoveredJobs = discoverJobsFromRepo(localPath);
393
+ const allDiscoveredDrives = discoverDrivesFromRepo(localPath);
390
394
  // Determine which agents to sync
391
395
  const cliStates = await getAllCliStates();
392
396
  let selectedAgents;
@@ -396,7 +400,7 @@ program
396
400
  console.log(chalk.gray(`\nFiltering for ${AGENTS[agentFilter].name} only\n`));
397
401
  }
398
402
  else if (options.yes || options.force) {
399
- selectedAgents = (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']);
403
+ selectedAgents = (manifest?.defaults?.agents || ALL_AGENT_IDS);
400
404
  }
401
405
  else {
402
406
  const installedAgents = ALL_AGENT_IDS.filter((id) => cliStates[id]?.installed || id === 'cursor');
@@ -405,7 +409,7 @@ program
405
409
  choices: installedAgents.map((id) => ({
406
410
  name: AGENTS[id].name,
407
411
  value: id,
408
- checked: (manifest?.defaults?.agents || ['claude', 'codex', 'gemini']).includes(id),
412
+ checked: (manifest?.defaults?.agents || ALL_AGENT_IDS).includes(id),
409
413
  })),
410
414
  });
411
415
  }
@@ -544,12 +548,36 @@ program
544
548
  existingItems.push({ type: 'instructions', name: AGENTS[instr.agentId].instructionsFile, agents: [instr.agentId], isNew: false });
545
549
  }
546
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
+ }
547
575
  // Display overview
548
576
  console.log(chalk.bold('\nOverview\n'));
549
577
  const formatAgentList = (agents) => agents.map((id) => AGENTS[id].name).join(', ');
550
578
  if (newItems.length > 0) {
551
579
  console.log(chalk.green(' NEW (will install):\n'));
552
- const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [] };
580
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
553
581
  for (const item of newItems)
554
582
  byType[item.type].push(item);
555
583
  if (byType.command.length > 0) {
@@ -582,11 +610,23 @@ program
582
610
  console.log(` ${chalk.cyan(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
583
611
  }
584
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
+ }
585
625
  console.log();
586
626
  }
587
627
  if (upToDateItems.length > 0) {
588
628
  console.log(chalk.gray(' UP TO DATE (no changes):\n'));
589
- const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [] };
629
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
590
630
  for (const item of upToDateItems)
591
631
  byType[item.type].push(item);
592
632
  if (byType.command.length > 0) {
@@ -613,11 +653,23 @@ program
613
653
  console.log(` ${chalk.dim(item.name.padEnd(20))} ${chalk.dim(formatAgentList(item.agents))}`);
614
654
  }
615
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
+ }
616
668
  console.log();
617
669
  }
618
670
  if (existingItems.length > 0) {
619
671
  console.log(chalk.yellow(' EXISTING (conflicts):\n'));
620
- const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [] };
672
+ const byType = { command: [], skill: [], hook: [], mcp: [], instructions: [], job: [], drive: [] };
621
673
  for (const item of existingItems)
622
674
  byType[item.type].push(item);
623
675
  if (byType.command.length > 0) {
@@ -650,6 +702,18 @@ program
650
702
  console.log(` ${chalk.yellow(item.name.padEnd(20))} ${chalk.gray(formatAgentList(item.agents))}`);
651
703
  }
652
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
+ }
653
717
  console.log();
654
718
  }
655
719
  if (newItems.length === 0 && existingItems.length === 0) {
@@ -722,8 +786,8 @@ program
722
786
  }
723
787
  // Install new items (no conflicts)
724
788
  console.log();
725
- let installed = { commands: 0, skills: 0, hooks: 0, mcps: 0, instructions: 0 };
726
- let skipped = { commands: 0, skills: 0, hooks: 0, mcps: 0, instructions: 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 };
727
791
  // Install commands
728
792
  const cmdSpinner = ora('Installing commands...').start();
729
793
  for (const item of [...newItems, ...existingItems].filter((i) => i.type === 'command')) {
@@ -853,6 +917,71 @@ program
853
917
  instrSpinner.info('No instructions to install');
854
918
  }
855
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
+ }
856
985
  // Sync CLI versions (user scope only)
857
986
  if (isUserScope && !options.skipClis && manifest?.clis) {
858
987
  const cliSpinner = ora('Checking CLI versions...').start();
@@ -1075,7 +1204,7 @@ commandsCmd
1075
1204
  spinner.succeed(`Found ${commands.length} commands`);
1076
1205
  const agents = options.agents
1077
1206
  ? options.agents.split(',')
1078
- : ['claude', 'codex', 'gemini'];
1207
+ : ALL_AGENT_IDS;
1079
1208
  const cliStates = await getAllCliStates();
1080
1209
  for (const command of commands) {
1081
1210
  console.log(`\n ${chalk.cyan(command.name)}: ${command.description}`);
@@ -1404,7 +1533,7 @@ skillsCmd
1404
1533
  choices: SKILLS_CAPABLE_AGENTS.filter((id) => cliStates[id]?.installed || id === 'cursor').map((id) => ({
1405
1534
  name: AGENTS[id].name,
1406
1535
  value: id,
1407
- checked: ['claude', 'codex', 'gemini'].includes(id),
1536
+ checked: true,
1408
1537
  })),
1409
1538
  });
1410
1539
  if (agents.length === 0) {
@@ -1924,204 +2053,290 @@ mcpCmd
1924
2053
  }
1925
2054
  });
1926
2055
  // =============================================================================
1927
- // CLI COMMANDS
2056
+ // VERSION MANAGEMENT COMMANDS (add, remove, use, list, upgrade)
1928
2057
  // =============================================================================
1929
- const cliCmd = program
1930
- .command('cli')
1931
- .description('Manage agent CLIs');
1932
- cliCmd
1933
- .command('list')
1934
- .description('List installed agent CLIs')
1935
- .action(async () => {
1936
- const spinner = ora('Checking installed CLIs...').start();
1937
- const states = await getAllCliStates();
1938
- spinner.stop();
1939
- console.log(chalk.bold('Agent CLIs\n'));
1940
- for (const agentId of ALL_AGENT_IDS) {
1941
- const agent = AGENTS[agentId];
1942
- const state = states[agentId];
1943
- if (state?.installed) {
1944
- console.log(` ${agent.name.padEnd(14)} ${chalk.green(state.version || 'installed')}`);
1945
- if (state.path) {
1946
- console.log(` ${''.padEnd(14)} ${chalk.gray(state.path)}`);
1947
- }
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;
1948
2070
  }
1949
- else {
1950
- console.log(` ${agent.name.padEnd(14)} ${chalk.gray('not installed')}`);
1951
- }
1952
- }
1953
- });
1954
- cliCmd
1955
- .command('add <agents...>')
1956
- .description('Install agent CLI(s)')
1957
- .option('-v, --version <version>', 'Version to install', 'latest')
1958
- .option('--manifest-only', 'Only add to manifest, do not install')
1959
- .action(async (agents, options) => {
1960
- const validAgents = [];
1961
- for (const agent of agents) {
1962
- const agentId = agent.toLowerCase();
1963
- if (!AGENTS[agentId]) {
1964
- console.log(chalk.red(`Unknown agent: ${agent}`));
1965
- console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
1966
- 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;
1967
2076
  }
1968
- validAgents.push(agentId);
1969
- }
1970
- const { exec } = await import('child_process');
1971
- const { promisify } = await import('util');
1972
- const execAsync = promisify(exec);
1973
- const version = options.version;
1974
- for (const agentId of validAgents) {
1975
- const agentConfig = AGENTS[agentId];
1976
- const pkg = agentConfig.npmPackage;
1977
- const installScript = agentConfig.installScript;
1978
- if (!options.manifestOnly) {
1979
- if (pkg) {
1980
- const spinner = ora(`Installing ${agentConfig.name}@${version}...`).start();
1981
- try {
1982
- await execAsync(`npm install -g ${pkg}@${version}`);
1983
- spinner.succeed(`Installed ${agentConfig.name}@${version}`);
1984
- }
1985
- catch (err) {
1986
- spinner.fail(`Failed to install ${agentConfig.name}`);
1987
- console.error(chalk.gray(err.message));
1988
- continue;
1989
- }
1990
- }
1991
- else if (installScript) {
1992
- const spinner = ora(`Installing ${agentConfig.name}...`).start();
1993
- try {
1994
- await execAsync(installScript, { shell: '/bin/bash' });
1995
- 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}`));
1996
2092
  }
1997
- catch (err) {
1998
- spinner.fail(`Failed to install ${agentConfig.name}`);
1999
- console.error(chalk.gray(err.message));
2000
- 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();
2001
2099
  }
2002
2100
  }
2003
2101
  else {
2004
- 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;
2005
2105
  }
2006
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
+ }
2007
2125
  }
2008
- const source = await ensureSource();
2009
- const localPath = getRepoLocalPath(source);
2010
- const manifest = readManifest(localPath) || createDefaultManifest();
2011
- manifest.clis = manifest.clis || {};
2012
- for (const agentId of validAgents) {
2013
- const agentConfig = AGENTS[agentId];
2014
- manifest.clis[agentId] = {
2015
- package: agentConfig.npmPackage,
2016
- version: version,
2017
- };
2018
- }
2019
- writeManifest(localPath, manifest);
2020
- if (validAgents.length === 1) {
2021
- 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];
2022
2134
  }
2023
- else {
2024
- console.log(chalk.green(`Added ${validAgents.length} agents to manifest`));
2025
- }
2026
- });
2027
- cliCmd
2028
- .command('remove <agents...>')
2029
- .description('Uninstall agent CLI(s)')
2030
- .option('--manifest-only', 'Only remove from manifest, do not uninstall')
2031
- .action(async (agents, options) => {
2032
- const validAgents = [];
2033
- for (const agent of agents) {
2034
- const agentId = agent.toLowerCase();
2035
- if (!AGENTS[agentId]) {
2036
- console.log(chalk.red(`Unknown agent: ${agent}`));
2037
- console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
2038
- 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;
2039
2149
  }
2040
- validAgents.push(agentId);
2041
- }
2042
- const { exec } = await import('child_process');
2043
- const { promisify } = await import('util');
2044
- const execAsync = promisify(exec);
2045
- for (const agentId of validAgents) {
2046
- const agentConfig = AGENTS[agentId];
2047
- const pkg = agentConfig.npmPackage;
2048
- if (!options.manifestOnly) {
2049
- if (!pkg) {
2050
- 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`));
2051
2157
  }
2052
- else if (!(await isCliInstalled(agentId))) {
2053
- 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`));
2054
2168
  }
2055
2169
  else {
2056
- const spinner = ora(`Uninstalling ${agentConfig.name}...`).start();
2057
- try {
2058
- await execAsync(`npm uninstall -g ${pkg}`);
2059
- 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);
2060
2176
  }
2061
- catch (err) {
2062
- spinner.fail(`Failed to uninstall ${agentConfig.name}`);
2063
- 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`));
2064
2188
  }
2065
2189
  }
2066
2190
  }
2067
2191
  }
2068
- const source = await ensureSource();
2069
- const localPath = getRepoLocalPath(source);
2070
- const manifest = readManifest(localPath);
2071
- let removed = 0;
2072
- for (const agentId of validAgents) {
2073
- if (manifest?.clis?.[agentId]) {
2074
- delete manifest.clis[agentId];
2075
- 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(', ')}`));
2076
2211
  }
2212
+ return;
2077
2213
  }
2078
- if (removed > 0 && manifest) {
2079
- writeManifest(localPath, manifest);
2080
- if (removed === 1) {
2081
- 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)`));
2082
2279
  }
2083
2280
  else {
2084
- 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'));
2085
2283
  }
2086
2284
  }
2087
2285
  });
2088
- 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
2089
2303
  .command('upgrade [agent]')
2090
- .description('Upgrade agent CLI(s) to version in manifest')
2091
- .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
2092
- .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')
2093
2306
  .action(async (agent, options) => {
2094
- const scopeName = options.scope;
2095
- const scope = getScope(scopeName);
2096
- const localPath = scope ? getRepoLocalPath(scope.source) : null;
2097
- const manifest = localPath ? readManifest(localPath) : null;
2098
2307
  const agentsToUpgrade = agent
2099
2308
  ? [agent.toLowerCase()]
2100
- : ALL_AGENT_IDS.filter((id) => manifest?.clis?.[id] || options.latest);
2309
+ : ALL_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
2101
2310
  if (agentsToUpgrade.length === 0) {
2102
- 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>'));
2103
2312
  return;
2104
2313
  }
2105
- const { exec } = await import('child_process');
2106
- const { promisify } = await import('util');
2107
- const execAsync = promisify(exec);
2108
2314
  for (const agentId of agentsToUpgrade) {
2109
2315
  const agentConfig = AGENTS[agentId];
2110
2316
  if (!agentConfig) {
2111
2317
  console.log(chalk.red(`Unknown agent: ${agentId}`));
2112
2318
  continue;
2113
2319
  }
2114
- const cliConfig = manifest?.clis?.[agentId];
2115
- const version = options.latest ? 'latest' : (cliConfig?.version || 'latest');
2116
- const pkg = cliConfig?.package || agentConfig.npmPackage;
2117
- const spinner = ora(`Upgrading ${agentConfig.name} to ${version}...`).start();
2118
- try {
2119
- await execAsync(`npm install -g ${pkg}@${version}`);
2120
- 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
+ }
2121
2327
  }
2122
- 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 {
2123
2338
  spinner.fail(`Failed to upgrade ${agentConfig.name}`);
2124
- console.error(chalk.gray(err.message));
2339
+ console.error(chalk.gray(result.error || 'Unknown error'));
2125
2340
  }
2126
2341
  }
2127
2342
  });
@@ -2265,31 +2480,6 @@ repoCmd
2265
2480
  console.log(chalk.green('\nSync complete.'));
2266
2481
  });
2267
2482
  // =============================================================================
2268
- // INIT COMMAND
2269
- // =============================================================================
2270
- program
2271
- .command('init')
2272
- .description('Initialize a new .agents repo')
2273
- .action(() => {
2274
- ensureAgentsDir();
2275
- const manifest = createDefaultManifest();
2276
- console.log(chalk.bold('\nDefault agents.yaml:\n'));
2277
- console.log(chalk.gray('clis:'));
2278
- console.log(chalk.gray(' claude:'));
2279
- console.log(chalk.gray(' package: "@anthropic-ai/claude-code"'));
2280
- console.log(chalk.gray(' version: "latest"'));
2281
- console.log(chalk.gray(' codex:'));
2282
- console.log(chalk.gray(' package: "@openai/codex"'));
2283
- console.log(chalk.gray(' version: "latest"'));
2284
- console.log();
2285
- console.log(chalk.green('Create a new repo with this structure:'));
2286
- console.log(chalk.gray(' .agents/'));
2287
- console.log(chalk.gray(' agents.yaml'));
2288
- console.log(chalk.gray(' shared/commands/'));
2289
- console.log(chalk.gray(' claude/hooks/'));
2290
- console.log();
2291
- });
2292
- // =============================================================================
2293
2483
  // REGISTRY COMMANDS
2294
2484
  // =============================================================================
2295
2485
  const registryCmd = program
@@ -2472,11 +2662,11 @@ program
2472
2662
  }
2473
2663
  });
2474
2664
  // =============================================================================
2475
- // ADD COMMAND (unified package installation)
2665
+ // INSTALL COMMAND (unified package installation)
2476
2666
  // =============================================================================
2477
2667
  program
2478
- .command('add <identifier>')
2479
- .description('Add a package (mcp:name, skill:user/repo, or gh:user/repo)')
2668
+ .command('install <identifier>')
2669
+ .description('Install a package (mcp:name, skill:user/repo, or gh:user/repo)')
2480
2670
  .option('-a, --agents <list>', 'Comma-separated agents to install to')
2481
2671
  .action(async (identifier, options) => {
2482
2672
  const spinner = ora('Resolving package...').start();
@@ -2577,7 +2767,7 @@ program
2577
2767
  console.log(` ${hooks.shared.length + Object.values(hooks.agentSpecific).flat().length} hooks`);
2578
2768
  const agents = options.agents
2579
2769
  ? options.agents.split(',')
2580
- : ['claude', 'codex', 'gemini'];
2770
+ : ALL_AGENT_IDS;
2581
2771
  const gitCliStates = await getAllCliStates();
2582
2772
  // Install commands
2583
2773
  if (hasCommands) {
@@ -2638,7 +2828,7 @@ program
2638
2828
  });
2639
2829
  // Self-upgrade command
2640
2830
  program
2641
- .command('upgrade')
2831
+ .command('self-upgrade')
2642
2832
  .description('Upgrade agents-cli to the latest version')
2643
2833
  .action(async () => {
2644
2834
  const spinner = ora('Checking for updates...').start();
@@ -2700,6 +2890,498 @@ program
2700
2890
  process.exit(1);
2701
2891
  }
2702
2892
  });
2893
+ // =============================================================================
2894
+ // DAEMON COMMANDS
2895
+ // =============================================================================
2896
+ import { startDaemon, stopDaemon, isDaemonRunning, readDaemonPid, readDaemonLog, runDaemon, signalDaemonReload, } from './lib/daemon.js';
2897
+ import { listJobs as listAllJobs, readJob, validateJob, writeJob, setJobEnabled, getLatestRun, getRunDir, discoverJobsFromRepo, jobExists, jobContentMatches, installJobFromSource, } from './lib/jobs.js';
2898
+ import { executeJob } from './lib/runner.js';
2899
+ import { JobScheduler } from './lib/scheduler.js';
2900
+ const daemonCmd = program.command('daemon').description('Manage the jobs daemon');
2901
+ daemonCmd
2902
+ .command('start')
2903
+ .description('Start the daemon')
2904
+ .action(() => {
2905
+ const result = startDaemon();
2906
+ if (result.method === 'already-running') {
2907
+ console.log(chalk.yellow(`Daemon already running (PID: ${result.pid})`));
2908
+ }
2909
+ else {
2910
+ console.log(chalk.green(`Daemon started (PID: ${result.pid}, method: ${result.method})`));
2911
+ }
2912
+ });
2913
+ daemonCmd
2914
+ .command('stop')
2915
+ .description('Stop the daemon')
2916
+ .action(() => {
2917
+ if (!isDaemonRunning()) {
2918
+ console.log(chalk.yellow('Daemon is not running'));
2919
+ return;
2920
+ }
2921
+ stopDaemon();
2922
+ console.log(chalk.green('Daemon stopped'));
2923
+ });
2924
+ daemonCmd
2925
+ .command('status')
2926
+ .description('Show daemon status')
2927
+ .action(() => {
2928
+ const running = isDaemonRunning();
2929
+ const pid = readDaemonPid();
2930
+ console.log(chalk.bold('Daemon Status\n'));
2931
+ console.log(` Status: ${running ? chalk.green('running') : chalk.gray('stopped')}`);
2932
+ if (pid)
2933
+ console.log(` PID: ${pid}`);
2934
+ const jobs = listAllJobs();
2935
+ const enabled = jobs.filter((j) => j.enabled);
2936
+ console.log(` Jobs: ${enabled.length} enabled / ${jobs.length} total`);
2937
+ if (running && enabled.length > 0) {
2938
+ const scheduler = new JobScheduler(async () => { });
2939
+ scheduler.loadAll();
2940
+ const scheduled = scheduler.listScheduled();
2941
+ console.log(chalk.bold('\n Scheduled Jobs\n'));
2942
+ for (const job of scheduled) {
2943
+ const next = job.nextRun ? job.nextRun.toLocaleString() : 'unknown';
2944
+ console.log(` ${chalk.cyan(job.name.padEnd(24))} next: ${chalk.gray(next)}`);
2945
+ }
2946
+ scheduler.stopAll();
2947
+ }
2948
+ });
2949
+ daemonCmd
2950
+ .command('logs')
2951
+ .description('Show daemon logs')
2952
+ .option('-n, --lines <number>', 'Number of lines to show', '50')
2953
+ .option('-f, --follow', 'Follow log output')
2954
+ .action(async (options) => {
2955
+ if (options.follow) {
2956
+ const { exec: execCb } = await import('child_process');
2957
+ const { getAgentsDir } = await import('./lib/state.js');
2958
+ const logPath = path.join(getAgentsDir(), 'daemon.log');
2959
+ const child = execCb(`tail -f "${logPath}"`);
2960
+ child.stdout?.pipe(process.stdout);
2961
+ child.stderr?.pipe(process.stderr);
2962
+ child.on('exit', () => process.exit(0));
2963
+ process.on('SIGINT', () => { child.kill(); process.exit(0); });
2964
+ return;
2965
+ }
2966
+ const lines = parseInt(options.lines, 10);
2967
+ const output = readDaemonLog(lines);
2968
+ if (output) {
2969
+ console.log(output);
2970
+ }
2971
+ else {
2972
+ console.log(chalk.gray('No daemon logs'));
2973
+ }
2974
+ });
2975
+ daemonCmd
2976
+ .command('_run')
2977
+ .description('Run daemon in foreground (internal)')
2978
+ .action(async () => {
2979
+ await runDaemon();
2980
+ });
2981
+ // =============================================================================
2982
+ // JOBS COMMANDS
2983
+ // =============================================================================
2984
+ const jobsCmd = program.command('jobs').description('Manage scheduled jobs');
2985
+ jobsCmd
2986
+ .command('list')
2987
+ .description('List all jobs')
2988
+ .action(() => {
2989
+ const jobs = listAllJobs();
2990
+ if (jobs.length === 0) {
2991
+ console.log(chalk.gray('No jobs configured'));
2992
+ console.log(chalk.gray(' Add a job: agents jobs add <path-to-job.yml>'));
2993
+ return;
2994
+ }
2995
+ const scheduler = new JobScheduler(async () => { });
2996
+ scheduler.loadAll();
2997
+ console.log(chalk.bold('Scheduled Jobs\n'));
2998
+ const header = ` ${'Name'.padEnd(24)} ${'Agent'.padEnd(10)} ${'Schedule'.padEnd(20)} ${'Enabled'.padEnd(10)} ${'Next Run'.padEnd(24)} ${'Last Status'}`;
2999
+ console.log(chalk.gray(header));
3000
+ console.log(chalk.gray(' ' + '-'.repeat(110)));
3001
+ for (const job of jobs) {
3002
+ const nextRun = scheduler.getNextRun(job.name);
3003
+ const nextStr = nextRun ? nextRun.toLocaleString() : '-';
3004
+ const latestRun = getLatestRun(job.name);
3005
+ const lastStatus = latestRun?.status || '-';
3006
+ const enabledStr = job.enabled ? chalk.green('yes') : chalk.gray('no');
3007
+ const statusColor = lastStatus === 'completed' ? chalk.green : lastStatus === 'failed' ? chalk.red : lastStatus === 'timeout' ? chalk.yellow : chalk.gray;
3008
+ 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)}`);
3009
+ }
3010
+ scheduler.stopAll();
3011
+ console.log();
3012
+ });
3013
+ jobsCmd
3014
+ .command('add <path>')
3015
+ .description('Add a job from a YAML file')
3016
+ .action(async (filePath) => {
3017
+ const resolved = path.resolve(filePath);
3018
+ if (!fs.existsSync(resolved)) {
3019
+ console.log(chalk.red(`File not found: ${resolved}`));
3020
+ process.exit(1);
3021
+ }
3022
+ const content = fs.readFileSync(resolved, 'utf-8');
3023
+ let parsed;
3024
+ try {
3025
+ const yamlMod = await import('yaml');
3026
+ parsed = yamlMod.parse(content);
3027
+ }
3028
+ catch (err) {
3029
+ console.log(chalk.red(`Invalid YAML: ${err.message}`));
3030
+ process.exit(1);
3031
+ }
3032
+ const name = parsed.name || path.basename(resolved).replace(/\.ya?ml$/, '');
3033
+ parsed.name = name;
3034
+ const errors = validateJob(parsed);
3035
+ if (errors.length > 0) {
3036
+ console.log(chalk.red('Validation errors:'));
3037
+ for (const err of errors) {
3038
+ console.log(chalk.red(` - ${err}`));
3039
+ }
3040
+ process.exit(1);
3041
+ }
3042
+ const config = {
3043
+ mode: 'plan',
3044
+ effort: 'default',
3045
+ timeout: '30m',
3046
+ enabled: true,
3047
+ ...parsed,
3048
+ };
3049
+ writeJob(config);
3050
+ console.log(chalk.green(`Job '${name}' added`));
3051
+ if (isDaemonRunning()) {
3052
+ signalDaemonReload();
3053
+ console.log(chalk.gray('Daemon reloaded'));
3054
+ }
3055
+ });
3056
+ jobsCmd
3057
+ .command('run <name>')
3058
+ .description('Run a job immediately in the foreground')
3059
+ .action(async (name) => {
3060
+ const job = readJob(name);
3061
+ if (!job) {
3062
+ console.log(chalk.red(`Job '${name}' not found`));
3063
+ process.exit(1);
3064
+ }
3065
+ console.log(chalk.bold(`Running job '${name}' (agent: ${job.agent}, mode: ${job.mode})\n`));
3066
+ const spinner = ora('Executing...').start();
3067
+ try {
3068
+ const result = await executeJob(job);
3069
+ if (result.meta.status === 'completed') {
3070
+ spinner.succeed(`Job completed (exit code: ${result.meta.exitCode})`);
3071
+ }
3072
+ else if (result.meta.status === 'timeout') {
3073
+ spinner.warn(`Job timed out after ${job.timeout}`);
3074
+ }
3075
+ else {
3076
+ spinner.fail(`Job failed (exit code: ${result.meta.exitCode})`);
3077
+ }
3078
+ console.log(chalk.gray(` Run: ${result.meta.runId}`));
3079
+ console.log(chalk.gray(` Log: ${getRunDir(name, result.meta.runId)}/stdout.log`));
3080
+ if (result.reportPath) {
3081
+ console.log(chalk.bold('\nReport:\n'));
3082
+ console.log(fs.readFileSync(result.reportPath, 'utf-8'));
3083
+ }
3084
+ }
3085
+ catch (err) {
3086
+ spinner.fail('Execution failed');
3087
+ console.error(chalk.red(err.message));
3088
+ process.exit(1);
3089
+ }
3090
+ });
3091
+ jobsCmd
3092
+ .command('logs <name>')
3093
+ .description('Show stdout from the latest (or specific) run')
3094
+ .option('-r, --run <runId>', 'Specific run ID')
3095
+ .action((name, options) => {
3096
+ let runId = options.run;
3097
+ if (!runId) {
3098
+ const latest = getLatestRun(name);
3099
+ if (!latest) {
3100
+ console.log(chalk.yellow(`No runs found for job '${name}'`));
3101
+ return;
3102
+ }
3103
+ runId = latest.runId;
3104
+ }
3105
+ const logPath = path.join(getRunDir(name, runId), 'stdout.log');
3106
+ if (!fs.existsSync(logPath)) {
3107
+ console.log(chalk.yellow(`Log not found: ${logPath}`));
3108
+ return;
3109
+ }
3110
+ console.log(chalk.gray(`Run: ${runId}\n`));
3111
+ console.log(fs.readFileSync(logPath, 'utf-8'));
3112
+ });
3113
+ jobsCmd
3114
+ .command('report <name>')
3115
+ .description('Show report from the latest (or specific) run')
3116
+ .option('-r, --run <runId>', 'Specific run ID')
3117
+ .action((name, options) => {
3118
+ let runId = options.run;
3119
+ if (!runId) {
3120
+ const latest = getLatestRun(name);
3121
+ if (!latest) {
3122
+ console.log(chalk.yellow(`No runs found for job '${name}'`));
3123
+ return;
3124
+ }
3125
+ runId = latest.runId;
3126
+ }
3127
+ const reportPath = path.join(getRunDir(name, runId), 'report.md');
3128
+ if (!fs.existsSync(reportPath)) {
3129
+ console.log(chalk.yellow(`No report found for run ${runId}`));
3130
+ console.log(chalk.gray(` Reports are extracted from agent output on completion`));
3131
+ return;
3132
+ }
3133
+ console.log(chalk.gray(`Run: ${runId}\n`));
3134
+ console.log(fs.readFileSync(reportPath, 'utf-8'));
3135
+ });
3136
+ jobsCmd
3137
+ .command('enable <name>')
3138
+ .description('Enable a job')
3139
+ .action((name) => {
3140
+ try {
3141
+ setJobEnabled(name, true);
3142
+ console.log(chalk.green(`Job '${name}' enabled`));
3143
+ if (isDaemonRunning()) {
3144
+ signalDaemonReload();
3145
+ console.log(chalk.gray('Daemon reloaded'));
3146
+ }
3147
+ }
3148
+ catch (err) {
3149
+ console.log(chalk.red(err.message));
3150
+ process.exit(1);
3151
+ }
3152
+ });
3153
+ jobsCmd
3154
+ .command('disable <name>')
3155
+ .description('Disable a job')
3156
+ .action((name) => {
3157
+ try {
3158
+ setJobEnabled(name, false);
3159
+ console.log(chalk.green(`Job '${name}' disabled`));
3160
+ if (isDaemonRunning()) {
3161
+ signalDaemonReload();
3162
+ console.log(chalk.gray('Daemon reloaded'));
3163
+ }
3164
+ }
3165
+ catch (err) {
3166
+ console.log(chalk.red(err.message));
3167
+ process.exit(1);
3168
+ }
3169
+ });
3170
+ // =============================================================================
3171
+ // DRIVE COMMANDS
3172
+ // =============================================================================
3173
+ import { createDrive, readDrive, listDrives as listAllDrives, deleteDrive, updateDriveFrontmatter, driveExists, discoverDrivesFromRepo, installDriveFromSource, driveContentMatches, } from './lib/drives.js';
3174
+ import { runDriveServer } from './lib/drive-server.js';
3175
+ const driveCmd = program.command('drive').description('Manage context drives');
3176
+ driveCmd
3177
+ .command('create <name>')
3178
+ .description('Create a new empty drive')
3179
+ .option('-d, --description <desc>', 'Drive description')
3180
+ .option('-p, --project <path>', 'Link to a project directory')
3181
+ .action((name, options) => {
3182
+ try {
3183
+ const filePath = createDrive(name, options.description);
3184
+ if (options.project) {
3185
+ updateDriveFrontmatter(name, { project: path.resolve(options.project) });
3186
+ }
3187
+ console.log(chalk.green(`Drive '${name}' created`));
3188
+ console.log(chalk.gray(` ${filePath}`));
3189
+ }
3190
+ catch (err) {
3191
+ console.log(chalk.red(err.message));
3192
+ process.exit(1);
3193
+ }
3194
+ });
3195
+ driveCmd
3196
+ .command('list')
3197
+ .description('List all drives')
3198
+ .action(() => {
3199
+ const drives = listAllDrives();
3200
+ if (drives.length === 0) {
3201
+ console.log(chalk.gray('No drives configured'));
3202
+ console.log(chalk.gray(' Create a drive: agents drive create <name>'));
3203
+ return;
3204
+ }
3205
+ console.log(chalk.bold('Context Drives\n'));
3206
+ const header = ` ${'Name'.padEnd(24)} ${'Description'.padEnd(40)} ${'Project'}`;
3207
+ console.log(chalk.gray(header));
3208
+ console.log(chalk.gray(' ' + '-'.repeat(90)));
3209
+ for (const drive of drives) {
3210
+ const desc = (drive.description || '-').slice(0, 38);
3211
+ const proj = drive.project || '-';
3212
+ console.log(` ${chalk.cyan(drive.name.padEnd(24))} ${desc.padEnd(40)} ${chalk.gray(proj)}`);
3213
+ }
3214
+ console.log();
3215
+ });
3216
+ driveCmd
3217
+ .command('info <name>')
3218
+ .description('Show drive metadata and content preview')
3219
+ .action((name) => {
3220
+ const drive = readDrive(name);
3221
+ if (!drive) {
3222
+ console.log(chalk.red(`Drive '${name}' not found`));
3223
+ process.exit(1);
3224
+ }
3225
+ console.log(chalk.bold(`Drive: ${drive.frontmatter.name}\n`));
3226
+ if (drive.frontmatter.description) {
3227
+ console.log(chalk.gray(` Description: ${drive.frontmatter.description}`));
3228
+ }
3229
+ if (drive.frontmatter.project) {
3230
+ console.log(chalk.gray(` Project: ${drive.frontmatter.project}`));
3231
+ }
3232
+ if (drive.frontmatter.repo) {
3233
+ console.log(chalk.gray(` Repo: ${drive.frontmatter.repo}`));
3234
+ }
3235
+ if (drive.frontmatter.updated) {
3236
+ console.log(chalk.gray(` Updated: ${drive.frontmatter.updated}`));
3237
+ }
3238
+ console.log(chalk.gray(` Path: ${drive.path}`));
3239
+ const content = drive.content.trim();
3240
+ if (content) {
3241
+ console.log(chalk.bold('\nContent:\n'));
3242
+ const lines = content.split('\n');
3243
+ const preview = lines.slice(0, 30);
3244
+ for (const line of preview) {
3245
+ console.log(` ${line}`);
3246
+ }
3247
+ if (lines.length > 30) {
3248
+ console.log(chalk.gray(`\n ... ${lines.length - 30} more lines`));
3249
+ }
3250
+ }
3251
+ console.log();
3252
+ });
3253
+ driveCmd
3254
+ .command('edit <name>')
3255
+ .description('Open drive in $EDITOR')
3256
+ .action((name) => {
3257
+ const drive = readDrive(name);
3258
+ if (!drive) {
3259
+ console.log(chalk.red(`Drive '${name}' not found`));
3260
+ process.exit(1);
3261
+ }
3262
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
3263
+ const { execSync } = require('child_process');
3264
+ try {
3265
+ execSync(`${editor} "${drive.path}"`, { stdio: 'inherit' });
3266
+ }
3267
+ catch {
3268
+ console.log(chalk.red(`Failed to open editor: ${editor}`));
3269
+ process.exit(1);
3270
+ }
3271
+ });
3272
+ driveCmd
3273
+ .command('delete <name>')
3274
+ .description('Delete a drive')
3275
+ .action(async (name) => {
3276
+ if (!driveExists(name)) {
3277
+ console.log(chalk.red(`Drive '${name}' not found`));
3278
+ process.exit(1);
3279
+ }
3280
+ try {
3281
+ const answer = await confirm({ message: `Delete drive '${name}'?` });
3282
+ if (!answer)
3283
+ return;
3284
+ }
3285
+ catch (err) {
3286
+ if (isPromptCancelled(err))
3287
+ return;
3288
+ throw err;
3289
+ }
3290
+ deleteDrive(name);
3291
+ console.log(chalk.green(`Drive '${name}' deleted`));
3292
+ });
3293
+ driveCmd
3294
+ .command('link <name> <path>')
3295
+ .description('Link a drive to a project directory')
3296
+ .action((name, projectPath) => {
3297
+ if (!driveExists(name)) {
3298
+ console.log(chalk.red(`Drive '${name}' not found`));
3299
+ process.exit(1);
3300
+ }
3301
+ const resolved = path.resolve(projectPath);
3302
+ if (!fs.existsSync(resolved)) {
3303
+ console.log(chalk.red(`Directory not found: ${resolved}`));
3304
+ process.exit(1);
3305
+ }
3306
+ updateDriveFrontmatter(name, { project: resolved });
3307
+ console.log(chalk.green(`Drive '${name}' linked to ${resolved}`));
3308
+ });
3309
+ driveCmd
3310
+ .command('sync')
3311
+ .description('Sync drives with .agents repo')
3312
+ .action(async () => {
3313
+ const source = await ensureSource();
3314
+ const repoPath = getRepoLocalPath(source);
3315
+ const discovered = discoverDrivesFromRepo(repoPath);
3316
+ if (discovered.length === 0) {
3317
+ console.log(chalk.gray('No drives found in repo'));
3318
+ return;
3319
+ }
3320
+ let installed = 0;
3321
+ let skipped = 0;
3322
+ for (const d of discovered) {
3323
+ if (driveExists(d.name) && driveContentMatches(d.name, d.path)) {
3324
+ skipped++;
3325
+ continue;
3326
+ }
3327
+ const result = installDriveFromSource(d.path, d.name);
3328
+ if (result.success) {
3329
+ installed++;
3330
+ console.log(chalk.green(` + ${d.name}`));
3331
+ }
3332
+ else {
3333
+ console.log(chalk.red(` x ${d.name}: ${result.error}`));
3334
+ }
3335
+ }
3336
+ if (installed === 0 && skipped > 0) {
3337
+ console.log(chalk.gray(`All ${skipped} drives up to date`));
3338
+ }
3339
+ else if (installed > 0) {
3340
+ console.log(chalk.green(`\nSynced ${installed} drive(s)`));
3341
+ }
3342
+ });
3343
+ driveCmd
3344
+ .command('generate <name>')
3345
+ .description('Run a drive generation job now')
3346
+ .action(async (name) => {
3347
+ if (!driveExists(name)) {
3348
+ console.log(chalk.red(`Drive '${name}' not found`));
3349
+ process.exit(1);
3350
+ }
3351
+ const jobName = `update-drive-${name}`;
3352
+ const job = readJob(jobName);
3353
+ if (!job) {
3354
+ console.log(chalk.red(`No generation job found for drive '${name}'`));
3355
+ console.log(chalk.gray(` Expected job name: ${jobName}`));
3356
+ console.log(chalk.gray(` Create one at: ~/.agents/jobs/${jobName}.yml`));
3357
+ process.exit(1);
3358
+ }
3359
+ console.log(chalk.bold(`Generating drive '${name}' (job: ${jobName})\n`));
3360
+ const spinner = ora('Executing...').start();
3361
+ try {
3362
+ const result = await executeJob(job);
3363
+ if (result.meta.status === 'completed') {
3364
+ spinner.succeed(`Drive '${name}' updated`);
3365
+ }
3366
+ else if (result.meta.status === 'timeout') {
3367
+ spinner.warn(`Generation timed out after ${job.timeout}`);
3368
+ }
3369
+ else {
3370
+ spinner.fail(`Generation failed (exit code: ${result.meta.exitCode})`);
3371
+ }
3372
+ }
3373
+ catch (err) {
3374
+ spinner.fail('Generation failed');
3375
+ console.error(chalk.red(err.message));
3376
+ process.exit(1);
3377
+ }
3378
+ });
3379
+ driveCmd
3380
+ .command('serve')
3381
+ .description('Start the drive MCP server (stdio)')
3382
+ .action(async () => {
3383
+ await runDriveServer();
3384
+ });
2703
3385
  async function showWhatsNew(fromVersion, toVersion) {
2704
3386
  try {
2705
3387
  // Fetch changelog from npm package