@swarmify/agents-cli 1.0.0 → 1.1.0

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.
package/dist/index.js CHANGED
@@ -2,13 +2,62 @@
2
2
  import { Command } from 'commander';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
- import { checkbox, select } from '@inquirer/prompts';
6
- import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, getAllCliStates, isCliInstalled, isMcpRegistered, registerMcp, unregisterMcp, } from './lib/agents.js';
5
+ import { checkbox, confirm, select } from '@inquirer/prompts';
6
+ import { AGENTS, ALL_AGENT_IDS, MCP_CAPABLE_AGENTS, SKILLS_CAPABLE_AGENTS, HOOKS_CAPABLE_AGENTS, getAllCliStates, isCliInstalled, isMcpRegistered, registerMcp, unregisterMcp, listInstalledMcpsWithScope, promoteMcpToUser, } from './lib/agents.js';
7
7
  import { readManifest, writeManifest, createDefaultManifest, MANIFEST_FILENAME, } from './lib/manifest.js';
8
- import { readState, writeState, ensureAgentsDir, getRepoLocalPath, } from './lib/state.js';
9
- import { cloneRepo } from './lib/git.js';
10
- import { discoverSkills, resolveSkillSource, installSkill, uninstallSkill, listInstalledSkills, } from './lib/skills.js';
8
+ import { readState, ensureAgentsDir, getRepoLocalPath, getScope, setScope, removeScope, getScopesByPriority, getScopePriority, } from './lib/state.js';
9
+ import { SCOPE_PRIORITIES, DEFAULT_SYSTEM_REPO } from './lib/types.js';
10
+ import { cloneRepo, parseSource } from './lib/git.js';
11
+ import { discoverCommands, resolveCommandSource, installCommand, uninstallCommand, listInstalledCommandsWithScope, promoteCommandToUser, } from './lib/commands.js';
12
+ import { discoverHooksFromRepo, installHooks, listInstalledHooksWithScope, promoteHookToUser, removeHook, } from './lib/hooks.js';
13
+ import { discoverSkillsFromRepo, installSkill, uninstallSkill, listInstalledSkillsWithScope, promoteSkillToUser, getSkillInfo, getSkillRules, } from './lib/skills.js';
14
+ import { DEFAULT_REGISTRIES } from './lib/types.js';
15
+ import { search as searchRegistries, getRegistries, setRegistry, removeRegistry, resolvePackage, } from './lib/registry.js';
11
16
  const program = new Command();
17
+ /**
18
+ * Ensure at least one scope is configured.
19
+ * If not, automatically initialize the system scope from DEFAULT_SYSTEM_REPO.
20
+ * Returns the highest priority scope's source.
21
+ */
22
+ async function ensureSource(scopeName) {
23
+ const meta = readState();
24
+ // If specific scope requested, check if it exists
25
+ if (scopeName) {
26
+ const scope = meta.scopes[scopeName];
27
+ if (scope?.source) {
28
+ return scope.source;
29
+ }
30
+ throw new Error(`Scope '${scopeName}' not configured. Run: agents repo add <source> --scope ${scopeName}`);
31
+ }
32
+ // Check if any scope is configured
33
+ const scopes = getScopesByPriority();
34
+ if (scopes.length > 0) {
35
+ // Return highest priority scope's source
36
+ return scopes[scopes.length - 1].config.source;
37
+ }
38
+ // No scopes configured - initialize system scope
39
+ console.log(chalk.gray(`No repo configured. Initializing system scope from ${DEFAULT_SYSTEM_REPO}...`));
40
+ const parsed = parseSource(DEFAULT_SYSTEM_REPO);
41
+ const { commit } = await cloneRepo(DEFAULT_SYSTEM_REPO);
42
+ setScope('system', {
43
+ source: DEFAULT_SYSTEM_REPO,
44
+ branch: parsed.ref || 'main',
45
+ commit,
46
+ lastSync: new Date().toISOString(),
47
+ priority: SCOPE_PRIORITIES.system,
48
+ readonly: true,
49
+ });
50
+ return DEFAULT_SYSTEM_REPO;
51
+ }
52
+ /**
53
+ * Get repo local path for a scope.
54
+ */
55
+ function getScopeLocalPath(scopeName) {
56
+ const scope = getScope(scopeName);
57
+ if (!scope)
58
+ return null;
59
+ return getRepoLocalPath(scope.source);
60
+ }
12
61
  program
13
62
  .name('agents')
14
63
  .description('Dotfiles manager for AI coding agents')
@@ -18,10 +67,11 @@ program
18
67
  // =============================================================================
19
68
  program
20
69
  .command('status')
