agent-switchboard 0.1.27 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +26 -1
  2. package/dist/commands/distribution.js +2 -2
  3. package/dist/commands/distribution.js.map +1 -1
  4. package/dist/commands/library.d.ts +1 -1
  5. package/dist/commands/library.js +5 -5
  6. package/dist/commands/library.js.map +1 -1
  7. package/dist/config/application-config.d.ts +37 -0
  8. package/dist/config/application-config.js +83 -0
  9. package/dist/config/application-config.js.map +1 -0
  10. package/dist/config/layered-config.js +43 -2
  11. package/dist/config/layered-config.js.map +1 -1
  12. package/dist/config/paths.d.ts +7 -2
  13. package/dist/config/paths.js +12 -3
  14. package/dist/config/paths.js.map +1 -1
  15. package/dist/config/schemas.d.ts +103 -22
  16. package/dist/config/schemas.js +22 -18
  17. package/dist/config/schemas.js.map +1 -1
  18. package/dist/config/switchboard-config.d.ts +3 -6
  19. package/dist/config/switchboard-config.js +3 -6
  20. package/dist/config/switchboard-config.js.map +1 -1
  21. package/dist/hooks/distribution.d.ts +27 -0
  22. package/dist/hooks/distribution.js +235 -0
  23. package/dist/hooks/distribution.js.map +1 -0
  24. package/dist/hooks/library.d.ts +40 -0
  25. package/dist/hooks/library.js +143 -0
  26. package/dist/hooks/library.js.map +1 -0
  27. package/dist/hooks/schema.d.ts +542 -0
  28. package/dist/hooks/schema.js +49 -0
  29. package/dist/hooks/schema.js.map +1 -0
  30. package/dist/index.js +608 -146
  31. package/dist/index.js.map +1 -1
  32. package/dist/library/fs.d.ts +1 -3
  33. package/dist/library/fs.js +24 -7
  34. package/dist/library/fs.js.map +1 -1
  35. package/dist/library/schema.d.ts +1 -1
  36. package/dist/library/schema.js +1 -1
  37. package/dist/library/sources.d.ts +3 -0
  38. package/dist/library/sources.js +8 -2
  39. package/dist/library/sources.js.map +1 -1
  40. package/dist/library/state.d.ts +4 -5
  41. package/dist/library/state.js +15 -31
  42. package/dist/library/state.js.map +1 -1
  43. package/dist/marketplace/plugin-loader.d.ts +30 -0
  44. package/dist/marketplace/plugin-loader.js +178 -0
  45. package/dist/marketplace/plugin-loader.js.map +1 -0
  46. package/dist/marketplace/reader.d.ts +36 -0
  47. package/dist/marketplace/reader.js +90 -0
  48. package/dist/marketplace/reader.js.map +1 -0
  49. package/dist/marketplace/schemas.d.ts +467 -0
  50. package/dist/marketplace/schemas.js +57 -0
  51. package/dist/marketplace/schemas.js.map +1 -0
  52. package/dist/marketplace/source-loader.d.ts +32 -0
  53. package/dist/marketplace/source-loader.js +45 -0
  54. package/dist/marketplace/source-loader.js.map +1 -0
  55. package/dist/rules/composer.d.ts +2 -2
  56. package/dist/rules/composer.js +5 -5
  57. package/dist/rules/composer.js.map +1 -1
  58. package/dist/rules/distribution.js +2 -2
  59. package/dist/rules/distribution.js.map +1 -1
  60. package/dist/skills/distribution.js +6 -6
  61. package/dist/skills/distribution.js.map +1 -1
  62. package/dist/skills/library.d.ts +1 -2
  63. package/dist/skills/library.js +6 -6
  64. package/dist/skills/library.js.map +1 -1
  65. package/dist/subagents/distribution.d.ts +1 -1
  66. package/dist/subagents/distribution.js +319 -20
  67. package/dist/subagents/distribution.js.map +1 -1
  68. package/dist/subagents/importer.d.ts +1 -1
  69. package/dist/subagents/importer.js +61 -1
  70. package/dist/subagents/importer.js.map +1 -1
  71. package/dist/subagents/inventory.js +3 -3
  72. package/dist/subagents/inventory.js.map +1 -1
  73. package/dist/subagents/library.d.ts +2 -2
  74. package/dist/subagents/library.js +14 -19
  75. package/dist/subagents/library.js.map +1 -1
  76. package/dist/ui/hook-ui.d.ts +8 -0
  77. package/dist/ui/hook-ui.js +17 -0
  78. package/dist/ui/hook-ui.js.map +1 -0
  79. package/dist/ui/library-selector.d.ts +1 -1
  80. package/dist/ui/subagent-ui.js +3 -3
  81. package/dist/ui/subagent-ui.js.map +1 -1
  82. package/dist/util/cli.d.ts +20 -0
  83. package/dist/util/cli.js +107 -14
  84. package/dist/util/cli.js.map +1 -1
  85. package/package.json +3 -2
  86. package/dist/config/agent-config.d.ts +0 -35
  87. package/dist/config/agent-config.js +0 -88
  88. package/dist/config/agent-config.js.map +0 -1
package/dist/index.js CHANGED
@@ -14,13 +14,16 @@ import { getAgentById } from './agents/registry.js';
14
14
  import { distributeCommands } from './commands/distribution.js';
15
15
  import { importCommandFromFile } from './commands/importer.js';
16
16
  import { buildCommandInventory } from './commands/inventory.js';
17
- import { resolveAgentSectionConfig } from './config/agent-config.js';
17
+ import { resolveApplicationSectionConfig } from './config/application-config.js';
18
18
  import { loadMcpConfig, stripLegacyEnabledFlagsFromMcpJson } from './config/mcp-config.js';
19
- import { getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getOpencodePath, getSkillsDir, getSourceCacheDir, getSubagentsDir, } from './config/paths.js';
19
+ import { getAgentsDir, getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getHooksDir, getOpencodePath, getSkillsDir, getSourceCacheDir, } from './config/paths.js';
20
20
  import { loadSwitchboardConfig, loadSwitchboardConfigWithLayers, } from './config/switchboard-config.js';
21
+ import { distributeHooks } from './hooks/distribution.js';
22
+ import { loadHookLibrary } from './hooks/library.js';
21
23
  import { ensureLibraryDirectories, writeFileSecure } from './library/fs.js';
22
24
  import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
23
- import { loadMcpActiveState, saveMcpActiveState } from './library/state.js';
25
+ import { loadLibraryStateSection, loadMcpActiveState, saveMcpActiveState, } from './library/state.js';
26
+ import { readMarketplace } from './marketplace/reader.js';
24
27
  import { RULE_SUPPORTED_AGENTS } from './rules/agents.js';
25
28
  import { composeActiveRules } from './rules/composer.js';
26
29
  import { distributeRules, listIndirectAgents, listPerFileAgents, listUnsupportedAgents, } from './rules/distribution.js';
