@swarmify/agents-cli 1.0.0 → 1.2.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(', ')}`);
40
116
  }
41
117
  }
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()}`);
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(', ')}`);
47
135
  }
48
136
  }
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()}`);
147
+ }
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,24 +229,27 @@ 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;
134
249
  for (const [name, config] of Object.entries(manifest.mcp)) {
250
+ // Skip HTTP transport MCPs for now (need different registration)
251
+ if (config.transport === 'http' || !config.command)
252
+ continue;
135
253
  for (const agentId of config.agents) {
136
254
  if (!isCliInstalled(agentId))
137
255
  continue;
@@ -144,12 +262,17 @@ program
144
262
  }
145
263
  mcpSpinner.succeed(`Registered ${registered} MCP servers`);
146
264
  }
147
- writeState({
148
- ...state,
265
+ // Update scope config
266
+ const priority = getScopePriority(scopeName);
267
+ setScope(scopeName, {
149
268
  source: targetSource,
269
+ branch: parsed.ref || 'main',
270
+ commit,
150
271
  lastSync: new Date().toISOString(),
272
+ priority,
273
+ readonly: scopeName === 'system',
151
274
  });
152
- console.log(chalk.green('\nSync complete.'));
275
+ console.log(chalk.green(`\nSync complete. Updated scope: ${scopeName}`));
153
276
  }
154
277
  catch (err) {
155
278
  spinner.fail('Failed to sync');
@@ -162,15 +285,23 @@ program
162
285
  // =============================================================================
163
286
  program
164
287
  .command('push')
165
- .description('Export local skills to .agents repo for manual commit')
166
- .option('--export-only', 'Only export skills, do not update manifest')
288
+ .description('Export local configuration to .agents repo for manual commit')
289
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
290
+ .option('--export-only', 'Only export, do not update manifest')
167
291
  .action(async (options) => {
168
- const state = readState();
169
- if (!state.source) {
170
- console.log(chalk.red('No .agents repo configured. Run: agents pull <source>'));
292
+ const scopeName = options.scope;
293
+ const scope = getScope(scopeName);
294
+ if (!scope) {
295
+ console.log(chalk.red(`Scope '${scopeName}' not configured.`));
296
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
297
+ console.log(chalk.gray(` Run: agents repo add <source>${scopeHint}`));
298
+ process.exit(1);
299
+ }
300
+ if (scope.readonly) {
301
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot push.`));
171
302
  process.exit(1);
172
303
  }
173
- const localPath = getRepoLocalPath(state.source);
304
+ const localPath = getRepoLocalPath(scope.source);
174
305
  const manifest = readManifest(localPath) || createDefaultManifest();
175
306
  console.log(chalk.bold('\nExporting local configuration...\n'));
176
307
  const cliStates = getAllCliStates();
@@ -218,71 +349,89 @@ program
218
349
  await program.commands.find((c) => c.name() === 'pull')?.parseAsync(args, { from: 'user' });
219
350
  });
220
351
  // =============================================================================
221
- // SKILLS COMMANDS
352
+ // COMMANDS COMMANDS
222
353
  // =============================================================================
223
- const skillsCmd = program
224
- .command('skills')
225
- .description('Manage skills/commands');
226
- skillsCmd
354
+ const commandsCmd = program
355
+ .command('commands')
356
+ .description('Manage slash commands');
357
+ commandsCmd
227
358
  .command('list')
228
- .description('List installed skills')
229
- .option('-a, --agent <agent>', 'Show skills for specific agent')
359
+ .description('List installed commands')
360
+ .option('-a, --agent <agent>', 'Filter by agent')
361
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
230
362
  .action((options) => {
231
- console.log(chalk.bold('\nInstalled Skills\n'));
363
+ console.log(chalk.bold('\nInstalled Commands\n'));
364
+ const cwd = process.cwd();
232
365
  const agents = options.agent
233
366
  ? [options.agent]
234
367
  : ALL_AGENT_IDS;
235
368
  for (const agentId of agents) {
236
369
  const agent = AGENTS[agentId];
237
- const skills = listInstalledSkills(agentId);
238
- if (skills.length === 0) {
370
+ let commands = listInstalledCommandsWithScope(agentId, cwd);
371
+ if (options.scope !== 'all') {
372
+ commands = commands.filter((c) => c.scope === options.scope);
373
+ }
374
+ if (commands.length === 0) {
239
375
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
240
376
  }
241
377
  else {
242
378
  console.log(` ${chalk.bold(agent.name)}:`);
243
- for (const skill of skills) {
244
- console.log(` ${chalk.cyan(skill)}`);
379
+ const userCommands = commands.filter((c) => c.scope === 'user');
380
+ const projectCommands = commands.filter((c) => c.scope === 'project');
381
+ if (userCommands.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
382
+ console.log(` ${chalk.gray('User:')}`);
383
+ for (const cmd of userCommands) {
384
+ const desc = cmd.description ? ` - ${chalk.gray(cmd.description)}` : '';
385
+ console.log(` ${chalk.cyan(cmd.name)}${desc}`);
386
+ }
387
+ }
388
+ if (projectCommands.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
389
+ console.log(` ${chalk.gray('Project:')}`);
390
+ for (const cmd of projectCommands) {
391
+ const desc = cmd.description ? ` - ${chalk.gray(cmd.description)}` : '';
392
+ console.log(` ${chalk.yellow(cmd.name)}${desc}`);
393
+ }
245
394
  }
246
395
  }
247
396
  console.log();
248
397
  }
249
398
  });
250
- skillsCmd
399
+ commandsCmd
251
400
  .command('add <source>')
252
- .description('Add skill from Git repo or local path')
401
+ .description('Add commands from Git repo or local path')
253
402
  .option('-a, --agents <list>', 'Comma-separated agents to install to')
254
403
  .action(async (source, options) => {
255
- const spinner = ora('Fetching skill...').start();
404
+ const spinner = ora('Fetching commands...').start();
256
405
  try {
257
406
  const { localPath } = await cloneRepo(source);
258
- const skills = discoverSkills(localPath);
259
- spinner.succeed(`Found ${skills.length} skills`);
407
+ const commands = discoverCommands(localPath);
408
+ spinner.succeed(`Found ${commands.length} commands`);
260
409
  const agents = options.agents
261
410
  ? options.agents.split(',')
262
411
  : ['claude', 'codex', 'gemini'];
263
- for (const skill of skills) {
264
- console.log(`\n ${chalk.cyan(skill.name)}: ${skill.description}`);
412
+ for (const command of commands) {
413
+ console.log(`\n ${chalk.cyan(command.name)}: ${command.description}`);
265
414
  for (const agentId of agents) {
266
415
  if (!isCliInstalled(agentId) && agentId !== 'cursor')
267
416
  continue;
268
- const sourcePath = resolveSkillSource(localPath, skill.name, agentId);
417
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
269
418
  if (sourcePath) {
270
- installSkill(sourcePath, agentId, skill.name, 'symlink');
419
+ installCommand(sourcePath, agentId, command.name, 'symlink');
271
420
  console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
272
421
  }
273
422
  }
274
423
  }
275
- console.log(chalk.green('\nSkills installed.'));
424
+ console.log(chalk.green('\nCommands installed.'));
276
425
  }
277
426
  catch (err) {
278
- spinner.fail('Failed to add skill');
427
+ spinner.fail('Failed to add commands');
279
428
  console.error(chalk.red(err.message));
280
429
  process.exit(1);
281
430
  }
282
431
  });
283
- skillsCmd
432
+ commandsCmd
284
433
  .command('remove <name>')
285
- .description('Remove a skill from all agents')
434
+ .description('Remove a command from all agents')
286
435
  .option('-a, --agents <list>', 'Comma-separated agents to remove from')
287
436
  .action((name, options) => {
288
437
  const agents = options.agents
@@ -290,18 +439,389 @@ skillsCmd
290
439
  : ALL_AGENT_IDS;
291
440
  let removed = 0;
292
441
  for (const agentId of agents) {
293
- if (uninstallSkill(agentId, name)) {
442
+ if (uninstallCommand(agentId, name)) {
294
443
  console.log(` ${chalk.red('-')} ${AGENTS[agentId].name}`);
295
444
  removed++;
296
445
  }
297
446
  }
298
447
  if (removed === 0) {
299
- console.log(chalk.yellow(`Skill '${name}' not found`));
448
+ console.log(chalk.yellow(`Command '${name}' not found`));
449
+ }
450
+ else {
451
+ console.log(chalk.green(`\nRemoved from ${removed} agents.`));
452
+ }
453
+ });
454
+ commandsCmd
455
+ .command('push <name>')
456
+ .description('Save project-scoped command to user scope')
457
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
458
+ .action((name, options) => {
459
+ const cwd = process.cwd();
460
+ const agents = options.agents
461
+ ? options.agents.split(',')
462
+ : ALL_AGENT_IDS;
463
+ let pushed = 0;
464
+ for (const agentId of agents) {
465
+ if (!isCliInstalled(agentId) && agentId !== 'cursor')
466
+ continue;
467
+ const result = promoteCommandToUser(agentId, name, cwd);
468
+ if (result.success) {
469
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
470
+ pushed++;
471
+ }
472
+ else if (result.error && !result.error.includes('not found')) {
473
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
474
+ }
475
+ }
476
+ if (pushed === 0) {
477
+ console.log(chalk.yellow(`Project command '${name}' not found for any agent`));
478
+ }
479
+ else {
480
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
481
+ }
482
+ });
483
+ const hooksCmd = program.command('hooks').description('Manage hooks');
484
+ hooksCmd
485
+ .command('list')
486
+ .description('List installed hooks')
487
+ .option('-a, --agent <agent>', 'Filter by agent')
488
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
489
+ .action((options) => {
490
+ console.log(chalk.bold('\nInstalled Hooks\n'));
491
+ const cwd = process.cwd();
492
+ const agents = options.agent
493
+ ? [options.agent]
494
+ : Array.from(HOOKS_CAPABLE_AGENTS);
495
+ for (const agentId of agents) {
496
+ const agent = AGENTS[agentId];
497
+ if (!agent.supportsHooks) {
498
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('hooks not supported')}`);
499
+ console.log();
500
+ continue;
501
+ }
502
+ let hooks = listInstalledHooksWithScope(agentId, cwd);
503
+ if (options.scope !== 'all') {
504
+ hooks = hooks.filter((h) => h.scope === options.scope);
505
+ }
506
+ if (hooks.length === 0) {
507
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
508
+ }
509
+ else {
510
+ console.log(` ${chalk.bold(agent.name)}:`);
511
+ const userHooks = hooks.filter((h) => h.scope === 'user');
512
+ const projectHooks = hooks.filter((h) => h.scope === 'project');
513
+ if (userHooks.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
514
+ console.log(` ${chalk.gray('User:')}`);
515
+ for (const hook of userHooks) {
516
+ console.log(` ${chalk.cyan(hook.name)}`);
517
+ }
518
+ }
519
+ if (projectHooks.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
520
+ console.log(` ${chalk.gray('Project:')}`);
521
+ for (const hook of projectHooks) {
522
+ console.log(` ${chalk.yellow(hook.name)}`);
523
+ }
524
+ }
525
+ }
526
+ console.log();
527
+ }
528
+ });
529
+ hooksCmd
530
+ .command('add <source>')
531
+ .description('Install hooks from git repo or local path')
532
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)', 'claude,gemini')
533
+ .action(async (source, options) => {
534
+ const spinner = ora('Fetching hooks...').start();
535
+ try {
536
+ const { localPath } = await cloneRepo(source);
537
+ const hooks = discoverHooksFromRepo(localPath);
538
+ const hookNames = new Set();
539
+ for (const name of hooks.shared) {
540
+ hookNames.add(name);
541
+ }
542
+ for (const list of Object.values(hooks.agentSpecific)) {
543
+ for (const name of list) {
544
+ hookNames.add(name);
545
+ }
546
+ }
547
+ spinner.succeed(`Found ${hookNames.size} hooks`);
548
+ const agents = options.agent
549
+ ? options.agent.split(',')
550
+ : ['claude', 'gemini'];
551
+ const result = await installHooks(localPath, agents, { scope: 'user' });
552
+ const installedByHook = new Map();
553
+ for (const item of result.installed) {
554
+ const [name, agentId] = item.split(':');
555
+ const list = installedByHook.get(name) || [];
556
+ list.push(agentId);
557
+ installedByHook.set(name, list);
558
+ }
559
+ const orderedHooks = Array.from(installedByHook.keys()).sort((a, b) => a.localeCompare(b));
560
+ for (const name of orderedHooks) {
561
+ console.log(`\n ${chalk.cyan(name)}`);
562
+ const agentIds = installedByHook.get(name) || [];
563
+ agentIds.sort();
564
+ for (const agentId of agentIds) {
565
+ console.log(` ${AGENTS[agentId].name}`);
566
+ }
567
+ }
568
+ if (result.errors.length > 0) {
569
+ console.log(chalk.red('\nErrors:'));
570
+ for (const error of result.errors) {
571
+ console.log(chalk.red(` ${error}`));
572
+ }
573
+ }
574
+ if (result.installed.length === 0) {
575
+ console.log(chalk.yellow('\nNo hooks installed.'));
576
+ }
577
+ else {
578
+ console.log(chalk.green('\nHooks installed.'));
579
+ }
580
+ }
581
+ catch (err) {
582
+ spinner.fail('Failed to add hooks');
583
+ console.error(chalk.red(err.message));
584
+ process.exit(1);
585
+ }
586
+ });
587
+ hooksCmd
588
+ .command('remove <name>')
589
+ .description('Remove a hook')
590
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)')
591
+ .action(async (name, options) => {
592
+ const agents = options.agent
593
+ ? options.agent.split(',')
594
+ : Array.from(HOOKS_CAPABLE_AGENTS);
595
+ const result = await removeHook(name, agents);
596
+ let removed = 0;
597
+ for (const item of result.removed) {
598
+ const [, agentId] = item.split(':');
599
+ console.log(` ${AGENTS[agentId].name}`);
600
+ removed++;
601
+ }
602
+ if (result.errors.length > 0) {
603
+ console.log(chalk.red('\nErrors:'));
604
+ for (const error of result.errors) {
605
+ console.log(chalk.red(` ${error}`));
606
+ }
607
+ }
608
+ if (removed === 0) {
609
+ console.log(chalk.yellow(`Hook '${name}' not found`));
300
610
  }
301
611
  else {
302
612
  console.log(chalk.green(`\nRemoved from ${removed} agents.`));
303
613
  }
304
614
  });