21
- .description('Show sync status, CLI versions, installed skills and MCP servers')
70
+ .description('Show sync status, CLI versions, installed commands and MCP servers')
22
71
  .action(() => {
23
72
  const state = readState();
24
73
  const cliStates = getAllCliStates();
74
+ const cwd = process.cwd();
25
75
  console.log(chalk.bold('\nAgent CLIs\n'));
26
76
  for (const agentId of ALL_AGENT_IDS) {
27
77
  const agent = AGENTS[agentId];
@@ -31,21 +81,75 @@ program
31
81
  : chalk.gray('not installed');
32
82
  console.log(` ${agent.name.padEnd(14)} ${status}`);
33
83
  }
34
- console.log(chalk.bold('\nInstalled Skills\n'));
84
+ console.log(chalk.bold('\nInstalled Commands\n'));
35
85
  for (const agentId of ALL_AGENT_IDS) {
36
86
  const agent = AGENTS[agentId];
37
- const skills = listInstalledSkills(agentId);
87
+ const commands = listInstalledCommandsWithScope(agentId, cwd);
88
+ const userCommands = commands.filter((c) => c.scope === 'user');
89
+ const projectCommands = commands.filter((c) => c.scope === 'project');
90
+ if (commands.length > 0) {
91
+ const parts = [];
92
+ if (userCommands.length > 0) {
93
+ parts.push(`${chalk.cyan(userCommands.length)} user`);
94
+ }
95
+ if (projectCommands.length > 0) {
96
+ parts.push(`${chalk.yellow(projectCommands.length)} project`);
97
+ }
98
+ console.log(` ${agent.name}: ${parts.join(', ')}`);
99
+ }
100
+ }
101
+ console.log(chalk.bold('\nInstalled Skills\n'));
102
+ for (const agentId of SKILLS_CAPABLE_AGENTS) {
103
+ const agent = AGENTS[agentId];
104
+ const skills = listInstalledSkillsWithScope(agentId, cwd);
105
+ const userSkills = skills.filter((s) => s.scope === 'user');
106
+ const projectSkills = skills.filter((s) => s.scope === 'project');
38
107
  if (skills.length > 0) {
39
- console.log(` ${agent.name}: ${chalk.cyan(skills.length)} skills`);
108
+ const parts = [];
109
+ if (userSkills.length > 0) {
110
+ parts.push(`${chalk.cyan(userSkills.length)} user`);
111
+ }
112
+ if (projectSkills.length > 0) {
113
+ parts.push(`${chalk.yellow(projectSkills.length)} project`);
114
+ }
115
+ console.log(` ${agent.name}: ${parts.join(', ')}`);
116
+ }
117
+ }
118
+ console.log(chalk.bold('\nInstalled MCP Servers\n'));
119
+ for (const agentId of MCP_CAPABLE_AGENTS) {
120
+ const agent = AGENTS[agentId];
121
+ if (!isCliInstalled(agentId))
122
+ continue;
123
+ const mcps = listInstalledMcpsWithScope(agentId, cwd);
124
+ const userMcps = mcps.filter((m) => m.scope === 'user');
125
+ const projectMcps = mcps.filter((m) => m.scope === 'project');
126
+ if (mcps.length > 0) {
127
+ const parts = [];
128
+ if (userMcps.length > 0) {
129
+ parts.push(`${chalk.cyan(userMcps.length)} user`);
130
+ }
131
+ if (projectMcps.length > 0) {
132
+ parts.push(`${chalk.yellow(projectMcps.length)} project`);
133
+ }
134
+ console.log(` ${agent.name}: ${parts.join(', ')}`);
40
135
  }
41
136
  }
42
- if (state.source) {
43
- console.log(chalk.bold('\nSync Source\n'));
44
- console.log(` ${state.source}`);
45
- if (state.lastSync) {
46
- console.log(` Last sync: ${new Date(state.lastSync).toLocaleString()}`);
137
+ const scopes = getScopesByPriority();
138
+ if (scopes.length > 0) {
139
+ console.log(chalk.bold('\nConfigured Scopes\n'));
140
+ for (const { name, config } of scopes) {
141
+ const readonlyTag = config.readonly ? chalk.gray(' (readonly)') : '';
142
+ const priorityTag = chalk.gray(` [priority: ${config.priority}]`);
143
+ console.log(` ${chalk.bold(name)}${readonlyTag}${priorityTag}`);
144
+ console.log(` ${config.source}`);
145
+ console.log(` Branch: ${config.branch} Commit: ${config.commit.substring(0, 8)}`);
146
+ console.log(` Last sync: ${new Date(config.lastSync).toLocaleString()}`);
47
147
  }
48
148
  }
149
+ else {
150
+ console.log(chalk.bold('\nNo scopes configured\n'));
151
+ console.log(chalk.gray(' Run: agents repo add <source>'));
152
+ }
49
153
  console.log();
50
154
  });
51
155
  // =============================================================================
@@ -56,18 +160,29 @@ program
56
160
  .description('Pull and sync from remote .agents repo')
57
161
  .option('-y, --yes', 'Skip interactive prompts')
58
162
  .option('-f, --force', 'Overwrite local changes')
163
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
59
164
  .option('--dry-run', 'Show what would change')
60
165
  .option('--skip-clis', 'Skip CLI installation')
61
166
  .option('--skip-mcp', 'Skip MCP registration')
62
167
  .action(async (source, options) => {
63
- const state = readState();
64
- const targetSource = source || state.source;
168
+ const scopeName = options.scope;
169
+ const meta = readState();
170
+ const existingScope = meta.scopes[scopeName];
171
+ const targetSource = source || existingScope?.source;
65
172
  if (!targetSource) {
66
- console.log(chalk.red('No source specified. Usage: agents pull <source>'));
173
+ console.log(chalk.red(`No source specified for scope '${scopeName}'.`));
174
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
175
+ console.log(chalk.gray(` Usage: agents pull <source>${scopeHint}`));
67
176
  console.log(chalk.gray(' Example: agents pull gh:username/.agents'));
68
177
  process.exit(1);
69
178
  }
70
- const spinner = ora('Cloning repository...').start();
179
+ // Prevent modification of readonly scopes
180
+ if (existingScope?.readonly && !options.force) {
181
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Use --force to override.`));
182
+ process.exit(1);
183
+ }
184
+ const parsed = parseSource(targetSource);
185
+ const spinner = ora(`Cloning repository for ${scopeName} scope...`).start();
71
186
  try {
72
187
  const { localPath, commit, isNew } = await cloneRepo(targetSource);
73
188
  spinner.succeed(isNew ? 'Repository cloned' : 'Repository updated');
@@ -75,14 +190,14 @@ program
75
190
  if (!manifest) {
76
191
  console.log(chalk.yellow(`No ${MANIFEST_FILENAME} found in repository`));
77
192
  }
78
- const skills = discoverSkills(localPath);
79
- console.log(chalk.bold(`\nFound ${skills.length} skills\n`));
80
- for (const skill of skills.slice(0, 10)) {
81
- const source = skill.isShared ? 'shared' : skill.agentSpecific;
82
- console.log(` ${chalk.cyan(skill.name.padEnd(20))} ${chalk.gray(source)}`);
193
+ const commands = discoverCommands(localPath);
194
+ console.log(chalk.bold(`\nFound ${commands.length} commands\n`));
195
+ for (const command of commands.slice(0, 10)) {
196
+ const src = command.isShared ? 'shared' : command.agentSpecific;
197
+ console.log(` ${chalk.cyan(command.name.padEnd(20))} ${chalk.gray(src)}`);
83
198
  }
84
- if (skills.length > 10) {
85
- console.log(chalk.gray(` ... and ${skills.length - 10} more`));
199
+ if (commands.length > 10) {
200
+ console.log(chalk.gray(` ... and ${commands.length - 10} more`));
86
201
  }
87
202
  if (options.dryRun) {
88
203
  console.log(chalk.yellow('\nDry run - no changes made'));
@@ -114,20 +229,20 @@ program
114
229
  });
115
230
  }
116
231
  const defaultAgents = selectedAgents;
117
- const installSpinner = ora('Installing skills...').start();
232
+ const installSpinner = ora('Installing commands...').start();
118
233
  let installed = 0;
119
- for (const skill of skills) {
234
+ for (const command of commands) {
120
235
  for (const agentId of defaultAgents) {
121
236
  if (!isCliInstalled(agentId) && agentId !== 'cursor')
122
237
  continue;
123
- const sourcePath = resolveSkillSource(localPath, skill.name, agentId);
238
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
124
239
  if (sourcePath) {
125
- installSkill(sourcePath, agentId, skill.name, method);
240
+ installCommand(sourcePath, agentId, command.name, method);
126
241
  installed++;
127
242
  }
128
243
  }
129
244
  }
130
- installSpinner.succeed(`Installed ${installed} skill instances`);
245
+ installSpinner.succeed(`Installed ${installed} command instances`);
131
246
  if (!options.skipMcp && manifest?.mcp) {
132
247
  const mcpSpinner = ora('Registering MCP servers...').start();
133
248
  let registered = 0;
@@ -144,12 +259,17 @@ program
144
259
  }
145
260
  mcpSpinner.succeed(`Registered ${registered} MCP servers`);
146
261
  }
147
- writeState({
148
- ...state,
262
+ // Update scope config
263
+ const priority = getScopePriority(scopeName);
264
+ setScope(scopeName, {
149
265
  source: targetSource,
266
+ branch: parsed.ref || 'main',
267
+ commit,
150
268
  lastSync: new Date().toISOString(),
269
+ priority,
270
+ readonly: scopeName === 'system',
151
271
  });
152
- console.log(chalk.green('\nSync complete.'));
272
+ console.log(chalk.green(`\nSync complete. Updated scope: ${scopeName}`));
153
273
  }
154
274
  catch (err) {
155
275
  spinner.fail('Failed to sync');
@@ -162,15 +282,23 @@ program
162
282
  // =============================================================================
163
283
  program
164
284
  .command('push')
165
- .description('Export local skills to .agents repo for manual commit')
166
- .option('--export-only', 'Only export skills, do not update manifest')
285
+ .description('Export local configuration to .agents repo for manual commit')
286
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
287
+ .option('--export-only', 'Only export, do not update manifest')
167
288
  .action(async (options) => {
168
- const state = readState();
169
- if (!state.source) {
170
- console.log(chalk.red('No .agents repo configured. Run: agents pull <source>'));
289
+ const scopeName = options.scope;
290
+ const scope = getScope(scopeName);
291
+ if (!scope) {
292
+ console.log(chalk.red(`Scope '${scopeName}' not configured.`));
293
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
294
+ console.log(chalk.gray(` Run: agents repo add <source>${scopeHint}`));
295
+ process.exit(1);
296
+ }
297
+ if (scope.readonly) {
298
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot push.`));
171
299
  process.exit(1);
172
300
  }
173
- const localPath = getRepoLocalPath(state.source);
301
+ const localPath = getRepoLocalPath(scope.source);
174
302
  const manifest = readManifest(localPath) || createDefaultManifest();
175
303
  console.log(chalk.bold('\nExporting local configuration...\n'));
176
304
  const cliStates = getAllCliStates();
@@ -218,71 +346,89 @@ program
218
346
  await program.commands.find((c) => c.name() === 'pull')?.parseAsync(args, { from: 'user' });
219
347
  });
220
348
  // =============================================================================
221
- // SKILLS COMMANDS
349
+ // COMMANDS COMMANDS
222
350
  // =============================================================================
223
- const skillsCmd = program
224
- .command('skills')
225
- .description('Manage skills/commands');
226
- skillsCmd
351
+ const commandsCmd = program
352
+ .command('commands')
353
+ .description('Manage slash commands');
354
+ commandsCmd
227
355
  .command('list')