@@ -34,17 +37,18 @@ import { distributeSubagents } from './subagents/distribution.js';
34
37
  import { importSubagentFromFile } from './subagents/importer.js';
35
38
  import { buildSubagentInventory } from './subagents/inventory.js';
36
39
  import { showCommandSelector } from './ui/command-ui.js';
40
+ import { showHookSelector } from './ui/hook-ui.js';
37
41
  import { showMcpServerUI } from './ui/mcp-ui.js';
38
42
  import { showRuleSelector } from './ui/rule-ui.js';
39
43
  import { showSkillSelector } from './ui/skill-ui.js';
40
44
  import { showSubagentSelector } from './ui/subagent-ui.js';
41
- import { printActiveSelection, printAgentSyncStatus, printDistributionResults, printTable, } from './util/cli.js';
45
+ import { printActiveSelection, printAgentSyncStatus, printCompactDistributions, printDistributionResults, printTable, shortenPath, } from './util/cli.js';
42
46
  const program = new Command();
43
47
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
48
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
45
49
  program
46
50
  .name('asb')
47
- .description('Manage MCP servers, rules, commands, subagents, and skills across AI coding agents')
51
+ .description('Manage MCP servers, rules, commands, agents, skills, and hooks across AI coding agents')
48
52
  .version(packageJson.version)
49
53
  .addHelpText('after', `
50
54
  Examples:
@@ -55,11 +59,11 @@ Examples:
55
59
 
56
60
  Alias: agent-switchboard
57
61
  Config: ~/.agent-switchboard/config.toml`);
58
- // Initialize library directories for commands/subagents (secure permissions)
62
+ // Initialize library directories for commands/agents (secure permissions)
59
63
  ensureLibraryDirectories();
60
64
  program
61
65
  .command('sync')
62
- .description('Synchronize active MCP servers, rules, commands, subagents, and skills to agent targets')
66
+ .description('Synchronize active MCP servers, rules, commands, agents, and skills to application targets')
63
67
  .option('-p, --profile <name>', 'Profile configuration to use')
64
68
  .option('--project <path>', 'Project directory containing .asb.toml')
65
69
  .option('--no-update', 'Skip updating remote sources')
@@ -68,14 +72,12 @@ program
68
72
  const scope = resolveScope(options);
69
73
  const loadOptions = scopeToLoadOptions(scope);
70
74
  const { config, layers } = loadSwitchboardConfigWithLayers(loadOptions);
71
- console.log();
72
- console.log(`${chalk.bgRed.white(' WARNING ')} ${chalk.red('`asb sync` overwrites target files without diff checks.')}`);
73
- console.log(chalk.red('Proceeding with synchronization...'));
75
+ console.log(chalk.yellow('⚠ Sync overwrites agent config without diff.'));
74
76
  console.log();