615
+ hooksCmd
616
+ .command('push <name>')
617
+ .description('Copy project-scoped hook to user scope')
618
+ .option('-a, --agent <agents>', 'Target agents (comma-separated)')
619
+ .action((name, options) => {
620
+ const cwd = process.cwd();
621
+ const agents = options.agent
622
+ ? options.agent.split(',')
623
+ : Array.from(HOOKS_CAPABLE_AGENTS);
624
+ let pushed = 0;
625
+ for (const agentId of agents) {
626
+ const result = promoteHookToUser(agentId, name, cwd);
627
+ if (result.success) {
628
+ console.log(` ${AGENTS[agentId].name}`);
629
+ pushed++;
630
+ }
631
+ else if (result.error && !result.error.includes('not found')) {
632
+ console.log(` ${AGENTS[agentId].name}: ${result.error}`);
633
+ }
634
+ }
635
+ if (pushed === 0) {
636
+ console.log(chalk.yellow(`Project hook '${name}' not found for any agent`));
637
+ }
638
+ else {
639
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
640
+ }
641
+ });
642
+ // =============================================================================
643
+ // SKILLS COMMANDS (Agent Skills)
644
+ // =============================================================================
645
+ const skillsCmd = program
646
+ .command('skills')
647
+ .description('Manage Agent Skills (SKILL.md + rules/)');
648
+ skillsCmd
649
+ .command('list')
650
+ .description('List installed Agent Skills')
651
+ .option('-a, --agent <agent>', 'Filter by agent')
652
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
653
+ .action((options) => {
654
+ console.log(chalk.bold('\nInstalled Agent Skills\n'));
655
+ const cwd = process.cwd();
656
+ const agents = options.agent
657
+ ? [options.agent]
658
+ : SKILLS_CAPABLE_AGENTS;
659
+ for (const agentId of agents) {
660
+ const agent = AGENTS[agentId];
661
+ if (!agent.capabilities.skills) {
662
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('skills not supported')}`);
663
+ console.log();
664
+ continue;
665
+ }
666
+ let skills = listInstalledSkillsWithScope(agentId, cwd);
667
+ if (options.scope !== 'all') {
668
+ skills = skills.filter((s) => s.scope === options.scope);
669
+ }
670
+ if (skills.length === 0) {
671
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
672
+ }
673
+ else {
674
+ console.log(` ${chalk.bold(agent.name)}:`);
675
+ const userSkills = skills.filter((s) => s.scope === 'user');
676
+ const projectSkills = skills.filter((s) => s.scope === 'project');
677
+ if (userSkills.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
678
+ console.log(` ${chalk.gray('User:')}`);
679
+ for (const skill of userSkills) {
680
+ const desc = skill.metadata.description ? ` - ${chalk.gray(skill.metadata.description)}` : '';
681
+ const ruleInfo = skill.ruleCount > 0 ? chalk.gray(` (${skill.ruleCount} rules)`) : '';
682
+ console.log(` ${chalk.cyan(skill.name)}${desc}${ruleInfo}`);
683
+ }
684
+ }
685
+ if (projectSkills.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
686
+ console.log(` ${chalk.gray('Project:')}`);
687
+ for (const skill of projectSkills) {
688
+ const desc = skill.metadata.description ? ` - ${chalk.gray(skill.metadata.description)}` : '';
689
+ const ruleInfo = skill.ruleCount > 0 ? chalk.gray(` (${skill.ruleCount} rules)`) : '';
690
+ console.log(` ${chalk.yellow(skill.name)}${desc}${ruleInfo}`);
691
+ }
692
+ }
693
+ }
694
+ console.log();
695
+ }
696
+ });
697
+ skillsCmd
698
+ .command('add <source>')
699
+ .description('Add Agent Skills from Git repo or local path')
700
+ .option('-a, --agents <list>', 'Comma-separated agents to install to')
701
+ .action(async (source, options) => {
702
+ const spinner = ora('Fetching skills...').start();
703
+ try {
704
+ const { localPath } = await cloneRepo(source);
705
+ const skills = discoverSkillsFromRepo(localPath);
706
+ spinner.succeed(`Found ${skills.length} skills`);
707
+ if (skills.length === 0) {
708
+ console.log(chalk.yellow('No skills found (looking for SKILL.md files)'));
709
+ return;
710
+ }
711
+ for (const skill of skills) {
712
+ console.log(`\n ${chalk.cyan(skill.name)}: ${skill.metadata.description || 'no description'}`);
713
+ if (skill.ruleCount > 0) {
714
+ console.log(` ${chalk.gray(`${skill.ruleCount} rules`)}`);
715
+ }
716
+ }
717
+ const agents = options.agents
718
+ ? options.agents.split(',')
719
+ : await checkbox({
720
+ message: 'Select agents to install skills to:',
721
+ choices: SKILLS_CAPABLE_AGENTS.filter((id) => isCliInstalled(id) || id === 'cursor').map((id) => ({
722
+ name: AGENTS[id].name,
723
+ value: id,
724
+ checked: ['claude', 'codex', 'gemini'].includes(id),
725
+ })),
726
+ });
727
+ if (agents.length === 0) {
728
+ console.log(chalk.yellow('\nNo agents selected.'));
729
+ return;
730
+ }
731
+ const installSpinner = ora('Installing skills...').start();
732
+ let installed = 0;
733
+ for (const skill of skills) {
734
+ const result = installSkill(skill.path, skill.name, agents);
735
+ if (result.success) {
736
+ installed++;
737
+ }
738
+ else {
739
+ console.log(chalk.red(`\n Failed to install ${skill.name}: ${result.error}`));
740
+ }
741
+ }
742
+ installSpinner.succeed(`Installed ${installed} skills to ${agents.length} agents`);
743
+ console.log(chalk.green('\nSkills installed.'));
744
+ }
745
+ catch (err) {
746
+ spinner.fail('Failed to add skills');
747
+ console.error(chalk.red(err.message));
748
+ process.exit(1);
749
+ }
750
+ });
751
+ skillsCmd
752
+ .command('remove <name>')
753
+ .description('Remove an Agent Skill')
754
+ .action((name) => {
755
+ const result = uninstallSkill(name);
756
+ if (result.success) {
757
+ console.log(chalk.green(`Removed skill '${name}'`));
758
+ }
759
+ else {
760
+ console.log(chalk.red(result.error || 'Failed to remove skill'));
761
+ }
762
+ });
763
+ skillsCmd
764
+ .command('push <name>')
765
+ .description('Save project-scoped skill to user scope')
766
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
767
+ .action((name, options) => {
768
+ const cwd = process.cwd();
769
+ const agents = options.agents
770
+ ? options.agents.split(',')
771
+ : SKILLS_CAPABLE_AGENTS;
772
+ let pushed = 0;
773
+ for (const agentId of agents) {
774
+ if (!AGENTS[agentId].capabilities.skills)
775
+ continue;
776
+ const result = promoteSkillToUser(agentId, name, cwd);
777
+ if (result.success) {
778
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
779
+ pushed++;
780
+ }
781
+ else if (result.error && !result.error.includes('not found')) {
782
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
783
+ }
784
+ }
785
+ if (pushed === 0) {
786
+ console.log(chalk.yellow(`Project skill '${name}' not found for any agent`));
787
+ }
788
+ else {
789
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
790
+ }
791
+ });
792
+ skillsCmd
793
+ .command('info <name>')
794
+ .description('Show detailed info about an installed skill')
795
+ .action((name) => {
796
+ const skill = getSkillInfo(name);
797
+ if (!skill) {
798
+ console.log(chalk.yellow(`Skill '${name}' not found`));
799
+ return;
800
+ }
801
+ console.log(chalk.bold(`\n${skill.metadata.name}\n`));
802
+ if (skill.metadata.description) {
803
+ console.log(` ${skill.metadata.description}`);
804
+ }
805
+ console.log();
806
+ if (skill.metadata.author) {
807
+ console.log(` Author: ${skill.metadata.author}`);
808
+ }
809
+ if (skill.metadata.version) {
810
+ console.log(` Version: ${skill.metadata.version}`);
811
+ }
812
+ if (skill.metadata.license) {
813
+ console.log(` License: ${skill.metadata.license}`);
814
+ }
815
+ console.log(` Path: ${skill.path}`);
816
+ const rules = getSkillRules(name);
817
+ if (rules.length > 0) {
818
+ console.log(chalk.bold(`\n Rules (${rules.length}):\n`));
819
+ for (const rule of rules) {
820
+ console.log(` ${chalk.cyan(rule)}`);
821
+ }
822
+ }
823
+ console.log();
824
+ });
305
825
  // =============================================================================
306
826
  // MCP COMMANDS
307
827
  // =============================================================================
@@ -311,40 +831,102 @@ const mcpCmd = program
311
831
  mcpCmd
312
832
  .command('list')
313
833
  .description('List MCP servers and registration status')
314
- .action(() => {
834
+ .option('-a, --agent <agent>', 'Filter by agent')
835
+ .option('-s, --scope <scope>', 'Filter by scope: user, project, or all', 'all')
836
+ .action((options) => {
315
837
  console.log(chalk.bold('\nMCP Servers\n'));
316
- for (const agentId of MCP_CAPABLE_AGENTS) {
838
+ const cwd = process.cwd();
839
+ const agents = options.agent
840
+ ? [options.agent]
841
+ : MCP_CAPABLE_AGENTS;
842
+ for (const agentId of agents) {
317
843
  const agent = AGENTS[agentId];
844
+ if (!agent.capabilities.mcp) {
845
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('mcp not supported')}`);
846
+ console.log();
847
+ continue;
848
+ }
318
849
  if (!isCliInstalled(agentId)) {
319
850
  console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('CLI not installed')}`);
320
851
  continue;
321
852
  }
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')}`);
853
+ let mcps = listInstalledMcpsWithScope(agentId, cwd);
854
+ if (options.scope !== 'all') {
855
+ mcps = mcps.filter((m) => m.scope === options.scope);
856
+ }
857
+ if (mcps.length === 0) {
858
+ console.log(` ${chalk.bold(agent.name)}: ${chalk.gray('none')}`);
859
+ }
860
+ else {
861
+ console.log(` ${chalk.bold(agent.name)}:`);
862
+ const userMcps = mcps.filter((m) => m.scope === 'user');
863
+ const projectMcps = mcps.filter((m) => m.scope === 'project');
864
+ if (userMcps.length > 0 && (options.scope === 'all' || options.scope === 'user')) {
865
+ console.log(` ${chalk.gray('User:')}`);
866
+ for (const mcp of userMcps) {
867
+ console.log(` ${chalk.cyan(mcp.name)}`);
868
+ }
869
+ }
870
+ if (projectMcps.length > 0 && (options.scope === 'all' || options.scope === 'project')) {
871
+ console.log(` ${chalk.gray('Project:')}`);
872
+ for (const mcp of projectMcps) {
873
+ console.log(` ${chalk.yellow(mcp.name)}`);
874
+ }
875
+ }
876
+ }
325
877
  console.log();