228
- .description('List installed skills')
229
- .option('-a, --agent <agent>', 'Show skills for specific agent')
356
+ .description('List installed commands')
357
+ .option('-a, --agent <agent>', 'Filter by agent')
358
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
230
359
  .action((options) => {
231
- console.log(chalk.bold('\nInstalled Skills\n'));
360
+ console.log(chalk.bold('\nInstalled Commands\n'));
361
+ const cwd = process.cwd();
232
362
  const agents = options.agent
233
363
  ? [options.agent]
234
364
  : ALL_AGENT_IDS;
235
365
  for (const agentId of agents) {
236
366
  const agent = AGENTS[agentId];
237
- const skills = listInstalledSkills(agentId);
238
- if (skills.length === 0) {
367
+ let commands = listInstalledCommandsWithScope(agentId, cwd);
368
+ if (options.scope !== 'all') {
369
+ commands = commands.filter((c) => c.scope === options.scope);
370
+ }
371
+ if (commands.length === 0) {
239
372
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
240
373
  }
241
374
  else {
242
375
  console.log(` ${chalk.bold(agent.name)}:`);
243
- for (const skill of skills) {
244
- console.log(` ${chalk.cyan(skill)}`);
376
+ const userCommands = commands.filter((c) => c.scope === 'user');
377
+ const projectCommands = commands.filter((c) => c.scope === 'project');
378
+ if (userCommands.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
379
+ console.log(` ${chalk.gray('User:')}`);
380
+ for (const cmd of userCommands) {
381
+ const desc = cmd.description ? ` - ${chalk.gray(cmd.description)}` : '';
382
+ console.log(` ${chalk.cyan(cmd.name)}${desc}`);
383
+ }
384
+ }
385
+ if (projectCommands.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
386
+ console.log(` ${chalk.gray('Project:')}`);
387
+ for (const cmd of projectCommands) {
388
+ const desc = cmd.description ? ` - ${chalk.gray(cmd.description)}` : '';
389
+ console.log(` ${chalk.yellow(cmd.name)}${desc}`);
390
+ }
245
391
  }
246
392
  }
247
393
  console.log();
248
394
  }
249
395
  });
250
- skillsCmd
396
+ commandsCmd
251
397
  .command('add <source>')
252
- .description('Add skill from Git repo or local path')
398
+ .description('Add commands from Git repo or local path')
253
399
  .option('-a, --agents <list>', 'Comma-separated agents to install to')
254
400
  .action(async (source, options) => {
255
- const spinner = ora('Fetching skill...').start();
401
+ const spinner = ora('Fetching commands...').start();
256
402
  try {
257
403
  const { localPath } = await cloneRepo(source);
258
- const skills = discoverSkills(localPath);
259
- spinner.succeed(`Found ${skills.length} skills`);
404
+ const commands = discoverCommands(localPath);
405
+ spinner.succeed(`Found ${commands.length} commands`);
260
406
  const agents = options.agents
261
407
  ? options.agents.split(',')
262
408
  : ['claude', 'codex', 'gemini'];
263
- for (const skill of skills) {
264
- console.log(`\n ${chalk.cyan(skill.name)}: ${skill.description}`);
409
+ for (const command of commands) {
410
+ console.log(`\n ${chalk.cyan(command.name)}: ${command.description}`);
265
411
  for (const agentId of agents) {
266
412
  if (!isCliInstalled(agentId) && agentId !== 'cursor')
267
413
  continue;
268
- const sourcePath = resolveSkillSource(localPath, skill.name, agentId);
414
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
269
415
  if (sourcePath) {
270
- installSkill(sourcePath, agentId, skill.name, 'symlink');
416
+ installCommand(sourcePath, agentId, command.name, 'symlink');
271
417
  console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
272
418
  }
273
419
  }
274
420
  }
275
- console.log(chalk.green('\nSkills installed.'));
421
+ console.log(chalk.green('\nCommands installed.'));
276
422
  }
277
423
  catch (err) {
278
- spinner.fail('Failed to add skill');
424
+ spinner.fail('Failed to add commands');
279
425
  console.error(chalk.red(err.message));
280
426
  process.exit(1);
281
427
  }
282
428
  });
283
- skillsCmd
429
+ commandsCmd
284
430
  .command('remove <name>')
285
- .description('Remove a skill from all agents')
431
+ .description('Remove a command from all agents')
286
432
  .option('-a, --agents <list>', 'Comma-separated agents to remove from')
287
433
  .action((name, options) => {
288
434
  const agents = options.agents
@@ -290,18 +436,389 @@ skillsCmd
290
436
  : ALL_AGENT_IDS;
291
437
  let removed = 0;
292
438
  for (const agentId of agents) {
293
- if (uninstallSkill(agentId, name)) {
439
+ if (uninstallCommand(agentId, name)) {
294
440
  console.log(` ${chalk.red('-')} ${AGENTS[agentId].name}`);
295
441
  removed++;
296
442
  }
297
443
  }
298
444
  if (removed === 0) {
299
- console.log(chalk.yellow(`Skill '${name}' not found`));
445
+ console.log(chalk.yellow(`Command '${name}' not found`));
300
446
  }
301
447
  else {
302
448
  console.log(chalk.green(`\nRemoved from ${removed} agents.`));
303
449
  }
304
450
  });
451
+ commandsCmd
452
+ .command('push <name>')
453
+ .description('Save project-scoped command to user scope')
454
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
455
+ .action((name, options) => {
456
+ const cwd = process.cwd();
457
+ const agents = options.agents
458
+ ? options.agents.split(',')
459
+ : ALL_AGENT_IDS;
460
+ let pushed = 0;
461
+ for (const agentId of agents) {
462
+ if (!isCliInstalled(agentId) && agentId !== 'cursor')
463
+ continue;
464
+ const result = promoteCommandToUser(agentId, name, cwd);
465
+ if (result.success) {
466
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
467
+ pushed++;
468
+ }
469
+ else if (result.error && !result.error.includes('not found')) {
470
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
471
+ }
472
+ }
473
+ if (pushed === 0) {
474
+ console.log(chalk.yellow(`Project command '${name}' not found for any agent`));
475
+ }
476
+ else {
477
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
478
+ }
479
+ });
480
+ const hooksCmd = program.command('hooks').description('Manage hooks');
481
+ hooksCmd
482
+ .command('list')
483
+ .description('List installed hooks')
484
+ .option('-a, --agent <agent>', 'Filter by agent')
485
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
486
+ .action((options) => {
487
+ console.log(chalk.bold('\nInstalled Hooks\n'));
488
+ const cwd = process.cwd();
489
+ const agents = options.agent
490
+ ? [options.agent]
491
+ : Array.from(HOOKS_CAPABLE_AGENTS);
492
+ for (const agentId of agents) {
493
+ const agent = AGENTS[agentId];
494
+ if (!agent.supportsHooks) {
495
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('hooks not supported')}`);
496
+ console.log();
497
+ continue;
498
+ }
499
+ let hooks = listInstalledHooksWithScope(agentId, cwd);
500
+ if (options.scope !== 'all') {
501
+ hooks = hooks.filter((h) => h.scope === options.scope);
502
+ }
503
+ if (hooks.length === 0) {
504
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
505
+ }
506
+ else {
507
+ console.log(` ${chalk.bold(agent.name)}:`);
508
+ const userHooks = hooks.filter((h) => h.scope === 'user');
509
+ const projectHooks = hooks.filter((h) => h.scope === 'project');
510
+ if (userHooks.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
511
+ console.log(` ${chalk.gray('User:')}`);
512
+ for (const hook of userHooks) {
513
+ console.log(` ${chalk.cyan(hook.name)}`);
514
+ }
515
+ }
516
+ if (projectHooks.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
517
+ console.log(` ${chalk.gray('Project:')}`);
518
+ for (const hook of projectHooks) {
519
+ console.log(` ${chalk.yellow(hook.name)}`);
520
+ }
521
+ }
522
+ }
523
+ console.log();
524
+ }
525
+ });
526
+ hooksCmd
527
+ .command('add <source>')
528
+ .description('Install hooks from git repo or local path')
529
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)', 'claude,gemini')
530
+ .action(async (source, options) => {
531
+ const spinner = ora('Fetching hooks...').start();
532
+ try {
533
+ const { localPath } = await cloneRepo(source);
534
+ const hooks = discoverHooksFromRepo(localPath);
535
+ const hookNames = new Set();
536
+ for (const name of hooks.shared) {
537
+ hookNames.add(name);
538
+ }
539
+ for (const list of Object.values(hooks.agentSpecific)) {
540
+ for (const name of list) {
541
+ hookNames.add(name);
542
+ }
543
+ }
544
+ spinner.succeed(`Found ${hookNames.size} hooks`);
545
+ const agents = options.agent
546
+ ? options.agent.split(',')
547
+ : ['claude', 'gemini'];
548
+ const result = await installHooks(localPath, agents, { scope: 'user' });
549
+ const installedByHook = new Map();
550
+ for (const item of result.installed) {
551
+ const [name, agentId] = item.split(':');
552
+ const list = installedByHook.get(name) || [];
553
+ list.push(agentId);
554
+ installedByHook.set(name, list);
555
+ }
556
+ const orderedHooks = Array.from(installedByHook.keys()).sort((a, b) => a.localeCompare(b));
557
+ for (const name of orderedHooks) {
558
+ console.log(`\n ${chalk.cyan(name)}`);
559
+ const agentIds = installedByHook.get(name) || [];
560
+ agentIds.sort();
561
+ for (const agentId of agentIds) {
562
+ console.log(` ${AGENTS[agentId].name}`);
563
+ }
564
+ }
565
+ if (result.errors.length > 0) {
566
+ console.log(chalk.red('\nErrors:'));
567
+ for (const error of result.errors) {
568
+ console.log(chalk.red(` ${error}`));
569
+ }
570
+ }
571
+ if (result.installed.length === 0) {
572
+ console.log(chalk.yellow('\nNo hooks installed.'));
573
+ }
574
+ else {
575
+ console.log(chalk.green('\nHooks installed.'));
576
+ }
577
+ }
578
+ catch (err) {
579
+ spinner.fail('Failed to add hooks');
580
+ console.error(chalk.red(err.message));
581
+ process.exit(1);
582
+ }
583
+ });
584
+ hooksCmd
585
+ .command('remove <name>')
586
+ .description('Remove a hook')
587
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)')
588
+ .action(async (name, options) => {
589
+ const agents = options.agent
590
+ ? options.agent.split(',')
591
+ : Array.from(HOOKS_CAPABLE_AGENTS);
592
+ const result = await removeHook(name, agents);
593
+ let removed = 0;
594
+ for (const item of result.removed) {
595
+ const [, agentId] = item.split(':');
596
+ console.log(` ${AGENTS[agentId].name}`);
597
+ removed++;
598
+ }
599
+ if (result.errors.length > 0) {
600
+ console.log(chalk.red('\nErrors:'));
601
+ for (const error of result.errors) {
602
+ console.log(chalk.red(` ${error}`));
603
+ }
604
+ }
605
+ if (removed === 0) {
606
+ console.log(chalk.yellow(`Hook '${name}' not found`));
607
+ }
608
+ else {
609
+ console.log(chalk.green(`\nRemoved from ${removed} agents.`));
610
+ }
611
+ });
612
+ hooksCmd
613
+ .command('push <name>')
614
+ .description('Copy project-scoped hook to user scope')
615
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)')
616
+ .action((name, options) => {
617
+ const cwd = process.cwd();
618
+ const agents = options.agent
619
+ ? options.agent.split(',')
620
+ : Array.from(HOOKS_CAPABLE_AGENTS);
621
+ let pushed = 0;
622
+ for (const agentId of agents) {
623
+ const result = promoteHookToUser(agentId, name, cwd);
624
+ if (result.success) {
625
+ console.log(` ${AGENTS[agentId].name}`);
626
+ pushed++;
627
+ }
628
+ else if (result.error && !result.error.includes('not found')) {
629
+ console.log(` ${AGENTS[agentId].name}: ${result.error}`);
630
+ }
631
+ }
632
+ if (pushed === 0) {
633
+ console.log(chalk.yellow(`Project hook '${name}' not found for any agent`));
634
+ }
635
+ else {
636
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
637
+ }
638
+ });
639
+ // =============================================================================
640
+ // SKILLS COMMANDS (Agent Skills)
641
+ // =============================================================================
642
+ const skillsCmd = program
643
+ .command('skills')
644
+ .description('Manage Agent Skills (SKILL.md + rules/)');
645
+ skillsCmd
646
+ .command('list')
647
+ .description('List installed Agent Skills')
648
+ .option('-a, --agent <agent>', 'Filter by agent')
649
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
650
+ .action((options) => {
651
+ console.log(chalk.bold('\nInstalled Agent Skills\n'));
652
+ const cwd = process.cwd();
653
+ const agents = options.agent
654
+ ? [options.agent]
655
+ : SKILLS_CAPABLE_AGENTS;
656
+ for (const agentId of agents) {
657
+ const agent = AGENTS[agentId];
658
+ if (!agent.capabilities.skills) {
659
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('skills not supported')}`);
660
+ console.log();
661
+ continue;
662
+ }
663
+ let skills = listInstalledSkillsWithScope(agentId, cwd);
664
+ if (options.scope !== 'all') {
665
+ skills = skills.filter((s) => s.scope === options.scope);
666
+ }
667
+ if (skills.length === 0) {
668
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
669
+ }
670
+ else {
671
+ console.log(` ${chalk.bold(agent.name)}:`);
672
+ const userSkills = skills.filter((s) => s.scope === 'user');
673
+ const projectSkills = skills.filter((s) => s.scope === 'project');
674
+ if (userSkills.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
675
+ console.log(` ${chalk.gray('User:')}`);
676
+ for (const skill of userSkills) {
677
+ const desc = skill.metadata.description ? ` - ${chalk.gray(skill.metadata.description)}` : '';
678
+ const ruleInfo = skill.ruleCount > 0 ? chalk.gray(` (${skill.ruleCount} rules)`) : '';
679
+ console.log(` ${chalk.cyan(skill.name)}${desc}${ruleInfo}`);
680
+ }
681
+ }
682
+ if (projectSkills.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
683
+ console.log(` ${chalk.gray('Project:')}`);
684
+ for (const skill of projectSkills) {
685
+ const desc = skill.metadata.description ? ` - ${chalk.gray(skill.metadata.description)}` : '';
686
+ const ruleInfo = skill.ruleCount > 0 ? chalk.gray(` (${skill.ruleCount} rules)`) : '';
687
+ console.log(` ${chalk.yellow(skill.name)}${desc}${ruleInfo}`);
688
+ }
689
+ }
690
+ }
691
+ console.log();
692
+ }
693
+ });
694
+ skillsCmd
695
+ .command('add <source>')
696
+ .description('Add Agent Skills from Git repo or local path')
697
+ .option('-a, --agents <list>', 'Comma-separated agents to install to')
698
+ .action(async (source, options) => {
699
+ const spinner = ora('Fetching skills...').start();
700
+ try {
701
+ const { localPath } = await cloneRepo(source);
702
+ const skills = discoverSkillsFromRepo(localPath);
703
+ spinner.succeed(`Found ${skills.length} skills`);
704
+ if (skills.length === 0) {
705
+ console.log(chalk.yellow('No skills found (looking for SKILL.md files)'));
706
+ return;
707
+ }
708
+ for (const skill of skills) {
709
+ console.log(`\n ${chalk.cyan(skill.name)}: ${skill.metadata.description || 'no description'}`);
710
+ if (skill.ruleCount > 0) {
711
+ console.log(` ${chalk.gray(`${skill.ruleCount} rules`)}`);
712
+ }
713
+ }
714
+ const agents = options.agents
715
+ ? options.agents.split(',')
716
+ : await checkbox({
717
+ message: 'Select agents to install skills to:',
718
+ choices: SKILLS_CAPABLE_AGENTS.filter((id) => isCliInstalled(id) || id === 'cursor').map((id) => ({
719
+ name: AGENTS[id].name,
720
+ value: id,
721
+ checked: ['claude', 'codex', 'gemini'].includes(id),
722
+ })),
723
+ });
724
+ if (agents.length === 0) {
725
+ console.log(chalk.yellow('\nNo agents selected.'));
726
+ return;
727
+ }
728
+ const installSpinner = ora('Installing skills...').start();
729
+ let installed = 0;
730
+ for (const skill of skills) {
731
+ const result = installSkill(skill.path, skill.name, agents);
732
+ if (result.success) {
733
+ installed++;
734
+ }
735
+ else {
736
+ console.log(chalk.red(`\n Failed to install ${skill.name}: ${result.error}`));
737
+ }
738
+ }
739
+ installSpinner.succeed(`Installed ${installed} skills to ${agents.length} agents`);
740
+ console.log(chalk.green('\nSkills installed.'));
741
+ }
742
+ catch (err) {
743
+ spinner.fail('Failed to add skills');
744
+ console.error(chalk.red(err.message));
745
+ process.exit(1);
746
+ }
747
+ });
748
+ skillsCmd
749
+ .command('remove <name>')
750
+ .description('Remove an Agent Skill')
751
+ .action((name) => {
752
+ const result = uninstallSkill(name);
753
+ if (result.success) {
754
+ console.log(chalk.green(`Removed skill '${name}'`));
755
+ }
756
+ else {
757
+ console.log(chalk.red(result.error || 'Failed to remove skill'));
758
+ }
759
+ });
760
+ skillsCmd
761
+ .command('push <name>')
762
+ .description('Save project-scoped skill to user scope')
763
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
764
+ .action((name, options) => {
765
+ const cwd = process.cwd();
766
+ const agents = options.agents
767
+ ? options.agents.split(',')
768
+ : SKILLS_CAPABLE_AGENTS;
769
+ let pushed = 0;
770
+ for (const agentId of agents) {
771
+ if (!AGENTS[agentId].capabilities.skills)
772
+ continue;
773
+ const result = promoteSkillToUser(agentId, name, cwd);
774
+ if (result.success) {
775
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
776
+ pushed++;
777
+ }
778
+ else if (result.error && !result.error.includes('not found')) {
779
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
780
+ }
781
+ }
782
+ if (pushed === 0) {
783
+ console.log(chalk.yellow(`Project skill '${name}' not found for any agent`));
784
+ }
785
+ else {
786
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
787
+ }
788
+ });
789
+ skillsCmd
790
+ .command('info <name>')
791
+ .description('Show detailed info about an installed skill')
792
+ .action((name) => {
793
+ const skill = getSkillInfo(name);
794
+ if (!skill) {
795
+ console.log(chalk.yellow(`Skill '${name}' not found`));
796
+ return;
797
+ }
798
+ console.log(chalk.bold(`\n${skill.metadata.name}\n`));
799
+ if (skill.metadata.description) {
800
+ console.log(` ${skill.metadata.description}`);
801
+ }
802
+ console.log();
803
+ if (skill.metadata.author) {
804
+ console.log(` Author: ${skill.metadata.author}`);
805
+ }
806
+ if (skill.metadata.version) {
807
+ console.log(` Version: ${skill.metadata.version}`);
808
+ }
809
+ if (skill.metadata.license) {
810
+ console.log(` License: ${skill.metadata.license}`);
811
+ }
812
+ console.log(` Path: ${skill.path}`);
813
+ const rules = getSkillRules(name);
814
+ if (rules.length > 0) {
815
+ console.log(chalk.bold(`\n Rules (${rules.length}):\n`));
816
+ for (const rule of rules) {
817
+ console.log(` ${chalk.cyan(rule)}`);
818
+ }
819
+ }
820
+ console.log();
821
+ });
305
822
  // =============================================================================
306
823
  // MCP COMMANDS
307
824
  // =============================================================================
@@ -311,32 +828,71 @@ const mcpCmd = program
311
828
  mcpCmd
312
829
  .command('list')
313
830
  .description('List MCP servers and registration status')
314
- .action(() => {
831
+ .option('-a, --agent <agent>', 'Filter by agent')
832
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
833
+ .action((options) => {
315
834
  console.log(chalk.bold('\nMCP Servers\n'));
316
- for (const agentId of MCP_CAPABLE_AGENTS) {
835
+ const cwd = process.cwd();
836
+ const agents = options.agent
837
+ ? [options.agent]
838
+ : MCP_CAPABLE_AGENTS;
839
+ for (const agentId of agents) {
317
840
  const agent = AGENTS[agentId];
841
+ if (!agent.capabilities.mcp) {
842
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('mcp not supported')}`);
843
+ console.log();
844
+ continue;
845
+ }
318
846
  if (!isCliInstalled(agentId)) {
319
847
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('CLI not installed')}`);
320
848
  continue;
321
849
  }
322
- console.log(` ${chalk.bold(agent.name)}:`);
323
- const swarmRegistered = isMcpRegistered(agentId, 'Swarm');
324
- console.log(` Swarm: ${swarmRegistered ? chalk.green('registered') : chalk.gray('not registered')}`);
850
+ let mcps = listInstalledMcpsWithScope(agentId, cwd);
851
+ if (options.scope !== 'all') {
852
+ mcps = mcps.filter((m) => m.scope === options.scope);
853
+ }
854
+ if (mcps.length === 0) {
855
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
856
+ }
857
+ else {
858
+ console.log(` ${chalk.bold(agent.name)}:`);
859
+ const userMcps = mcps.filter((m) => m.scope === 'user');
860
+ const projectMcps = mcps.filter((m) => m.scope === 'project');
861
+ if (userMcps.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
862
+ console.log(` ${chalk.gray('User:')}`);
863
+ for (const mcp of userMcps) {
864
+ console.log(` ${chalk.cyan(mcp.name)}`);
865
+ }
866
+ }
867
+ if (projectMcps.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
868
+ console.log(` ${chalk.gray('Project:')}`);
869
+ for (const mcp of projectMcps) {
870
+ console.log(` ${chalk.yellow(mcp.name)}`);
871
+ }
872
+ }
873
+ }
325
874
  console.log();
326
875
  }
327
876
  });
328
877
  mcpCmd
329
- .command('add <name> <command>')
330
- .description('Add MCP server to manifest')
878
+ .command('add <name>')
879
+ .description('Add MCP server to manifest (use -- before command)')
331
880
  .option('-a, --agents <list>', 'Comma-separated agents', 'claude,codex,gemini')
332
881
  .option('-s, --scope <scope>', 'Scope: user or project', 'user')
333
- .action((name, command, options) => {
334
- const state = readState();
335
- if (!state.source) {
336
- console.log(chalk.yellow('No .agents repo configured. Run: agents pull <source>'));
337
- return;
882
+ .allowUnknownOption(true)
883
+ .action(async (name, options, cmd) => {
884
+ // Get everything after -- as the command
885
+ const rawArgs = cmd.args || [];
886
+ const commandParts = rawArgs.slice(rawArgs.indexOf(name) + 1);
887
+ if (commandParts.length === 0) {
888
+ console.error(chalk.red('Error: Command required after --'));
889
+ console.log(chalk.gray('Usage: agents mcp add <name> -- <command...>'));
890
+ console.log(chalk.gray('Example: agents mcp add swarm -- npx @swarmify/agents-mcp'));
891
+ process.exit(1);
338
892
  }
339
- const localPath = getRepoLocalPath(state.source);
893
+ const command = commandParts.join(' ');
894
+ const source = await ensureSource();
895
+ const localPath = getRepoLocalPath(source);
340
896
  const manifest = readManifest(localPath) || createDefaultManifest();
341
897
  manifest.mcp = manifest.mcp || {};
342
898
  manifest.mcp[name] = {
@@ -378,14 +934,10 @@ mcpCmd
378
934
  .command('register [name]')
379
935
  .description('Register MCP server(s) with agent CLIs')
380
936
  .option('-a, --agents <list>', 'Comma-separated agents')
381
- .action((name, options) => {
382
- const state = readState();
937
+ .action(async (name, options) => {
383
938
  if (!name) {
384
- if (!state.source) {
385
- console.log(chalk.yellow('No .agents repo configured. Run: agents pull <source>'));
386
- return;
387
- }
388
- const localPath = getRepoLocalPath(state.source);
939
+ const source = await ensureSource();
940
+ const localPath = getRepoLocalPath(source);
389
941
  const manifest = readManifest(localPath);
390
942
  if (!manifest?.mcp) {
391
943
  console.log(chalk.yellow('No MCP servers in manifest'));
@@ -409,6 +961,35 @@ mcpCmd
409
961
  }
410
962
  console.log(chalk.yellow('Single MCP registration not yet implemented'));
411
963
  });
964
+ mcpCmd
965
+ .command('push <name>')
966
+ .description('Save project-scoped MCP to user scope')
967
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
968
+ .action((name, options) => {
969
+ const cwd = process.cwd();
970
+ const agents = options.agents
971
+ ? options.agents.split(',')
972
+ : MCP_CAPABLE_AGENTS;
973
+ let pushed = 0;
974
+ for (const agentId of agents) {
975
+ if (!isCliInstalled(agentId))
976
+ continue;
977
+ const result = promoteMcpToUser(agentId, name, cwd);
978
+ if (result.success) {
979
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
980
+ pushed++;
981
+ }
982
+ else if (result.error && !result.error.includes('not found')) {
983
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
984
+ }
985
+ }
986
+ if (pushed === 0) {
987
+ console.log(chalk.yellow(`Project MCP '${name}' not found for any agent`));
988
+ }
989
+ else {
990
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
991
+ }
992
+ });
412
993
  // =============================================================================
413
994
  // CLI COMMANDS
414
995
  // =============================================================================
@@ -440,19 +1021,15 @@ cliCmd
440
1021
  .command('add <agent>')
441
1022
  .description('Add agent CLI to manifest')
442
1023
  .option('-v, --version <version>', 'Version to pin', 'latest')
443
- .action((agent, options) => {
444
- const state = readState();
445
- if (!state.source) {
446
- console.log(chalk.yellow('No .agents repo configured. Run: agents pull <source>'));
447
- return;
448
- }
1024
+ .action(async (agent, options) => {
449
1025
  const agentId = agent.toLowerCase();
450
1026
  if (!AGENTS[agentId]) {
451
1027
  console.log(chalk.red(`Unknown agent: ${agent}`));
452
1028
  console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
453
1029
  return;
454
1030
  }
455
- const localPath = getRepoLocalPath(state.source);
1031
+ const source = await ensureSource();
1032
+ const localPath = getRepoLocalPath(source);
456
1033
  const manifest = readManifest(localPath) || createDefaultManifest();
457
1034
  manifest.clis = manifest.clis || {};
458
1035
  manifest.clis[agentId] = {
@@ -465,14 +1042,10 @@ cliCmd
465
1042
  cliCmd
466
1043
  .command('remove <agent>')
467
1044
  .description('Remove agent CLI from manifest')
468
- .action((agent) => {
469
- const state = readState();
470
- if (!state.source) {
471
- console.log(chalk.yellow('No .agents repo configured. Run: agents pull <source>'));
472
- return;
473
- }
1045
+ .action(async (agent) => {
1046
+ const source = await ensureSource();
474
1047
  const agentId = agent.toLowerCase();
475
- const localPath = getRepoLocalPath(state.source);
1048
+ const localPath = getRepoLocalPath(source);
476
1049
  const manifest = readManifest(localPath);
477
1050
  if (manifest?.clis?.[agentId]) {
478
1051
  delete manifest.clis[agentId];
@@ -486,10 +1059,12 @@ cliCmd
486
1059
  cliCmd
487
1060
  .command('upgrade [agent]')
488
1061
  .description('Upgrade agent CLI(s) to version in manifest')
1062
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
489
1063
  .option('--latest', 'Upgrade to latest version (ignore manifest)')
490
1064
  .action(async (agent, options) => {
491
- const state = readState();
492
- const localPath = state.source ? getRepoLocalPath(state.source) : null;
1065
+ const scopeName = options.scope;
1066
+ const scope = getScope(scopeName);
1067
+ const localPath = scope ? getRepoLocalPath(scope.source) : null;
493
1068
  const manifest = localPath ? readManifest(localPath) : null;
494
1069
  const agentsToUpgrade = agent
495
1070
  ? [agent.toLowerCase()]
@@ -520,6 +1095,145 @@ cliCmd
520
1095
  }
521
1096
  });
522
1097
  // =============================================================================
1098
+ // REPO COMMANDS
1099
+ // =============================================================================
1100
+ const repoCmd = program
1101
+ .command('repo')
1102
+ .description('Manage .agents repository scopes');
1103
+ repoCmd
1104
+ .command('list')
1105
+ .description('List configured repository scopes')
1106
+ .action(() => {
1107
+ const scopes = getScopesByPriority();
1108
+ if (scopes.length === 0) {
1109
+ console.log(chalk.yellow('\nNo scopes configured.'));
1110
+ console.log(chalk.gray(' Run: agents repo add <source>'));
1111
+ console.log();
1112
+ return;
1113
+ }
1114
+ console.log(chalk.bold('\nConfigured Scopes\n'));
1115
+ console.log(chalk.gray(' Scopes are applied in priority order (higher overrides lower)\n'));
1116
+ for (const { name, config } of scopes) {
1117
+ const readonlyTag = config.readonly ? chalk.gray(' (readonly)') : '';
1118
+ console.log(` ${chalk.bold(name)}${readonlyTag}`);
1119
+ console.log(` Source: ${config.source}`);
1120
+ console.log(` Branch: ${config.branch}`);
1121
+ console.log(` Commit: ${config.commit.substring(0, 8)}`);
1122
+ console.log(` Priority: ${config.priority}`);
1123
+ console.log(` Synced: ${new Date(config.lastSync).toLocaleString()}`);
1124
+ console.log();
1125
+ }
1126
+ });
1127
+ repoCmd
1128
+ .command('add <source>')
1129
+ .description('Add a repository scope')
1130
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
1131
+ .option('-y, --yes', 'Skip confirmation prompts')
1132
+ .action(async (source, options) => {
1133
+ const scopeName = options.scope;
1134
+ const existingScope = getScope(scopeName);
1135
+ if (existingScope && !options.yes) {
1136
+ const shouldOverwrite = await confirm({
1137
+ message: `Scope '${scopeName}' already exists (${existingScope.source}). Overwrite?`,
1138
+ default: false,
1139
+ });
1140
+ if (!shouldOverwrite) {
1141
+ console.log(chalk.yellow('Cancelled.'));
1142
+ return;
1143
+ }
1144
+ }
1145
+ if (existingScope?.readonly && !options.yes) {
1146
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot overwrite.`));
1147
+ return;
1148
+ }
1149
+ const parsed = parseSource(source);
1150
+ const spinner = ora(`Cloning repository for ${scopeName} scope...`).start();
1151
+ try {
1152
+ const { commit, isNew } = await cloneRepo(source);
1153
+ spinner.succeed(isNew ? 'Repository cloned' : 'Repository updated');
1154
+ const priority = getScopePriority(scopeName);
1155
+ setScope(scopeName, {
1156
+ source,
1157
+ branch: parsed.ref || 'main',
1158
+ commit,
1159
+ lastSync: new Date().toISOString(),
1160
+ priority,
1161
+ readonly: scopeName === 'system',
1162
+ });
1163
+ console.log(chalk.green(`\nAdded scope '${scopeName}' with priority ${priority}`));
1164
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
1165
+ console.log(chalk.gray(` Run: agents pull${scopeHint} to sync commands`));
1166
+ }
1167
+ catch (err) {
1168
+ spinner.fail('Failed to add scope');
1169
+ console.error(chalk.red(err.message));
1170
+ process.exit(1);
1171
+ }
1172
+ });
1173
+ repoCmd
1174
+ .command('remove <scope>')
1175
+ .description('Remove a repository scope')
1176
+ .option('-y, --yes', 'Skip confirmation prompts')
1177
+ .action(async (scopeName, options) => {
1178
+ const existingScope = getScope(scopeName);
1179
+ if (!existingScope) {
1180
+ console.log(chalk.yellow(`Scope '${scopeName}' not found.`));
1181
+ return;
1182
+ }
1183
+ if (existingScope.readonly) {
1184
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot remove.`));
1185
+ return;
1186
+ }
1187
+ if (!options.yes) {
1188
+ const shouldRemove = await confirm({
1189
+ message: `Remove scope '${scopeName}' (${existingScope.source})?`,
1190
+ default: false,
1191
+ });
1192
+ if (!shouldRemove) {
1193
+ console.log(chalk.yellow('Cancelled.'));
1194
+ return;
1195
+ }
1196
+ }
1197
+ const removed = removeScope(scopeName);
1198
+ if (removed) {
1199
+ console.log(chalk.green(`Removed scope '${scopeName}'`));
1200
+ }
1201
+ else {
1202
+ console.log(chalk.yellow(`Failed to remove scope '${scopeName}'`));
1203
+ }
1204
+ });
1205
+ repoCmd
1206
+ .command('sync [scope]')
1207
+ .description('Sync a specific scope or all scopes')
1208
+ .option('-y, --yes', 'Skip confirmation prompts')
1209
+ .action(async (scopeName, options) => {
1210
+ const scopes = scopeName ? [{ name: scopeName, config: getScope(scopeName) }].filter(s => s.config) : getScopesByPriority();
1211
+ if (scopes.length === 0) {
1212
+ console.log(chalk.yellow('No scopes to sync.'));
1213
+ return;
1214
+ }
1215
+ for (const { name, config } of scopes) {
1216
+ if (!config)
1217
+ continue;
1218
+ console.log(chalk.bold(`\nSyncing scope: ${name}`));
1219
+ const spinner = ora('Updating repository...').start();
1220
+ try {
1221
+ const { commit } = await cloneRepo(config.source);
1222
+ spinner.succeed('Repository updated');
1223
+ setScope(name, {
1224
+ ...config,
1225
+ commit,
1226
+ lastSync: new Date().toISOString(),
1227
+ });
1228
+ }
1229
+ catch (err) {
1230
+ spinner.fail(`Failed to sync ${name}`);
1231
+ console.error(chalk.gray(err.message));
1232
+ }
1233
+ }
1234
+ console.log(chalk.green('\nSync complete.'));
1235
+ });
1236
+ // =============================================================================
523
1237
  // INIT COMMAND
524
1238
  // =============================================================================
525
1239
  program
@@ -544,5 +1258,341 @@ program
544
1258
  console.log(chalk.gray(' claude/hooks/'));
545
1259
  console.log();
546
1260
  });
1261
+ // =============================================================================
1262
+ // REGISTRY COMMANDS
1263
+ // =============================================================================
1264
+ const registryCmd = program
1265
+ .command('registry')
1266
+ .description('Manage package registries (MCP servers, skills)');
1267
+ registryCmd
1268
+ .command('list')
1269
+ .description('List configured registries')
1270
+ .option('-t, --type <type>', 'Filter by type: mcp or skill')
1271
+ .action((options) => {
1272
+ const types = options.type ? [options.type] : ['mcp', 'skill'];
1273
+ console.log(chalk.bold('\nConfigured Registries\n'));
1274
+ for (const type of types) {
1275
+ console.log(chalk.bold(` ${type.toUpperCase()}`));
1276
+ const registries = getRegistries(type);
1277
+ const entries = Object.entries(registries);
1278
+ if (entries.length === 0) {
1279
+ console.log(chalk.gray(' No registries configured'));
1280
+ }
1281
+ else {
1282
+ for (const [name, config] of entries) {
1283
+ const status = config.enabled ? chalk.green('enabled') : chalk.gray('disabled');
1284
+ const isDefault = DEFAULT_REGISTRIES[type]?.[name] ? chalk.gray(' (default)') : '';
1285
+ console.log(` ${name}${isDefault}: ${status}`);
1286
+ console.log(chalk.gray(` ${config.url}`));
1287
+ }
1288
+ }
1289
+ console.log();
1290
+ }
1291
+ });
1292
+ registryCmd
1293
+ .command('add <type> <name> <url>')
1294
+ .description('Add a registry (type: mcp or skill)')
1295
+ .option('--api-key <key>', 'API key for authentication')
1296
+ .action((type, name, url, options) => {
1297
+ if (type !== 'mcp' && type !== 'skill') {
1298
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1299
+ process.exit(1);
1300
+ }
1301
+ setRegistry(type, name, {
1302
+ url,
1303
+ enabled: true,
1304
+ apiKey: options.apiKey,
1305
+ });
1306
+ console.log(chalk.green(`Added ${type} registry '${name}'`));
1307
+ });
1308
+ registryCmd
1309
+ .command('remove <type> <name>')
1310
+ .description('Remove a registry')
1311
+ .action((type, name) => {
1312
+ if (type !== 'mcp' && type !== 'skill') {
1313
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1314
+ process.exit(1);
1315
+ }
1316
+ // Check if it's a default registry
1317
+ if (DEFAULT_REGISTRIES[type]?.[name]) {
1318
+ console.log(chalk.yellow(`Cannot remove default registry '${name}'. Use 'agents registry disable' instead.`));
1319
+ process.exit(1);
1320
+ }
1321
+ if (removeRegistry(type, name)) {
1322
+ console.log(chalk.green(`Removed ${type} registry '${name}'`));
1323
+ }
1324
+ else {
1325
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1326
+ }
1327
+ });
1328
+ registryCmd
1329
+ .command('enable <type> <name>')
1330
+ .description('Enable a registry')
1331
+ .action((type, name) => {
1332
+ if (type !== 'mcp' && type !== 'skill') {
1333
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1334
+ process.exit(1);
1335
+ }
1336
+ const registries = getRegistries(type);
1337
+ if (!registries[name]) {
1338
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1339
+ process.exit(1);
1340
+ }
1341
+ setRegistry(type, name, { enabled: true });
1342
+ console.log(chalk.green(`Enabled ${type} registry '${name}'`));
1343
+ });
1344
+ registryCmd
1345
+ .command('disable <type> <name>')
1346
+ .description('Disable a registry')
1347
+ .action((type, name) => {
1348
+ if (type !== 'mcp' && type !== 'skill') {
1349
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1350
+ process.exit(1);
1351
+ }
1352
+ const registries = getRegistries(type);
1353
+ if (!registries[name]) {
1354
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1355
+ process.exit(1);
1356
+ }
1357
+ setRegistry(type, name, { enabled: false });
1358
+ console.log(chalk.green(`Disabled ${type} registry '${name}'`));
1359
+ });
1360
+ registryCmd
1361
+ .command('config <type> <name>')
1362
+ .description('Configure a registry')
1363
+ .option('--api-key <key>', 'Set API key')
1364
+ .option('--url <url>', 'Update URL')
1365
+ .action((type, name, options) => {
1366
+ if (type !== 'mcp' && type !== 'skill') {
1367
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1368
+ process.exit(1);
1369
+ }
1370
+ const registries = getRegistries(type);
1371
+ if (!registries[name]) {
1372
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1373
+ process.exit(1);
1374
+ }
1375
+ const updates = {};
1376
+ if (options.apiKey)
1377
+ updates.apiKey = options.apiKey;
1378
+ if (options.url)
1379
+ updates.url = options.url;
1380
+ if (Object.keys(updates).length === 0) {
1381
+ console.log(chalk.yellow('No options provided. Use --api-key or --url.'));
1382
+ process.exit(1);
1383
+ }
1384
+ setRegistry(type, name, updates);
1385
+ console.log(chalk.green(`Updated ${type} registry '${name}'`));
1386
+ });
1387
+ // =============================================================================
1388
+ // SEARCH COMMAND
1389
+ // =============================================================================
1390
+ program
1391
+ .command('search <query>')
1392
+ .description('Search registries for packages (MCP servers, skills)')
1393
+ .option('-t, --type <type>', 'Filter by type: mcp or skill')
1394
+ .option('-r, --registry <name>', 'Search specific registry')
1395
+ .option('-l, --limit <n>', 'Max results', '20')
1396
+ .action(async (query, options) => {
1397
+ const spinner = ora('Searching registries...').start();
1398
+ try {
1399
+ const results = await searchRegistries(query, {
1400
+ type: options.type,
1401
+ registry: options.registry,
1402
+ limit: parseInt(options.limit, 10),
1403
+ });
1404
+ spinner.stop();
1405
+ if (results.length === 0) {
1406
+ console.log(chalk.yellow('\nNo packages found.'));
1407
+ if (!options.type) {
1408
+ console.log(chalk.gray('\nTip: skill registries not yet available. Use gh:user/repo for skills.'));
1409
+ }
1410
+ return;
1411
+ }
1412
+ console.log(chalk.bold(`\nFound ${results.length} packages\n`));
1413
+ // Group by type
1414
+ const mcpResults = results.filter((r) => r.type === 'mcp');
1415
+ const skillResults = results.filter((r) => r.type === 'skill');
1416
+ if (mcpResults.length > 0) {
1417
+ console.log(chalk.bold(' MCP Servers'));
1418
+ for (const result of mcpResults) {
1419
+ const desc = result.description
1420
+ ? chalk.gray(` - ${result.description.slice(0, 50)}${result.description.length > 50 ? '...' : ''}`)
1421
+ : '';
1422
+ console.log(` ${chalk.cyan(result.name)}${desc}`);
1423
+ console.log(chalk.gray(` Registry: ${result.registry} Install: agents add mcp:${result.name}`));
1424
+ }
1425
+ console.log();
1426
+ }
1427
+ if (skillResults.length > 0) {
1428
+ console.log(chalk.bold(' Skills'));
1429
+ for (const result of skillResults) {
1430
+ const desc = result.description
1431
+ ? chalk.gray(` - ${result.description.slice(0, 50)}${result.description.length > 50 ? '...' : ''}`)
1432
+ : '';
1433
+ console.log(` ${chalk.cyan(result.name)}${desc}`);
1434
+ console.log(chalk.gray(` Registry: ${result.registry} Install: agents add skill:${result.name}`));
1435
+ }
1436
+ console.log();
1437
+ }
1438
+ }
1439
+ catch (err) {
1440
+ spinner.fail('Search failed');
1441
+ console.error(chalk.red(err.message));
1442
+ process.exit(1);
1443
+ }
1444
+ });
1445
+ // =============================================================================
1446
+ // ADD COMMAND (unified package installation)
1447
+ // =============================================================================
1448
+ program
1449
+ .command('add <identifier>')
1450
+ .description('Add a package (mcp:name, skill:user/repo, or gh:user/repo)')
1451
+ .option('-a, --agents <list>', 'Comma-separated agents to install to')
1452
+ .action(async (identifier, options) => {
1453
+ const spinner = ora('Resolving package...').start();
1454
+ try {
1455
+ const resolved = await resolvePackage(identifier);
1456
+ if (!resolved) {
1457
+ spinner.fail('Package not found');
1458
+ console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, gh:) or check the identifier.'));
1459
+ process.exit(1);
1460
+ }
1461
+ spinner.succeed(`Found ${resolved.type} package`);
1462
+ if (resolved.type === 'mcp') {
1463
+ // Install MCP server
1464
+ const entry = resolved.mcpEntry;
1465
+ if (!entry) {
1466
+ console.log(chalk.red('Failed to get MCP server details'));
1467
+ process.exit(1);
1468
+ }
1469
+ console.log(chalk.bold(`\n${entry.name}`));
1470
+ if (entry.description) {
1471
+ console.log(chalk.gray(` ${entry.description}`));
1472
+ }
1473
+ if (entry.repository?.url) {
1474
+ console.log(chalk.gray(` ${entry.repository.url}`));
1475
+ }
1476
+ // Get package info
1477
+ const pkg = entry.packages?.[0];
1478
+ if (!pkg) {
1479
+ console.log(chalk.yellow('\nNo installable package found for this server.'));
1480
+ console.log(chalk.gray('You may need to install it manually.'));
1481
+ process.exit(1);
1482
+ }
1483
+ console.log(chalk.bold('\nPackage:'));
1484
+ console.log(` Name: ${pkg.name || pkg.registry_name}`);
1485
+ console.log(` Runtime: ${pkg.runtime || 'unknown'}`);
1486
+ console.log(` Transport: ${pkg.transport || 'stdio'}`);
1487
+ if (pkg.packageArguments && pkg.packageArguments.length > 0) {
1488
+ console.log(chalk.bold('\nRequired arguments:'));
1489
+ for (const arg of pkg.packageArguments) {
1490
+ const req = arg.required ? chalk.red('*') : '';
1491
+ console.log(` ${arg.name}${req}: ${arg.description || ''}`);
1492
+ }
1493
+ }
1494
+ // Determine command based on runtime
1495
+ let command;
1496
+ if (pkg.runtime === 'node') {
1497
+ command = `npx -y ${pkg.name || pkg.registry_name}`;
1498
+ }
1499
+ else if (pkg.runtime === 'python') {
1500
+ command = `uvx ${pkg.name || pkg.registry_name}`;
1501
+ }
1502
+ else {
1503
+ command = pkg.name || pkg.registry_name;
1504
+ }
1505
+ const agents = options.agents
1506
+ ? options.agents.split(',')
1507
+ : MCP_CAPABLE_AGENTS.filter((id) => isCliInstalled(id));
1508
+ if (agents.length === 0) {
1509
+ console.log(chalk.yellow('\nNo MCP-capable agents installed.'));
1510
+ process.exit(1);
1511
+ }
1512
+ console.log(chalk.bold('\nInstalling to agents...'));
1513
+ for (const agentId of agents) {
1514
+ if (!isCliInstalled(agentId))
1515
+ continue;
1516
+ const result = registerMcp(agentId, entry.name, command, 'user');
1517
+ if (result.success) {
1518
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1519
+ }
1520
+ else {
1521
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
1522
+ }
1523
+ }
1524
+ console.log(chalk.green('\nMCP server installed.'));
1525
+ }
1526
+ else if (resolved.type === 'git' || resolved.type === 'skill') {
1527
+ // Install from git source (skills/commands/hooks)
1528
+ console.log(chalk.bold(`\nInstalling from ${resolved.source}`));
1529
+ const { localPath } = await cloneRepo(resolved.source);
1530
+ // Discover what's in the repo
1531
+ const commands = discoverCommands(localPath);
1532
+ const skills = discoverSkillsFromRepo(localPath);
1533
+ const hooks = discoverHooksFromRepo(localPath);
1534
+ const hasCommands = commands.length > 0;
1535
+ const hasSkills = skills.length > 0;
1536
+ const hasHooks = hooks.shared.length > 0 || Object.values(hooks.agentSpecific).some((h) => h.length > 0);
1537
+ if (!hasCommands && !hasSkills && !hasHooks) {
1538
+ console.log(chalk.yellow('No installable content found in repository.'));
1539
+ process.exit(1);
1540
+ }
1541
+ console.log(chalk.bold('\nFound:'));
1542
+ if (hasCommands)
1543
+ console.log(` ${commands.length} commands`);
1544
+ if (hasSkills)
1545
+ console.log(` ${skills.length} skills`);
1546
+ if (hasHooks)
1547
+ console.log(` ${hooks.shared.length + Object.values(hooks.agentSpecific).flat().length} hooks`);
1548
+ const agents = options.agents
1549
+ ? options.agents.split(',')
1550
+ : ['claude', 'codex', 'gemini'];
1551
+ // Install commands
1552
+ if (hasCommands) {
1553
+ console.log(chalk.bold('\nInstalling commands...'));
1554
+ let installed = 0;
1555
+ for (const command of commands) {
1556
+ for (const agentId of agents) {
1557
+ if (!isCliInstalled(agentId) && agentId !== 'cursor')
1558
+ continue;
1559
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
1560
+ if (sourcePath) {
1561
+ installCommand(sourcePath, agentId, command.name, 'symlink');
1562
+ installed++;
1563
+ }
1564
+ }
1565
+ }
1566
+ console.log(` Installed ${installed} command instances`);
1567
+ }
1568
+ // Install skills
1569
+ if (hasSkills) {
1570
+ console.log(chalk.bold('\nInstalling skills...'));
1571
+ for (const skill of skills) {
1572
+ const result = installSkill(skill.path, skill.name, agents);
1573
+ if (result.success) {
1574
+ console.log(` ${chalk.green('+')} ${skill.name}`);
1575
+ }
1576
+ else {
1577
+ console.log(` ${chalk.red('x')} ${skill.name}: ${result.error}`);
1578
+ }
1579
+ }
1580
+ }
1581
+ // Install hooks
1582
+ if (hasHooks) {
1583
+ console.log(chalk.bold('\nInstalling hooks...'));
1584
+ const hookAgents = agents.filter((id) => AGENTS[id].supportsHooks);
1585
+ const result = await installHooks(localPath, hookAgents, { scope: 'user' });
1586
+ console.log(` Installed ${result.installed.length} hooks`);
1587
+ }
1588
+ console.log(chalk.green('\nPackage installed.'));
1589
+ }
1590
+ }
1591
+ catch (err) {
1592
+ spinner.fail('Installation failed');
1593
+ console.error(chalk.red(err.message));
1594
+ process.exit(1);
1595
+ }
1596
+ });
547
1597
  program.parse();
548
1598
  //# sourceMappingURL=index.js.map