75
77
  if (options.update !== false) {
76
78
  const remoteResults = updateRemoteSources();
77
79
  if (remoteResults.length > 0) {
78
- console.log(chalk.blue('Remote source updates:'));
80
+ console.log(chalk.blue('Sources:'));
79
81
  for (const result of remoteResults) {
80
82
  if (result.status === 'updated') {
81
83
  console.log(` ${chalk.green('✓')} ${chalk.cyan(result.namespace)} ${chalk.dim(result.url)}`);
@@ -84,97 +86,175 @@ program
84
86
  console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(result.namespace)} ${chalk.yellow(result.error ?? 'update failed')}`);
85
87
  }
86
88
  }
87
- console.log();
88
89
  }
89
90
  }
90
- console.log(chalk.blue('Configuration layers:'));
91
- const layerEntries = [
92
- { label: 'User', exists: layers.user.exists, path: layers.user.path },
93
- {
94
- label: 'Profile',
95
- exists: layers.profile?.exists === true,
96
- path: layers.profile?.path ?? '(none)',
97
- },
98
- {
99
- label: 'Project',
100
- exists: layers.project?.exists === true,
101
- path: layers.project?.path ?? '(none)',
102
- },
103
- ];
104
- for (const entry of layerEntries) {
105
- const marker = entry.exists ? chalk.green('✓') : chalk.gray('•');
106
- const pathLabel = entry.exists ? chalk.dim(entry.path) : chalk.gray(entry.path);
107
- console.log(` ${marker} ${entry.label}: ${pathLabel}`);
108
- }
91
+ // Config summary: show active layers on one line
92
+ const activeLayers = [];
93
+ if (layers.user.exists)
94
+ activeLayers.push(shortenPath(layers.user.path));
95
+ if (layers.profile?.exists)
96
+ activeLayers.push(shortenPath(layers.profile.path));
97
+ if (layers.project?.exists)
98
+ activeLayers.push(shortenPath(layers.project.path));
99
+ console.log(`${chalk.blue('Config:')} ${activeLayers.length > 0 ? chalk.dim(activeLayers.join(' + ')) : chalk.gray('no config files')}`);
100
+ const appsLabel = config.applications.active.length > 0
101
+ ? chalk.cyan(config.applications.active.join(', '))
102
+ : chalk.gray('none configured');
103
+ console.log(`${chalk.blue('Apps:')} ${appsLabel}`);
109
104
  console.log();
110
- console.log(chalk.blue('Active selections:'));
111
- console.log(` MCP servers: ${chalk.cyan(String(config.mcp.active.length))}`);
112
- console.log(` Rules: ${chalk.cyan(String(config.rules.active.length))}`);
113
- console.log(` Commands: ${chalk.cyan(String(config.commands.active.length))}`);
114
- console.log(` Subagents: ${chalk.cyan(String(config.subagents.active.length))}`);
115
- console.log(` Skills: ${chalk.cyan(String(config.skills.active.length))}`);
116
- if (config.agents.active.length > 0) {
117
- console.log(` Agents: ${chalk.cyan(config.agents.active.join(', '))}`);
105
+ const cursorSkillsDeduped = config.applications.active.includes('claude-code') &&
106
+ resolveApplicationSectionConfig('skills', 'claude-code', scope).active.length > 0;
107
+ console.log(chalk.blue('Inventory:'));
108
+ {
109
+ const sections = ['mcp', 'rules', 'commands', 'agents', 'skills', 'hooks'];
110
+ // Keep in sync with platform lists in each distribution module
111
+ const sectionPlatforms = {
112
+ mcp: ['claude-code', 'claude-desktop', 'codex', 'cursor', 'gemini', 'opencode'],
113
+ rules: ['claude-code', 'codex', 'cursor', 'gemini', 'opencode'],
114
+ commands: ['claude-code', 'codex', 'cursor', 'gemini', 'opencode'],
115
+ agents: ['claude-code', 'opencode', 'cursor'],
116
+ skills: cursorSkillsDeduped
117
+ ? ['claude-code', 'codex', 'gemini', 'opencode']
118
+ : ['claude-code', 'codex', 'gemini', 'opencode', 'cursor'],
119
+ hooks: ['claude-code'],
120
+ };
121
+ const termWidth = process.stdout.columns || 80;
122
+ const maxSectionLen = Math.max(...sections.map((s) => s.length));
123
+ const maxCountLen = Math.max(...sections.map((s) => `(${config[s].active.length})`.length));
124
+ const prefixPlainLen = 2 + maxSectionLen + 1 + maxCountLen + 2;
125
+ const fitPreview = (ids, maxWidth) => {
126
+ if (ids.length === 0)
127
+ return chalk.gray('none');
128
+ const full = ids.join(', ');
129
+ if (full.length <= maxWidth)
130
+ return full;
131
+ let text = '';
132
+ let shown = 0;
133
+ for (let i = 0; i < ids.length; i++) {
134
+ const sep = shown > 0 ? ', ' : '';
135
+ const candidate = text + sep + ids[i];
136
+ const remaining = ids.length - (i + 1);
137
+ if (remaining > 0) {
138
+ const suffix = `, ... (+${remaining} more)`;
139
+ if (candidate.length + suffix.length > maxWidth && shown > 0) {
140
+ const left = ids.length - shown;
141
+ return `${text}${chalk.gray(`, ... (+${left} more)`)}`;
142
+ }
143
+ }
144
+ text = candidate;
145
+ shown++;
146
+ }
147
+ return text;
148
+ };
149
+ for (const section of sections) {
150
+ const globalActive = config[section].active;
151
+ const globalCount = globalActive.length;
152
+ const supported = new Set(sectionPlatforms[section] ?? []);
153
+ const applicableApps = config.applications.active.filter((id) => supported.has(id));
154
+ const effectiveByApp = new Map();
155
+ for (const appId of applicableApps) {
156
+ effectiveByApp.set(appId, resolveApplicationSectionConfig(section, appId, scope).active);
157
+ }
158
+ const perAppParts = applicableApps.map((appId) => {
159
+ const eff = effectiveByApp.get(appId) ?? [];
160
+ const delta = eff.length - globalCount;
161
+ const d = delta === 0 ? '' : delta > 0 ? `(+${delta})` : `(${delta})`;
162
+ return `${appId}:${eff.length}${d}`;
163
+ });
164
+ const union = new Set();
165
+ for (const [, ids] of effectiveByApp) {
166
+ for (const id of ids)
167
+ union.add(id);
168
+ }
169
+ const previewIds = globalActive.length > 0 ? [...globalActive] : [...union];
170
+ const paddedSection = section.padEnd(maxSectionLen);
171
+ const countStr = `(${globalCount})`.padStart(maxCountLen);
172
+ const appsStr = perAppParts.join(' ');
173
+ console.log(` ${chalk.cyan(paddedSection)} ${chalk.gray(countStr)} ${appsStr}`);
174
+ if (previewIds.length > 0) {
175
+ const indent = ' '.repeat(prefixPlainLen);
176
+ const previewWidth = Math.max(20, termWidth - prefixPlainLen - 2);
177
+ const preview = fitPreview(previewIds, previewWidth);
178
+ console.log(`${indent}${chalk.gray('→')} ${preview}`);
179
+ }
180
+ }
118
181
  }
119
- else {
120
- console.log(` Agents: ${chalk.gray('none configured')}`);
182
+ console.log();
183
+ const notes = ['rules, skills also distribute to gemini'];
184
+ if (cursorSkillsDeduped)
185
+ notes.push('cursor reads skills via claude-code');
186
+ for (let i = 0; i < notes.length; i++) {
187
+ const prefix = i === 0 ? ' Note: ' : ' ';
188
+ const suffix = i === notes.length - 1 ? '.' : '.';
189
+ console.log(chalk.gray(`${prefix}${notes[i]}${suffix}`));
121
190
  }
122
191
  console.log();
123
- // Sync MCP servers to agents
124
- console.log(chalk.blue('MCP server distribution:'));
125
- await applyToAgents(scope);
126
- const ruleDistribution = distributeRules(undefined, { force: true }, scope);
192
+ const mcpDistribution = await applyToAgents(scope, undefined, { useSpinner: false });
193
+ const ruleDistribution = distributeRules(undefined, undefined, scope);
127
194
  const commandDistribution = distributeCommands(scope);
128
- const subagentDistribution = distributeSubagents(scope);
195
+ const agentDistribution = distributeSubagents(scope);
129
196
  const skillDistribution = distributeSkills(scope, {
130
197
  useAgentsDir: config.distribution.use_agents_dir,
131
198
  });
132
- const ruleErrors = ruleDistribution.results.filter((result) => result.status === 'error');
133
- const commandErrors = commandDistribution.results.filter((result) => result.status === 'error');
134
- const subagentErrors = subagentDistribution.results.filter((result) => result.status === 'error');
135
- const skillErrors = skillDistribution.results.filter((result) => result.status === 'error');
136
- printDistributionResults({
137
- title: 'Rule distribution',
138
- results: ruleDistribution.results,
139
- emptyMessage: 'no supported agents configured',
140
- getTargetLabel: (result) => result.agent,
141
- getPath: (result) => result.filePath,
142
- });
143
- console.log();
144
- printDistributionResults({
145
- title: 'Command distribution',
146
- results: commandDistribution.results,
147
- emptyMessage: 'no active commands',
148
- getTargetLabel: (result) => result.platform,
149
- getPath: (result) => result.filePath,
150
- });
151
- console.log();
152
- printDistributionResults({
153
- title: 'Subagent distribution',
154
- results: subagentDistribution.results,
155
- emptyMessage: 'no active subagents',
156
- getTargetLabel: (result) => result.platform,
157
- getPath: (result) => result.filePath,
158
- });
159
- console.log();
160
- printDistributionResults({
161
- title: 'Skill distribution',
162
- results: skillDistribution.results,
163
- emptyMessage: 'no active skills',
164
- getTargetLabel: (result) => result.platform === 'agents' ? 'codex+gemini+opencode' : result.platform,
165
- getPath: (result) => result.targetDir,
166
- });
199
+ const hookDistribution = distributeHooks(scope);
200
+ const distSections = [
201
+ {
202
+ label: 'mcp',
203
+ results: mcpDistribution,
204
+ emptyMessage: 'no apps configured',
205
+ getTargetLabel: (r) => r.application,
206
+ getPath: (r) => r.filePath,
207
+ },
208
+ {
209
+ label: 'rules',
210
+ results: ruleDistribution.results,
211
+ emptyMessage: 'none',
212
+ getTargetLabel: (r) => r.agent,
213
+ getPath: (r) => r.filePath,
214
+ },
215
+ {
216
+ label: 'commands',
217
+ results: commandDistribution.results,
218
+ emptyMessage: 'none',
219
+ getTargetLabel: (r) => r.platform,
220
+ getPath: (r) => r.filePath,
221
+ },
222
+ {
223
+ label: 'agents',
224
+ results: agentDistribution.results,
225
+ emptyMessage: 'none',
226
+ getTargetLabel: (r) => r.platform,
227
+ getPath: (r) => r.filePath,
228
+ },
229
+ {
230
+ label: 'skills',
231
+ results: skillDistribution.results,
232
+ emptyMessage: 'none',
233
+ getTargetLabel: (r) => {
234
+ const sr = r;
235
+ return sr.platform === 'agents' ? 'codex+gemini+opencode' : sr.platform;
236
+ },
237
+ getPath: (r) => r.targetDir,
238
+ },
239
+ {
240
+ label: 'hooks',
241
+ results: hookDistribution.results,
242
+ emptyMessage: 'none',
243
+ getTargetLabel: (r) => r.platform,
244
+ getPath: (r) => {
245
+ const hr = r;
246
+ return 'filePath' in hr ? hr.filePath : hr.targetDir;
247
+ },
248
+ },
249
+ ];
250
+ const { hasErrors } = printCompactDistributions(distSections);
167
251
  console.log();
168
- const hasErrors = ruleErrors.length > 0 ||
169
- commandErrors.length > 0 ||
170
- subagentErrors.length > 0 ||
171
- skillErrors.length > 0;
172
252
  if (hasErrors) {
173
- console.log(chalk.red('✗ Synchronization completed with errors.'));
253
+ console.log(chalk.red('✗ Sync completed with errors.'));
174
254
  process.exit(1);
175
255
  }
176
256
  else {
177
- console.log(chalk.green('✓ Synchronization complete.'));
257
+ console.log(chalk.green('✓ Sync complete.'));
178
258
  }
179
259
  }
180
260
  catch (error) {
@@ -235,7 +315,7 @@ function defaultCommandSourceDir(platform) {
235
315
  return getOpencodePath('command');
236
316
  }
237
317
  }
238
- function defaultSubagentSourceDir(platform) {
318
+ function defaultAgentSourceDir(platform) {
239
319
  switch (platform) {
240
320
  case 'claude-code':
241
321
  return path.join(getClaudeDir(), 'agents');
@@ -243,6 +323,8 @@ function defaultSubagentSourceDir(platform) {
243
323
  return path.join(getCursorDir(), 'agents');
244
324
  case 'opencode':
245
325
  return getOpencodePath('agent');
326
+ default:
327
+ throw new Error(`Unknown agent platform: ${String(platform)}`);
246
328
  }
247
329
  }
248
330
  function defaultSkillSourceDir(platform) {
@@ -291,6 +373,141 @@ async function confirmOverwrite(filePath, force) {
291
373
  return true;
292
374
  return await confirm({ message: `File exists: ${filePath}. Overwrite?`, default: false });
293
375
  }
376
+ function copyDirRecursive(src, dest) {
377
+ fs.mkdirSync(dest, { recursive: true });
378
+ const entries = fs.readdirSync(src, { withFileTypes: true });
379
+ for (const entry of entries) {
380
+ const srcPath = path.join(src, entry.name);
381
+ const destPath = path.join(dest, entry.name);
382
+ if (entry.isDirectory()) {
383
+ copyDirRecursive(srcPath, destPath);
384
+ }
385
+ else {
386
+ fs.copyFileSync(srcPath, destPath);
387
+ // Preserve executable permissions
388
+ try {
389
+ const mode = fs.statSync(srcPath).mode;
390
+ if (mode & 0o111)
391
+ fs.chmodSync(destPath, mode & 0o777);
392
+ }
393
+ catch {
394
+ // Ignore
395
+ }
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * Extract hooks from Claude Code's ~/.claude/settings.json and import as a
401
+ * bundle into ~/.asb/hooks/. Copies referenced script files if they exist
402
+ * alongside the hooks and rewrites commands to use ${HOOK_DIR}.
403
+ */
404
+ async function importHooksFromClaudeCode(opts) {
405
+ const settingsPath = path.join(getClaudeDir(), 'settings.json');
406
+ if (!isFile(settingsPath)) {
407
+ console.error(chalk.red(`\n✗ Claude Code settings not found: ${settingsPath}`));
408
+ process.exit(1);
409
+ }
410
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
411
+ const hooks = settings.hooks;
412
+ if (!hooks || Object.keys(hooks).length === 0) {
413
+ console.log(chalk.yellow('\n⚠ No hooks found in Claude Code settings.'));
414
+ return;
415
+ }
416
+ // Filter out ASB-managed hooks (avoid re-importing our own output)
417
+ const userHooks = {};
418
+ for (const [event, groups] of Object.entries(hooks)) {
419
+ const userGroups = groups.filter((g) => g._asb_source === undefined);
420
+ if (userGroups.length > 0)
421
+ userHooks[event] = userGroups;
422
+ }
423
+ if (Object.keys(userHooks).length === 0) {
424
+ console.log(chalk.yellow('\n⚠ No user-defined hooks found (all are ASB-managed).'));
425
+ return;
426
+ }
427
+ // Collect script paths referenced in commands
428
+ const referencedScripts = new Set();
429
+ for (const groups of Object.values(userHooks)) {
430
+ for (const group of groups) {
431
+ const handlers = group.hooks;
432
+ if (!handlers)
433
+ continue;
434
+ for (const handler of handlers) {
435
+ if (typeof handler.command !== 'string')
436
+ continue;
437
+ // Extract file paths from commands like "node ~/.claude/hooks/script.mjs"
438
+ const match = handler.command.match(/(?:^|\s)(~\/\.claude\/hooks\/\S+|(?:\/\S*\/)?\.claude\/hooks\/\S+)/);
439
+ if (match) {
440
+ const scriptPath = match[1].replace(/^~/, process.env.HOME ?? '');
441
+ if (fs.existsSync(scriptPath)) {
442
+ referencedScripts.add(scriptPath);
443
+ }
444
+ }
445
+ }
446
+ }
447
+ }
448
+ const slug = 'claude-code-hooks';
449
+ const bundleDir = path.join(getHooksDir(), slug);
450
+ if (fs.existsSync(bundleDir)) {
451
+ if (!(await confirmOverwrite(bundleDir, opts.force)))
452
+ return;
453
+ fs.rmSync(bundleDir, { recursive: true });
454
+ }
455
+ fs.mkdirSync(bundleDir, { recursive: true });
456
+ // Copy referenced scripts and rewrite commands to use ${HOOK_DIR}
457
+ const { HOOK_DIR_PLACEHOLDER } = await import('./hooks/schema.js');
458
+ const rewrittenHooks = {};
459
+ for (const [event, groups] of Object.entries(userHooks)) {
460
+ rewrittenHooks[event] = groups.map((group) => {
461
+ const handlers = group.hooks;
462
+ if (!handlers)
463
+ return group;
464
+ return {
465
+ ...group,
466
+ hooks: handlers.map((handler) => {
467
+ if (typeof handler.command !== 'string')
468
+ return handler;
469
+ let cmd = handler.command;
470
+ for (const scriptPath of referencedScripts) {
471
+ const scriptName = path.basename(scriptPath);
472
+ if (cmd.includes(scriptPath) ||
473
+ cmd.includes(scriptPath.replace(process.env.HOME ?? '', '~'))) {
474
+ cmd = cmd.replace(scriptPath, `${HOOK_DIR_PLACEHOLDER}/${scriptName}`);
475
+ cmd = cmd.replace(scriptPath.replace(process.env.HOME ?? '', '~'), `${HOOK_DIR_PLACEHOLDER}/${scriptName}`);
476
+ }
477
+ }
478
+ return { ...handler, command: cmd };
479
+ }),
480
+ };
481
+ });
482
+ }
483
+ // Copy script files into the bundle
484
+ let scriptsCopied = 0;
485
+ for (const scriptPath of referencedScripts) {
486
+ const scriptName = path.basename(scriptPath);
487
+ const destPath = path.join(bundleDir, scriptName);
488
+ fs.copyFileSync(scriptPath, destPath);
489
+ try {
490
+ const mode = fs.statSync(scriptPath).mode;
491
+ if (mode & 0o111)
492
+ fs.chmodSync(destPath, mode & 0o777);
493
+ }
494
+ catch {
495
+ // Ignore
496
+ }
497
+ scriptsCopied++;
498
+ console.log(` ${chalk.green('✓')} ${chalk.dim(scriptName)}`);
499
+ }
500
+ // Write hook.json
501
+ const hookJson = {
502
+ name: 'claude-code-hooks',
503
+ description: 'Hooks imported from Claude Code settings.json',
504
+ hooks: rewrittenHooks,
505
+ };
506
+ fs.writeFileSync(path.join(bundleDir, 'hook.json'), `${JSON.stringify(hookJson, null, 2)}\n`, 'utf-8');
507
+ const eventCount = Object.keys(rewrittenHooks).length;
508
+ console.log(`\n${chalk.green('✓')} Imported ${eventCount} event(s) + ${scriptsCopied} script(s) → ${chalk.dim(bundleDir)}`);
509
+ console.log(chalk.gray(' Activate with: asb hook (interactive) or add to [hooks].active in config.toml'));
510
+ }
294
511
  program
295
512
  .command('mcp')
296
513
  .description('Interactive UI to enable/disable MCP servers')
@@ -668,13 +885,13 @@ commandRoot
668
885
  process.exit(1);
669
886
  }
670
887
  });
671
- // Subagents library: scaffold, load, list, and interactive distribute
672
- const subagentRoot = program
673
- .command('subagent')
674
- .description('Select subagent definitions interactively and distribute to agents')
888
+ // Agents library: load, list, and interactive distribute
889
+ const agentRoot = program
890
+ .command('agent')
891
+ .description('Select agent definitions interactively and distribute to applications')
675
892
  .option('-p, --profile <name>', 'Profile configuration to use')
676
893
  .option('--project <path>', 'Project directory containing .asb.toml');
677
- subagentRoot.action(async (options) => {
894
+ agentRoot.action(async (options) => {
678
895
  try {
679
896
  const scope = resolveScope(options);
680
897
  const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
@@ -682,18 +899,17 @@ subagentRoot.action(async (options) => {
682
899
  if (!selection)
683
900
  return;
684
901
  console.log();
685
- printActiveSelection('subagents', selection.active);
902
+ printActiveSelection('agents', selection.active);
686
903
  const out = distributeSubagents(scope);
687
904
  if (out.results.length > 0) {
688
905
  console.log();
689
906
  printDistributionResults({
690
- title: 'Subagent distribution',
907
+ title: 'Agent distribution',
691
908
  results: out.results,
692
909
  getTargetLabel: (result) => result.platform,
693
910
  getPath: (result) => result.filePath,
694
911
  });
695
912
  }
696
- // Guidance: unsupported platforms for subagents
697
913
  console.log();
698
914
  console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
699
915
  }
@@ -704,9 +920,9 @@ subagentRoot.action(async (options) => {
704
920
  process.exit(1);
705
921
  }
706
922
  });
707
- subagentRoot
923
+ agentRoot
708
924
  .command('load')
709
- .description('Import existing platform files into the subagent library')
925
+ .description('Import existing platform files into the agent library')
710
926
  .argument('<platform>', 'claude-code | opencode | cursor')
711
927
  .argument('[path]', 'Source file or directory (defaults by platform)')
712
928
  .option('-r, --recursive', 'When [path] is a directory, import files recursively')
@@ -714,7 +930,7 @@ subagentRoot
714
930
  .action(async (platform, srcPath, opts) => {
715
931
  try {
716
932
  const exts = ['.md', '.markdown'];
717
- const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultSubagentSourceDir(platform);
933
+ const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultAgentSourceDir(platform);
718
934
  if (!fs.existsSync(source)) {
719
935
  console.error(chalk.red(`\n✗ Source not found: ${source}`));
720
936
  process.exit(1);
@@ -734,7 +950,7 @@ subagentRoot
734
950
  console.log(chalk.yellow('\n⚠ No files to import.'));
735
951
  return;
736
952
  }
737
- const outDir = getSubagentsDir();
953
+ const outDir = getAgentsDir();
738
954
  let imported = 0;
739
955
  for (const file of inputs) {
740
956
  try {
@@ -751,7 +967,7 @@ subagentRoot
751
967
  console.log(`${chalk.red('✗')} ${chalk.dim(file)} ${chalk.red(msg)}`);
752
968
  }
753
969
  }
754
- console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into subagent library.`);
970
+ console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into agent library.`);
755
971
  }
756
972
  catch (error) {
757
973
  if (error instanceof Error) {
@@ -760,9 +976,9 @@ subagentRoot
760
976
  process.exit(1);
761
977
  }
762
978
  });
763
- subagentRoot
979
+ agentRoot
764
980
  .command('list')
765
- .description('Display subagent inventory and sync information')
981
+ .description('Display agent inventory and sync information')
766
982
  .option('--json', 'Output inventory as JSON')
767
983
  .option('-p, --profile <name>', 'Profile configuration to use')
768
984
  .option('--project <path>', 'Project directory containing .asb.toml')
@@ -779,10 +995,10 @@ subagentRoot
779
995
  return;
780
996
  }
781
997
  if (inventory.entries.length === 0) {
782
- console.log(chalk.yellow('⚠ No subagents found. Use `asb subagent load <platform> [path]`.'));
998
+ console.log(chalk.yellow('⚠ No agents found. Use `asb agent load <platform> [path]`.'));
783
999
  }
784
1000
  else {
785
- console.log(chalk.blue('Subagents:'));
1001
+ console.log(chalk.blue('Agents:'));
786
1002
  const header = ['ID', 'Active', 'Title', 'Model', 'Tools', 'Extras'];
787
1003
  const rows = inventory.entries.map((row) => {
788
1004
  const activePlain = row.active ? 'yes' : 'no';
@@ -806,7 +1022,6 @@ subagentRoot
806
1022
  }
807
1023
  console.log();
808
1024
  printAgentSyncStatus({ agentSync: inventory.state.agentSync });
809
- // Guidance: unsupported platforms for subagents
810
1025
  console.log();
811
1026
  console.log(chalk.gray('Unsupported platforms (manual steps required): Codex, Gemini'));
812
1027
  }
@@ -958,30 +1173,198 @@ skillRoot
958
1173
  process.exit(1);
959
1174
  }
960
1175
  });
961
- /**
962
- * Apply enabled MCP servers to all registered agents
963
- * @param scope - Configuration scope (profile/project)
964
- * @param enabledServerNames - List of enabled server names
965
- */
966
- async function applyToAgents(scope, enabledServerNames) {
1176
+ // Hooks library: manage and distribute hooks to Claude Code
1177
+ const hookRoot = program
1178
+ .command('hook')
1179
+ .description('Select hooks interactively and distribute to Claude Code')
1180
+ .option('-p, --profile <name>', 'Profile configuration to use')
1181
+ .option('--project <path>', 'Project directory containing .asb.toml');
1182
+ hookRoot.action(async (options) => {
1183
+ try {
1184
+ const scope = resolveScope(options);
1185
+ const config = loadSwitchboardConfig(scopeToLoadOptions(scope));
1186
+ const selection = await showHookSelector({ scope, pageSize: config.ui.page_size });
1187
+ if (!selection)
1188
+ return;
1189
+ console.log();
1190
+ printActiveSelection('hooks', selection.active);
1191
+ const out = distributeHooks(scope);
1192
+ if (out.results.length > 0) {
1193
+ console.log();
1194
+ printDistributionResults({
1195
+ title: 'Hook distribution',
1196
+ results: out.results,
1197
+ getTargetLabel: (result) => result.platform,
1198
+ getPath: (result) => 'filePath' in result ? result.filePath : result.targetDir,
1199
+ });
1200
+ }
1201
+ }
1202
+ catch (error) {
1203
+ if (error instanceof Error) {
1204
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1205
+ }
1206
+ process.exit(1);
1207
+ }
1208
+ });
1209
+ hookRoot
1210
+ .command('list')
1211
+ .description('Display hook library entries')
1212
+ .option('--json', 'Output inventory as JSON')
1213
+ .option('-p, --profile <name>', 'Profile configuration to use')
1214
+ .option('--project <path>', 'Project directory containing .asb.toml')
1215
+ .action((options) => {
1216
+ try {
1217
+ const scope = resolveScope(options);
1218
+ const entries = loadHookLibrary();
1219
+ const state = loadLibraryStateSection('hooks', scope);
1220
+ const activeSet = new Set(state.active);
1221
+ if (options.json) {
1222
+ console.log(JSON.stringify({
1223
+ entries: entries.map((e) => ({
1224
+ id: e.id,
1225
+ name: e.name,
1226
+ description: e.description,
1227
+ events: Object.keys(e.hooks),
1228
+ active: activeSet.has(e.id),
1229
+ })),
1230
+ active: state.active,
1231
+ }, null, 2));
1232
+ return;
1233
+ }
1234
+ if (entries.length === 0) {
1235
+ console.log(chalk.yellow(`⚠ No hooks found. Add JSON hook files to ${getHooksDir()}`));
1236
+ }
1237
+ else {
1238
+ console.log(chalk.blue('Hooks:'));
1239
+ const header = ['ID', 'Active', 'Name', 'Events', 'Description'];
1240
+ const rows = entries.map((e) => {
1241
+ const active = activeSet.has(e.id);
1242
+ const activePlain = active ? 'yes' : 'no';
1243
+ const events = Object.keys(e.hooks).join(', ');
1244
+ const desc = e.description ?? '—';
1245
+ const descTrunc = desc.length > 40 ? `${desc.substring(0, 37)}...` : desc;
1246
+ return [
1247
+ { plain: e.id, formatted: e.id },
1248
+ {
1249
+ plain: activePlain,
1250
+ formatted: active ? chalk.green(activePlain) : chalk.gray(activePlain),
1251
+ },
1252
+ { plain: e.name ?? e.id, formatted: e.name ?? e.id },
1253
+ { plain: events, formatted: events },
1254
+ { plain: descTrunc, formatted: descTrunc },
1255
+ ];
1256
+ });
1257
+ printTable(header, rows);
1258
+ }
1259
+ }
1260
+ catch (error) {
1261
+ if (error instanceof Error) {
1262
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1263
+ }
1264
+ process.exit(1);
1265
+ }
1266
+ });
1267
+ hookRoot
1268
+ .command('load')
1269
+ .description('Import hooks into the library. Accepts:\n' +
1270
+ ' - A JSON file (single-file hook)\n' +
1271
+ ' - A directory with hook.json (bundle hook)\n' +
1272
+ ' - "claude-code" to extract from ~/.claude/settings.json')
1273
+ .argument('<source>', 'JSON file, directory, or "claude-code"')
1274
+ .option('-f, --force', 'Overwrite existing library entries without confirmation')
1275
+ .option('-n, --name <name>', 'Override the hook ID (default: basename of source)')
1276
+ .action(async (source, opts) => {
1277
+ try {
1278
+ if (source === 'claude-code') {
1279
+ await importHooksFromClaudeCode(opts);
1280
+ return;
1281
+ }
1282
+ const resolved = path.resolve(source);
1283
+ if (isFile(resolved)) {
1284
+ // Single-file hook import
1285
+ const content = fs.readFileSync(resolved, 'utf-8');
1286
+ try {
1287
+ const { hookFileSchema } = await import('./hooks/schema.js');
1288
+ hookFileSchema.parse(JSON.parse(content));
1289
+ }
1290
+ catch (error) {
1291
+ const msg = error instanceof Error ? error.message : String(error);
1292
+ console.error(chalk.red(`\n✗ Invalid hook file: ${msg}`));
1293
+ process.exit(1);
1294
+ }
1295
+ const slug = opts.name ?? path.basename(resolved, path.extname(resolved));
1296
+ const target = path.join(getHooksDir(), `${slug}.json`);
1297
+ if (!(await confirmOverwrite(target, opts.force)))
1298
+ return;
1299
+ writeFileSecure(target, content);
1300
+ console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(target)}`);
1301
+ }
1302
+ else if (isDir(resolved)) {
1303
+ // Bundle hook import (directory with hook.json + scripts)
1304
+ const hookJsonPath = path.join(resolved, 'hook.json');
1305
+ if (!isFile(hookJsonPath)) {
1306
+ console.error(chalk.red('\n✗ Directory does not contain hook.json'));
1307
+ process.exit(1);
1308
+ }
1309
+ // Validate hook.json
1310
+ try {
1311
+ const { hookFileSchema } = await import('./hooks/schema.js');
1312
+ hookFileSchema.parse(JSON.parse(fs.readFileSync(hookJsonPath, 'utf-8')));
1313
+ }
1314
+ catch (error) {
1315
+ const msg = error instanceof Error ? error.message : String(error);
1316
+ console.error(chalk.red(`\n✗ Invalid hook.json: ${msg}`));
1317
+ process.exit(1);
1318
+ }
1319
+ const slug = opts.name ?? path.basename(resolved);
1320
+ const targetDir = path.join(getHooksDir(), slug);
1321
+ if (fs.existsSync(targetDir)) {
1322
+ if (!(await confirmOverwrite(targetDir, opts.force)))
1323
+ return;
1324
+ fs.rmSync(targetDir, { recursive: true });
1325
+ }
1326
+ // Copy entire directory
1327
+ copyDirRecursive(resolved, targetDir);
1328
+ const fileCount = listFilesRecursively(targetDir, []).length;
1329
+ console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(targetDir)} (${fileCount} files)`);
1330
+ }
1331
+ else {
1332
+ console.error(chalk.red(`\n✗ Source not found: ${resolved}`));
1333
+ process.exit(1);
1334
+ }
1335
+ }
1336
+ catch (error) {
1337
+ if (error instanceof Error) {
1338
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
1339
+ }
1340
+ process.exit(1);
1341
+ }
1342
+ });
1343
+ async function applyToAgents(scope, enabledServerNames, options) {
967
1344
  const mcpConfig = loadMcpConfig();
968
1345
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
969
- if (switchboardConfig.agents.active.length === 0) {
970
- console.log(chalk.yellow('\n⚠ No agents found in the active configuration stack.'));
971
- console.log();
972
- console.log('Add agents under the relevant TOML layer (user, profile, or project).');
973
- console.log(chalk.dim(' Example: [agents]\n active = ["claude-code", "cursor"]'));
974
- return;
1346
+ const useSpinner = options?.useSpinner ?? true;
1347
+ const results = [];
1348
+ if (switchboardConfig.applications.active.length === 0) {
1349
+ if (useSpinner) {
1350
+ console.log(chalk.yellow('\n⚠ No applications found in the active configuration stack.'));
1351
+ console.log();
1352
+ console.log('Add applications under the relevant TOML layer (user, profile, or project).');
1353
+ console.log(chalk.dim(' Example: [applications]\n active = ["claude-code", "cursor"]'));
1354
+ }
1355
+ return results;
975
1356
  }
976
1357
  // Global MCP servers list (from UI selection or config)
977
1358
  const globalMcpServers = enabledServerNames ?? loadMcpActiveState(scope);
978
- // Apply to each registered agent with per-agent MCP overrides
979
- for (const agentId of switchboardConfig.agents.active) {
980
- const spinner = ora({ indent: 2 }).start(`Applying to ${agentId}...`);
981
- const persist = (symbol, text) => spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1359
+ for (const agentId of switchboardConfig.applications.active) {
1360
+ const spinner = useSpinner ? ora({ indent: 2 }).start(`Applying to ${agentId}...`) : null;
1361
+ const persist = (symbol, text) => {
1362
+ if (!spinner)
1363
+ return;
1364
+ spinner.stopAndPersist({ symbol: ` ${symbol}`, text });
1365
+ };
982
1366
  try {
983
- // Get per-agent MCP config (applies add/remove overrides)
984
- const agentMcpConfig = resolveAgentSectionConfig('mcp', agentId, scope);
1367
+ const agentMcpConfig = resolveApplicationSectionConfig('mcp', agentId, scope);
985
1368
  // If user selected servers via UI, use that as base; otherwise use per-agent resolved config
986
1369
  const agentActiveServers = enabledServerNames
987
1370
  ? agentMcpConfig.active.filter((s) => globalMcpServers.includes(s))
@@ -991,23 +1374,57 @@ async function applyToAgents(scope, enabledServerNames) {
991
1374
  const enabledServers = Object.fromEntries(Object.entries(mcpConfig.mcpServers).filter(([name]) => activeSet.has(name)));
992
1375
  const configToApply = { mcpServers: enabledServers };
993
1376
  const agent = getAgentById(agentId);
1377
+ const readFileSafe = (p) => {
1378
+ try {
1379
+ return fs.readFileSync(p, 'utf-8');
1380
+ }
1381
+ catch {
1382
+ return null;
1383
+ }
1384
+ };
994
1385
  // Use project-level config when --project is specified
995
1386
  if (scope?.project && agent.applyProjectConfig) {
996
- agent.applyProjectConfig(scope.project, configToApply);
997
1387
  const projectPath = agent.projectConfigPath?.(scope.project) ?? 'project config';
998
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(projectPath)}`);
1388
+ const before = readFileSafe(projectPath);
1389
+ agent.applyProjectConfig(scope.project, configToApply);
1390
+ const after = readFileSafe(projectPath);
1391
+ const changed = before !== after;
1392
+ persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(projectPath))}`);
1393
+ results.push({
1394
+ application: agentId,
1395
+ filePath: projectPath,
1396
+ status: changed ? 'written' : 'skipped',
1397
+ reason: changed ? 'applied' : 'up-to-date',
1398
+ });
999
1399
  }
1000
1400
  else {
1401
+ const configPath = agent.configPath();
1402
+ const before = readFileSafe(configPath);
1001
1403
  agent.applyConfig(configToApply);
1002
- persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(agent.configPath())}`);
1404
+ const after = readFileSafe(configPath);
1405
+ const changed = before !== after;
1406
+ persist(chalk.green('✓'), `${chalk.cyan(agentId)} ${chalk.dim(shortenPath(configPath))}`);
1407
+ results.push({
1408
+ application: agentId,
1409
+ filePath: configPath,
1410
+ status: changed ? 'written' : 'skipped',
1411
+ reason: changed ? 'applied' : 'up-to-date',
1412
+ });
1003
1413
  }