326
878
  }
327
879
  });
328
880
  mcpCmd
329
- .command('add <name> <command>')
330
- .description('Add MCP server to manifest')
331
- .option('-a, --agents <list>', 'Comma-separated agents', 'claude,codex,gemini')
881
+ .command('add <name> [command_or_url...]')
882
+ .description('Add MCP server (stdio: use -- before command, http: use URL)')
883
+ .option('-a, --agents <list>', 'Comma-separated agents', MCP_CAPABLE_AGENTS.join(','))
332
884
  .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;
885
+ .option('-t, --transport <type>', 'Transport: stdio or http', 'stdio')
886
+ .option('-H, --header <header>', 'HTTP header (name:value), can be repeated', (val, acc) => {
887
+ acc.push(val);
888
+ return acc;
889
+ }, [])
890
+ .action(async (name, commandOrUrl, options) => {
891
+ const transport = options.transport;
892
+ if (commandOrUrl.length === 0) {
893
+ console.error(chalk.red('Error: Command or URL required'));
894
+ console.log(chalk.gray('Stdio: agents mcp add <name> -- <command...>'));
895
+ console.log(chalk.gray('HTTP: agents mcp add <name> <url> --transport http'));
896
+ process.exit(1);
338
897
  }
339
- const localPath = getRepoLocalPath(state.source);
898
+ const source = await ensureSource();
899
+ const localPath = getRepoLocalPath(source);
340
900
  const manifest = readManifest(localPath) || createDefaultManifest();
341
901
  manifest.mcp = manifest.mcp || {};
342
- manifest.mcp[name] = {
343
- command,
344
- transport: 'stdio',
345
- scope: options.scope,
346
- agents: options.agents.split(','),
347
- };
902
+ if (transport === 'http') {
903
+ const url = commandOrUrl[0];
904
+ const headers = {};
905
+ if (options.header && options.header.length > 0) {
906
+ for (const h of options.header) {
907
+ const [key, ...valueParts] = h.split(':');
908
+ if (key && valueParts.length > 0) {
909
+ headers[key.trim()] = valueParts.join(':').trim();
910
+ }
911
+ }
912
+ }
913
+ manifest.mcp[name] = {
914
+ url,
915
+ transport: 'http',
916
+ scope: options.scope,
917
+ agents: options.agents.split(','),
918
+ ...(Object.keys(headers).length > 0 && { headers }),
919
+ };
920
+ }
921
+ else {
922
+ const command = commandOrUrl.join(' ');
923
+ manifest.mcp[name] = {
924
+ command,
925
+ transport: 'stdio',
926
+ scope: options.scope,
927
+ agents: options.agents.split(','),
928
+ };
929
+ }
348
930
  writeManifest(localPath, manifest);
349
931
  console.log(chalk.green(`Added MCP server '${name}' to manifest`));
350
932
  console.log(chalk.gray('Run: agents mcp register to apply'));
@@ -378,20 +960,21 @@ mcpCmd
378
960
  .command('register [name]')
379
961
  .description('Register MCP server(s) with agent CLIs')
380
962
  .option('-a, --agents <list>', 'Comma-separated agents')
381
- .action((name, options) => {
382
- const state = readState();
963
+ .action(async (name, options) => {
383
964
  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);
965
+ const source = await ensureSource();
966
+ const localPath = getRepoLocalPath(source);
389
967
  const manifest = readManifest(localPath);
390
968
  if (!manifest?.mcp) {
391
969
  console.log(chalk.yellow('No MCP servers in manifest'));
392
970
  return;
393
971
  }
394
972
  for (const [mcpName, config] of Object.entries(manifest.mcp)) {
973
+ // Skip HTTP transport MCPs for now (need different registration)
974
+ if (config.transport === 'http' || !config.command) {
975
+ console.log(`\n ${chalk.cyan(mcpName)}: ${chalk.yellow('HTTP transport not yet supported')}`);
976
+ continue;
977
+ }
395
978
  console.log(`\n ${chalk.cyan(mcpName)}:`);
396
979
  for (const agentId of config.agents) {
397
980
  if (!isCliInstalled(agentId))
@@ -409,6 +992,35 @@ mcpCmd
409
992
  }
410
993
  console.log(chalk.yellow('Single MCP registration not yet implemented'));
411
994
  });
995
+ mcpCmd
996
+ .command('push <name>')
997
+ .description('Save project-scoped MCP to user scope')
998
+ .option('-a, --agents <list>', 'Comma-separated agents to push for')
999
+ .action((name, options) => {
1000
+ const cwd = process.cwd();
1001
+ const agents = options.agents
1002
+ ? options.agents.split(',')
1003
+ : MCP_CAPABLE_AGENTS;
1004
+ let pushed = 0;
1005
+ for (const agentId of agents) {
1006
+ if (!isCliInstalled(agentId))
1007
+ continue;
1008
+ const result = promoteMcpToUser(agentId, name, cwd);
1009
+ if (result.success) {
1010
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1011
+ pushed++;
1012
+ }
1013
+ else if (result.error && !result.error.includes('not found')) {
1014
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
1015
+ }
1016
+ }
1017
+ if (pushed === 0) {
1018
+ console.log(chalk.yellow(`Project MCP '${name}' not found for any agent`));
1019
+ }
1020
+ else {
1021
+ console.log(chalk.green(`\nPushed to user scope for ${pushed} agents.`));
1022
+ }
1023
+ });
412
1024
  // =============================================================================
413
1025
  // CLI COMMANDS
414
1026
  // =============================================================================
@@ -440,19 +1052,15 @@ cliCmd
440
1052
  .command('add <agent>')
441
1053
  .description('Add agent CLI to manifest')
442
1054
  .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
- }
1055
+ .action(async (agent, options) => {
449
1056
  const agentId = agent.toLowerCase();
450
1057
  if (!AGENTS[agentId]) {
451
1058
  console.log(chalk.red(`Unknown agent: ${agent}`));
452
1059
  console.log(chalk.gray(`Available: ${ALL_AGENT_IDS.join(', ')}`));
453
1060
  return;
454
1061
  }
455
- const localPath = getRepoLocalPath(state.source);
1062
+ const source = await ensureSource();
1063
+ const localPath = getRepoLocalPath(source);
456
1064
  const manifest = readManifest(localPath) || createDefaultManifest();
457
1065
  manifest.clis = manifest.clis || {};
458
1066
  manifest.clis[agentId] = {
@@ -465,14 +1073,10 @@ cliCmd
465
1073
  cliCmd
466
1074
  .command('remove <agent>')
467
1075
  .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
- }
1076
+ .action(async (agent) => {
1077
+ const source = await ensureSource();
474
1078
  const agentId = agent.toLowerCase();
475
- const localPath = getRepoLocalPath(state.source);
1079
+ const localPath = getRepoLocalPath(source);
476
1080
  const manifest = readManifest(localPath);
477
1081
  if (manifest?.clis?.[agentId]) {
478
1082
  delete manifest.clis[agentId];
@@ -486,10 +1090,12 @@ cliCmd
486
1090
  cliCmd
487
1091
  .command('upgrade [agent]')
488
1092
  .description('Upgrade agent CLI(s) to version in manifest')
1093
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
489
1094
  .option('--latest', 'Upgrade to latest version (ignore manifest)')
490
1095
  .action(async (agent, options) => {
491
- const state = readState();
492
- const localPath = state.source ? getRepoLocalPath(state.source) : null;
1096
+ const scopeName = options.scope;
1097
+ const scope = getScope(scopeName);
1098
+ const localPath = scope ? getRepoLocalPath(scope.source) : null;
493
1099
  const manifest = localPath ? readManifest(localPath) : null;
494
1100
  const agentsToUpgrade = agent
495
1101
  ? [agent.toLowerCase()]
@@ -520,6 +1126,145 @@ cliCmd
520
1126
  }
521
1127
  });
522
1128
  // =============================================================================
1129
+ // REPO COMMANDS
1130
+ // =============================================================================
1131
+ const repoCmd = program
1132
+ .command('repo')
1133
+ .description('Manage .agents repository scopes');
1134
+ repoCmd
1135
+ .command('list')
1136
+ .description('List configured repository scopes')
1137
+ .action(() => {
1138
+ const scopes = getScopesByPriority();
1139
+ if (scopes.length === 0) {
1140
+ console.log(chalk.yellow('\nNo scopes configured.'));
1141
+ console.log(chalk.gray(' Run: agents repo add <source>'));
1142
+ console.log();
1143
+ return;
1144
+ }
1145
+ console.log(chalk.bold('\nConfigured Scopes\n'));
1146
+ console.log(chalk.gray(' Scopes are applied in priority order (higher overrides lower)\n'));
1147
+ for (const { name, config } of scopes) {
1148
+ const readonlyTag = config.readonly ? chalk.gray(' (readonly)') : '';
1149
+ console.log(` ${chalk.bold(name)}${readonlyTag}`);
1150
+ console.log(` Source: ${config.source}`);
1151
+ console.log(` Branch: ${config.branch}`);
1152
+ console.log(` Commit: ${config.commit.substring(0, 8)}`);
1153
+ console.log(` Priority: ${config.priority}`);
1154
+ console.log(` Synced: ${new Date(config.lastSync).toLocaleString()}`);
1155
+ console.log();
1156
+ }
1157
+ });
1158
+ repoCmd
1159
+ .command('add <source>')
1160
+ .description('Add a repository scope')
1161
+ .option('-s, --scope <scope>', 'Target scope (default: user)', 'user')
1162
+ .option('-y, --yes', 'Skip confirmation prompts')
1163
+ .action(async (source, options) => {
1164
+ const scopeName = options.scope;
1165
+ const existingScope = getScope(scopeName);
1166
+ if (existingScope && !options.yes) {
1167
+ const shouldOverwrite = await confirm({
1168
+ message: `Scope '${scopeName}' already exists (${existingScope.source}). Overwrite?`,
1169
+ default: false,
1170
+ });
1171
+ if (!shouldOverwrite) {
1172
+ console.log(chalk.yellow('Cancelled.'));
1173
+ return;
1174
+ }
1175
+ }
1176
+ if (existingScope?.readonly && !options.yes) {
1177
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot overwrite.`));
1178
+ return;
1179
+ }
1180
+ const parsed = parseSource(source);
1181
+ const spinner = ora(`Cloning repository for ${scopeName} scope...`).start();
1182
+ try {
1183
+ const { commit, isNew } = await cloneRepo(source);
1184
+ spinner.succeed(isNew ? 'Repository cloned' : 'Repository updated');
1185
+ const priority = getScopePriority(scopeName);
1186
+ setScope(scopeName, {
1187
+ source,
1188
+ branch: parsed.ref || 'main',
1189
+ commit,
1190
+ lastSync: new Date().toISOString(),
1191
+ priority,
1192
+ readonly: scopeName === 'system',
1193
+ });
1194
+ console.log(chalk.green(`\nAdded scope '${scopeName}' with priority ${priority}`));
1195
+ const scopeHint = scopeName === 'user' ? '' : ` --scope ${scopeName}`;
1196
+ console.log(chalk.gray(` Run: agents pull${scopeHint} to sync commands`));
1197
+ }
1198
+ catch (err) {
1199
+ spinner.fail('Failed to add scope');
1200
+ console.error(chalk.red(err.message));
1201
+ process.exit(1);
1202
+ }
1203
+ });
1204
+ repoCmd
1205
+ .command('remove <scope>')
1206
+ .description('Remove a repository scope')
1207
+ .option('-y, --yes', 'Skip confirmation prompts')
1208
+ .action(async (scopeName, options) => {
1209
+ const existingScope = getScope(scopeName);
1210
+ if (!existingScope) {
1211
+ console.log(chalk.yellow(`Scope '${scopeName}' not found.`));
1212
+ return;
1213
+ }
1214
+ if (existingScope.readonly) {
1215
+ console.log(chalk.red(`Scope '${scopeName}' is readonly. Cannot remove.`));
1216
+ return;
1217
+ }
1218
+ if (!options.yes) {
1219
+ const shouldRemove = await confirm({
1220
+ message: `Remove scope '${scopeName}' (${existingScope.source})?`,
1221
+ default: false,
1222
+ });
1223
+ if (!shouldRemove) {
1224
+ console.log(chalk.yellow('Cancelled.'));
1225
+ return;
1226
+ }
1227
+ }
1228
+ const removed = removeScope(scopeName);
1229
+ if (removed) {
1230
+ console.log(chalk.green(`Removed scope '${scopeName}'`));
1231
+ }
1232
+ else {
1233
+ console.log(chalk.yellow(`Failed to remove scope '${scopeName}'`));
1234
+ }
1235
+ });
1236
+ repoCmd
1237
+ .command('sync [scope]')
1238
+ .description('Sync a specific scope or all scopes')
1239
+ .option('-y, --yes', 'Skip confirmation prompts')
1240
+ .action(async (scopeName, options) => {
1241
+ const scopes = scopeName ? [{ name: scopeName, config: getScope(scopeName) }].filter(s => s.config) : getScopesByPriority();
1242
+ if (scopes.length === 0) {
1243
+ console.log(chalk.yellow('No scopes to sync.'));
1244
+ return;
1245
+ }
1246
+ for (const { name, config } of scopes) {
1247
+ if (!config)
1248
+ continue;
1249
+ console.log(chalk.bold(`\nSyncing scope: ${name}`));
1250
+ const spinner = ora('Updating repository...').start();
1251
+ try {
1252
+ const { commit } = await cloneRepo(config.source);
1253
+ spinner.succeed('Repository updated');
1254
+ setScope(name, {
1255
+ ...config,
1256
+ commit,
1257
+ lastSync: new Date().toISOString(),
1258
+ });
1259
+ }
1260
+ catch (err) {
1261
+ spinner.fail(`Failed to sync ${name}`);
1262
+ console.error(chalk.gray(err.message));
1263
+ }
1264
+ }
1265
+ console.log(chalk.green('\nSync complete.'));
1266
+ });
1267
+ // =============================================================================
523
1268
  // INIT COMMAND
524
1269
  // =============================================================================
525
1270
  program
@@ -544,5 +1289,341 @@ program
544
1289
  console.log(chalk.gray(' claude/hooks/'));
545
1290
  console.log();
546
1291
  });
1292
+ // =============================================================================
1293
+ // REGISTRY COMMANDS
1294
+ // =============================================================================
1295
+ const registryCmd = program
1296
+ .command('registry')
1297
+ .description('Manage package registries (MCP servers, skills)');
1298
+ registryCmd
1299
+ .command('list')
1300
+ .description('List configured registries')
1301
+ .option('-t, --type <type>', 'Filter by type: mcp or skill')
1302
+ .action((options) => {
1303
+ const types = options.type ? [options.type] : ['mcp', 'skill'];
1304
+ console.log(chalk.bold('\nConfigured Registries\n'));
1305
+ for (const type of types) {
1306
+ console.log(chalk.bold(` ${type.toUpperCase()}`));
1307
+ const registries = getRegistries(type);
1308
+ const entries = Object.entries(registries);
1309
+ if (entries.length === 0) {
1310
+ console.log(chalk.gray(' No registries configured'));
1311
+ }
1312
+ else {
1313
+ for (const [name, config] of entries) {
1314
+ const status = config.enabled ? chalk.green('enabled') : chalk.gray('disabled');
1315
+ const isDefault = DEFAULT_REGISTRIES[type]?.[name] ? chalk.gray(' (default)') : '';
1316
+ console.log(` ${name}${isDefault}: ${status}`);
1317
+ console.log(chalk.gray(` ${config.url}`));
1318
+ }
1319
+ }
1320
+ console.log();
1321
+ }
1322
+ });
1323
+ registryCmd
1324
+ .command('add <type> <name> <url>')
1325
+ .description('Add a registry (type: mcp or skill)')
1326
+ .option('--api-key <key>', 'API key for authentication')
1327
+ .action((type, name, url, options) => {
1328
+ if (type !== 'mcp' && type !== 'skill') {
1329
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1330
+ process.exit(1);
1331
+ }
1332
+ setRegistry(type, name, {
1333
+ url,
1334
+ enabled: true,
1335
+ apiKey: options.apiKey,
1336
+ });
1337
+ console.log(chalk.green(`Added ${type} registry '${name}'`));
1338
+ });
1339
+ registryCmd
1340
+ .command('remove <type> <name>')
1341
+ .description('Remove a registry')
1342
+ .action((type, name) => {
1343
+ if (type !== 'mcp' && type !== 'skill') {
1344
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1345
+ process.exit(1);
1346
+ }
1347
+ // Check if it's a default registry
1348
+ if (DEFAULT_REGISTRIES[type]?.[name]) {
1349
+ console.log(chalk.yellow(`Cannot remove default registry '${name}'. Use 'agents registry disable' instead.`));
1350
+ process.exit(1);
1351
+ }
1352
+ if (removeRegistry(type, name)) {
1353
+ console.log(chalk.green(`Removed ${type} registry '${name}'`));
1354
+ }
1355
+ else {
1356
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1357
+ }
1358
+ });
1359
+ registryCmd
1360
+ .command('enable <type> <name>')
1361
+ .description('Enable a registry')
1362
+ .action((type, name) => {
1363
+ if (type !== 'mcp' && type !== 'skill') {
1364
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1365
+ process.exit(1);
1366
+ }
1367
+ const registries = getRegistries(type);
1368
+ if (!registries[name]) {
1369
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1370
+ process.exit(1);
1371
+ }
1372
+ setRegistry(type, name, { enabled: true });
1373
+ console.log(chalk.green(`Enabled ${type} registry '${name}'`));
1374
+ });
1375
+ registryCmd
1376
+ .command('disable <type> <name>')
1377
+ .description('Disable a registry')
1378
+ .action((type, name) => {
1379
+ if (type !== 'mcp' && type !== 'skill') {
1380
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1381
+ process.exit(1);
1382
+ }
1383
+ const registries = getRegistries(type);
1384
+ if (!registries[name]) {
1385
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1386
+ process.exit(1);
1387
+ }
1388
+ setRegistry(type, name, { enabled: false });
1389
+ console.log(chalk.green(`Disabled ${type} registry '${name}'`));
1390
+ });
1391
+ registryCmd
1392
+ .command('config <type> <name>')
1393
+ .description('Configure a registry')
1394
+ .option('--api-key <key>', 'Set API key')
1395
+ .option('--url <url>', 'Update URL')
1396
+ .action((type, name, options) => {
1397
+ if (type !== 'mcp' && type !== 'skill') {
1398
+ console.log(chalk.red(`Invalid type '${type}'. Use 'mcp' or 'skill'.`));
1399
+ process.exit(1);
1400
+ }
1401
+ const registries = getRegistries(type);
1402
+ if (!registries[name]) {
1403
+ console.log(chalk.yellow(`Registry '${name}' not found`));
1404
+ process.exit(1);
1405
+ }
1406
+ const updates = {};
1407
+ if (options.apiKey)
1408
+ updates.apiKey = options.apiKey;
1409
+ if (options.url)
1410
+ updates.url = options.url;
1411
+ if (Object.keys(updates).length === 0) {
1412
+ console.log(chalk.yellow('No options provided. Use --api-key or --url.'));
1413
+ process.exit(1);
1414
+ }
1415
+ setRegistry(type, name, updates);
1416
+ console.log(chalk.green(`Updated ${type} registry '${name}'`));
1417
+ });
1418
+ // =============================================================================
1419
+ // SEARCH COMMAND
1420
+ // =============================================================================
1421
+ program
1422
+ .command('search <query>')
1423
+ .description('Search registries for packages (MCP servers, skills)')
1424
+ .option('-t, --type <type>', 'Filter by type: mcp or skill')
1425
+ .option('-r, --registry <name>', 'Search specific registry')
1426
+ .option('-l, --limit <n>', 'Max results', '20')
1427
+ .action(async (query, options) => {
1428
+ const spinner = ora('Searching registries...').start();
1429
+ try {
1430
+ const results = await searchRegistries(query, {
1431
+ type: options.type,
1432
+ registry: options.registry,
1433
+ limit: parseInt(options.limit, 10),
1434
+ });
1435
+ spinner.stop();
1436
+ if (results.length === 0) {
1437
+ console.log(chalk.yellow('\nNo packages found.'));
1438
+ if (!options.type) {
1439
+ console.log(chalk.gray('\nTip: skill registries not yet available. Use gh:user/repo for skills.'));
1440
+ }
1441
+ return;
1442
+ }
1443
+ console.log(chalk.bold(`\nFound ${results.length} packages\n`));
1444
+ // Group by type
1445
+ const mcpResults = results.filter((r) => r.type === 'mcp');
1446
+ const skillResults = results.filter((r) => r.type === 'skill');
1447
+ if (mcpResults.length > 0) {
1448
+ console.log(chalk.bold(' MCP Servers'));
1449
+ for (const result of mcpResults) {
1450
+ const desc = result.description
1451
+ ? chalk.gray(` - ${result.description.slice(0, 50)}${result.description.length > 50 ? '...' : ''}`)
1452
+ : '';
1453
+ console.log(` ${chalk.cyan(result.name)}${desc}`);
1454
+ console.log(chalk.gray(` Registry: ${result.registry} Install: agents add mcp:${result.name}`));
1455
+ }
1456
+ console.log();
1457
+ }
1458
+ if (skillResults.length > 0) {
1459
+ console.log(chalk.bold(' Skills'));
1460
+ for (const result of skillResults) {
1461
+ const desc = result.description
1462
+ ? chalk.gray(` - ${result.description.slice(0, 50)}${result.description.length > 50 ? '...' : ''}`)
1463
+ : '';
1464
+ console.log(` ${chalk.cyan(result.name)}${desc}`);
1465
+ console.log(chalk.gray(` Registry: ${result.registry} Install: agents add skill:${result.name}`));
1466
+ }
1467
+ console.log();
1468
+ }
1469
+ }
1470
+ catch (err) {
1471
+ spinner.fail('Search failed');
1472
+ console.error(chalk.red(err.message));
1473
+ process.exit(1);
1474
+ }
1475
+ });
1476
+ // =============================================================================
1477
+ // ADD COMMAND (unified package installation)
1478
+ // =============================================================================
1479
+ program
1480
+ .command('add <identifier>')
1481
+ .description('Add a package (mcp:name, skill:user/repo, or gh:user/repo)')
1482
+ .option('-a, --agents <list>', 'Comma-separated agents to install to')
1483
+ .action(async (identifier, options) => {
1484
+ const spinner = ora('Resolving package...').start();
1485
+ try {
1486
+ const resolved = await resolvePackage(identifier);
1487
+ if (!resolved) {
1488
+ spinner.fail('Package not found');
1489
+ console.log(chalk.gray('\nTip: Use explicit prefix (mcp:, skill:, gh:) or check the identifier.'));
1490
+ process.exit(1);
1491
+ }
1492
+ spinner.succeed(`Found ${resolved.type} package`);
1493
+ if (resolved.type === 'mcp') {
1494
+ // Install MCP server
1495
+ const entry = resolved.mcpEntry;
1496
+ if (!entry) {
1497
+ console.log(chalk.red('Failed to get MCP server details'));
1498
+ process.exit(1);
1499
+ }
1500
+ console.log(chalk.bold(`\n${entry.name}`));
1501
+ if (entry.description) {
1502
+ console.log(chalk.gray(` ${entry.description}`));
1503
+ }
1504
+ if (entry.repository?.url) {
1505
+ console.log(chalk.gray(` ${entry.repository.url}`));
1506
+ }
1507
+ // Get package info
1508
+ const pkg = entry.packages?.[0];
1509
+ if (!pkg) {
1510
+ console.log(chalk.yellow('\nNo installable package found for this server.'));
1511
+ console.log(chalk.gray('You may need to install it manually.'));
1512
+ process.exit(1);
1513
+ }
1514
+ console.log(chalk.bold('\nPackage:'));
1515
+ console.log(` Name: ${pkg.name || pkg.registry_name}`);
1516
+ console.log(` Runtime: ${pkg.runtime || 'unknown'}`);
1517
+ console.log(` Transport: ${pkg.transport || 'stdio'}`);
1518
+ if (pkg.packageArguments && pkg.packageArguments.length > 0) {
1519
+ console.log(chalk.bold('\nRequired arguments:'));
1520
+ for (const arg of pkg.packageArguments) {
1521
+ const req = arg.required ? chalk.red('*') : '';
1522
+ console.log(` ${arg.name}${req}: ${arg.description || ''}`);
1523
+ }
1524
+ }
1525
+ // Determine command based on runtime
1526
+ let command;
1527
+ if (pkg.runtime === 'node') {
1528
+ command = `npx -y ${pkg.name || pkg.registry_name}`;
1529
+ }
1530
+ else if (pkg.runtime === 'python') {
1531
+ command = `uvx ${pkg.name || pkg.registry_name}`;
1532
+ }
1533
+ else {
1534
+ command = pkg.name || pkg.registry_name;
1535
+ }
1536
+ const agents = options.agents
1537
+ ? options.agents.split(',')
1538
+ : MCP_CAPABLE_AGENTS.filter((id) => isCliInstalled(id));
1539
+ if (agents.length === 0) {
1540
+ console.log(chalk.yellow('\nNo MCP-capable agents installed.'));
1541
+ process.exit(1);
1542
+ }
1543
+ console.log(chalk.bold('\nInstalling to agents...'));
1544
+ for (const agentId of agents) {
1545
+ if (!isCliInstalled(agentId))
1546
+ continue;
1547
+ const result = registerMcp(agentId, entry.name, command, 'user');
1548
+ if (result.success) {
1549
+ console.log(` ${chalk.green('+')} ${AGENTS[agentId].name}`);
1550
+ }
1551
+ else {
1552
+ console.log(` ${chalk.red('x')} ${AGENTS[agentId].name}: ${result.error}`);
1553
+ }
1554
+ }
1555
+ console.log(chalk.green('\nMCP server installed.'));
1556
+ }
1557
+ else if (resolved.type === 'git' || resolved.type === 'skill') {
1558
+ // Install from git source (skills/commands/hooks)
1559
+ console.log(chalk.bold(`\nInstalling from ${resolved.source}`));
1560
+ const { localPath } = await cloneRepo(resolved.source);
1561
+ // Discover what's in the repo
1562
+ const commands = discoverCommands(localPath);
1563
+ const skills = discoverSkillsFromRepo(localPath);
1564
+ const hooks = discoverHooksFromRepo(localPath);
1565
+ const hasCommands = commands.length > 0;
1566
+ const hasSkills = skills.length > 0;
1567
+ const hasHooks = hooks.shared.length > 0 || Object.values(hooks.agentSpecific).some((h) => h.length > 0);
1568
+ if (!hasCommands && !hasSkills && !hasHooks) {
1569
+ console.log(chalk.yellow('No installable content found in repository.'));
1570
+ process.exit(1);
1571
+ }
1572
+ console.log(chalk.bold('\nFound:'));
1573
+ if (hasCommands)
1574
+ console.log(` ${commands.length} commands`);
1575
+ if (hasSkills)
1576
+ console.log(` ${skills.length} skills`);
1577
+ if (hasHooks)
1578
+ console.log(` ${hooks.shared.length + Object.values(hooks.agentSpecific).flat().length} hooks`);
1579
+ const agents = options.agents
1580
+ ? options.agents.split(',')
1581
+ : ['claude', 'codex', 'gemini'];
1582
+ // Install commands
1583
+ if (hasCommands) {
1584
+ console.log(chalk.bold('\nInstalling commands...'));
1585
+ let installed = 0;
1586
+ for (const command of commands) {
1587
+ for (const agentId of agents) {
1588
+ if (!isCliInstalled(agentId) && agentId !== 'cursor')
1589
+ continue;
1590
+ const sourcePath = resolveCommandSource(localPath, command.name, agentId);
1591
+ if (sourcePath) {
1592
+ installCommand(sourcePath, agentId, command.name, 'symlink');
1593
+ installed++;
1594
+ }
1595
+ }
1596
+ }
1597
+ console.log(` Installed ${installed} command instances`);
1598
+ }
1599
+ // Install skills
1600
+ if (hasSkills) {
1601
+ console.log(chalk.bold('\nInstalling skills...'));
1602
+ for (const skill of skills) {
1603
+ const result = installSkill(skill.path, skill.name, agents);
1604
+ if (result.success) {
1605
+ console.log(` ${chalk.green('+')} ${skill.name}`);
1606
+ }
1607
+ else {
1608
+ console.log(` ${chalk.red('x')} ${skill.name}: ${result.error}`);
1609
+ }
1610
+ }
1611
+ }
1612
+ // Install hooks
1613
+ if (hasHooks) {
1614
+ console.log(chalk.bold('\nInstalling hooks...'));
1615
+ const hookAgents = agents.filter((id) => AGENTS[id].supportsHooks);
1616
+ const result = await installHooks(localPath, hookAgents, { scope: 'user' });
1617
+ console.log(` Installed ${result.installed.length} hooks`);
1618
+ }
1619
+ console.log(chalk.green('\nPackage installed.'));
1620
+ }
1621
+ }
1622
+ catch (err) {
1623
+ spinner.fail('Installation failed');
1624
+ console.error(chalk.red(err.message));
1625
+ process.exit(1);
1626
+ }
1627
+ });
547
1628
  program.parse();
548
1629
  //# sourceMappingURL=index.js.map