1004
1414
  }
1005
1415
  catch (error) {
1006
1416
  if (error instanceof Error) {
1007
1417
  persist(chalk.yellow('⚠'), `${chalk.cyan(agentId)} - ${error.message} (skipped)`);
1418
+ results.push({
1419
+ application: agentId,
1420
+ filePath: '(unknown)',
1421
+ status: 'error',
1422
+ error: `${error.message} (skipped)`,
1423
+ });
1008
1424
  }
1009
1425
  }
1010
1426
  }
1427
+ return results;
1011
1428
  }
1012
1429
  /**
1013
1430
  * Show summary of enabled/disabled servers and applied agents
@@ -1032,15 +1449,32 @@ function showSummary(selectedServers, scope) {
1032
1449
  }
1033
1450
  }
1034
1451
  const switchboardConfig = loadSwitchboardConfig(scopeToLoadOptions(scope));
1035
- if (switchboardConfig.agents.active.length > 0) {
1036
- console.log(chalk.blue(`\nApplied to agents (${switchboardConfig.agents.active.length}):`));
1037
- for (const agent of switchboardConfig.agents.active) {
1452
+ if (switchboardConfig.applications.active.length > 0) {
1453
+ console.log(chalk.blue(`\nApplied to applications (${switchboardConfig.applications.active.length}):`));
1454
+ for (const agent of switchboardConfig.applications.active) {
1038
1455
  console.log(` ${chalk.dim('•')} ${agent}`);
1039
1456
  }
1040
1457
  }
1041
1458
  console.log();
1042
1459
  }
1043
- // Library source management commands
1460
+ function printMarketplaceSummary(localPath) {
1461
+ try {
1462
+ const mp = readMarketplace(localPath);
1463
+ console.log(chalk.dim(` Type: marketplace (${mp.name})`));
1464
+ console.log(chalk.dim(` Plugins: ${mp.plugins.length}`));
1465
+ for (const plugin of mp.plugins) {
1466
+ const desc = plugin.description ? ` - ${plugin.description}` : '';
1467
+ console.log(chalk.dim(` ${plugin.name}${desc}`));
1468
+ }
1469
+ for (const w of mp.warnings) {
1470
+ console.log(chalk.yellow(` ⚠ ${w}`));
1471
+ }
1472
+ }
1473
+ catch (error) {
1474
+ const msg = error instanceof Error ? error.message : String(error);
1475
+ console.log(chalk.yellow(` ⚠ Failed to read marketplace: ${msg}`));
1476
+ }
1477
+ }
1044
1478
  const sourceRoot = program
1045
1479
  .command('source')
1046
1480
  .description('Manage external library sources (local paths or git repos)');
@@ -1073,7 +1507,7 @@ sourceRoot
1073
1507
  const validation = validateSourcePath(effectivePath);
1074
1508
  if (!validation.valid) {
1075
1509
  removeSource(name);
1076
- console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/, subagents/, skills/).'));
1510
+ console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
1077
1511
  process.exit(1);
1078
1512
  }
1079
1513
  console.log(chalk.green(`\n✓ Added source "${name}" from ${parsed.url}`));
@@ -1081,22 +1515,32 @@ sourceRoot
1081
1515
  console.log(chalk.dim(` Ref: ${parsed.ref}`));
1082
1516
  if (parsed.subdir)
1083
1517
  console.log(chalk.dim(` Subdir: ${parsed.subdir}`));
1084
- console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1085
- if (validation.missing.length > 0) {
1086
- console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1518
+ if (validation.isMarketplace) {
1519
+ printMarketplaceSummary(effectivePath);
1520
+ }
1521
+ else {
1522
+ console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1523
+ if (validation.missing.length > 0) {
1524
+ console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1525
+ }
1087
1526
  }
1088
1527
  }
1089
1528
  else {
1090
1529
  const validation = validateSourcePath(location);
1091
1530
  if (!validation.valid) {
1092
- console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/, subagents/, skills/).'));
1531
+ console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/, agents/, skills/) or marketplace manifest.'));
1093
1532
  process.exit(1);
1094
1533
  }
1095
1534
  addLocalSource(name, location);
1096
1535
  console.log(chalk.green(`\n✓ Added source "${name}" at ${path.resolve(location)}`));
1097
- console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1098
- if (validation.missing.length > 0) {
1099
- console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1536
+ if (validation.isMarketplace) {
1537
+ printMarketplaceSummary(path.resolve(location));
1538
+ }
1539
+ else {
1540
+ console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1541
+ if (validation.missing.length > 0) {
1542
+ console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1543
+ }
1100
1544
  }
1101
1545
  }
1102
1546
  console.log();
@@ -1136,7 +1580,7 @@ sourceRoot
1136
1580
  sourceRoot
1137
1581
  .command('list')
1138
1582
  .description('List all library sources')
1139
- .option('--json', 'Output as JSON')
1583
+ .option('--json', 'Output inventory as JSON')
1140
1584
  .action((options) => {
1141
1585
  try {
1142
1586
  const sources = getSources();
@@ -1145,7 +1589,7 @@ sourceRoot
1145
1589
  return;
1146
1590
  }
1147
1591
  if (sources.length === 0) {
1148
- console.log(chalk.yellow('\n⚠ No library sources configured.'));
1592
+ console.log(chalk.yellow('⚠ No library sources configured.'));
1149
1593
  console.log(chalk.dim(' Use `asb source add <location> [name]` to add one.'));
1150
1594
  return;
1151
1595
  }
@@ -1153,10 +1597,12 @@ sourceRoot
1153
1597
  const header = ['Namespace', 'Type', 'Source', 'Status', 'Contains'];
1154
1598
  const rows = sources.map((src) => {
1155
1599
  const isRemote = !!src.remote;
1156
- const typePlain = isRemote ? 'remote' : 'local';
1157
- const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
1158
1600
  const exists = fs.existsSync(src.path);
1159
- const validation = exists ? validateSourcePath(src.path) : { found: [], missing: [] };
1601
+ const validation = exists
1602
+ ? validateSourcePath(src.path)
1603
+ : { found: [], missing: [], isMarketplace: false };
1604
+ const typePlain = validation.isMarketplace ? 'marketplace' : isRemote ? 'remote' : 'local';
1605
+ const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
1160
1606
  let statusPlain;
1161
1607
  if (isRemote) {
1162
1608
  statusPlain = exists ? 'cached' : 'not cached';
@@ -1164,12 +1610,28 @@ sourceRoot
1164
1610
  else {
1165
1611
  statusPlain = exists ? 'ok' : 'missing';
1166
1612
  }
1167
- const containsPlain = validation.found.length > 0 ? validation.found.join(', ') : '-';
1613
+ let containsPlain;
1614
+ if (validation.isMarketplace && exists) {
1615
+ try {
1616
+ const mp = readMarketplace(src.path);
1617
+ containsPlain = `${mp.plugins.length} plugin(s)`;
1618
+ }
1619
+ catch {
1620
+ containsPlain = 'marketplace (error)';
1621
+ }
1622
+ }
1623
+ else {
1624
+ containsPlain = validation.found.length > 0 ? validation.found.join(', ') : '-';
1625
+ }
1168
1626
  return [
1169
1627
  { plain: src.namespace, formatted: chalk.cyan(src.namespace) },
1170
1628
  {
1171
1629
  plain: typePlain,
1172
- formatted: isRemote ? chalk.blue(typePlain) : chalk.gray(typePlain),
1630
+ formatted: validation.isMarketplace
1631
+ ? chalk.magenta(typePlain)
1632
+ : isRemote
1633
+ ? chalk.blue(typePlain)
1634
+ : chalk.gray(typePlain),
1173
1635
  },
1174
1636
  { plain: sourcePlain, formatted: chalk.dim(sourcePlain) },
1175
1637
  {