agent-switchboard 0.1.25 → 0.1.27

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
@@ -16,14 +16,14 @@ import { importCommandFromFile } from './commands/importer.js';
16
16
  import { buildCommandInventory } from './commands/inventory.js';
17
17
  import { resolveAgentSectionConfig } from './config/agent-config.js';
18
18
  import { loadMcpConfig, stripLegacyEnabledFlagsFromMcpJson } from './config/mcp-config.js';
19
- import { getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getOpencodePath, getSkillsDir, getSubagentsDir, } from './config/paths.js';
19
+ import { getAgentsHome, getClaudeDir, getCodexDir, getCommandsDir, getCursorDir, getGeminiDir, getOpencodePath, getSkillsDir, getSourceCacheDir, getSubagentsDir, } from './config/paths.js';
20
20
  import { loadSwitchboardConfig, loadSwitchboardConfigWithLayers, } from './config/switchboard-config.js';
21
21
  import { ensureLibraryDirectories, writeFileSecure } from './library/fs.js';
22
+ import { addLocalSource, addRemoteSource, getSources, inferSourceName, isGitUrl, parseGitUrl, removeSource, updateRemoteSources, validateSourcePath, } from './library/sources.js';
22
23
  import { loadMcpActiveState, saveMcpActiveState } from './library/state.js';
23
- import { addSubscription, getSubscriptions, removeSubscription, validateSubscriptionPath, } from './library/subscriptions.js';
24
24
  import { RULE_SUPPORTED_AGENTS } from './rules/agents.js';
25
25
  import { composeActiveRules } from './rules/composer.js';
26
- import { distributeRules, listIndirectAgents, listUnsupportedAgents, } from './rules/distribution.js';
26
+ import { distributeRules, listIndirectAgents, listPerFileAgents, listUnsupportedAgents, } from './rules/distribution.js';
27
27
  import { buildRuleInventory } from './rules/inventory.js';
28
28
  import { loadRuleLibrary } from './rules/library.js';
29
29
  import { loadRuleState, updateRuleState } from './rules/state.js';
@@ -44,8 +44,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
44
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'));
45
45
  program
46
46
  .name('asb')
47
- .description('Unified MCP server manager for AI coding agents')
48
- .version(packageJson.version);
47
+ .description('Manage MCP servers, rules, commands, subagents, and skills across AI coding agents')
48
+ .version(packageJson.version)
49
+ .addHelpText('after', `
50
+ Examples:
51
+ $ asb mcp Enable/disable MCP servers interactively
52
+ $ asb rule Select and order rule snippets
53
+ $ asb sync Push all libraries to every active agent
54
+ $ asb sync --project . Sync with project-level overrides
55
+
56
+ Alias: agent-switchboard
57
+ Config: ~/.agent-switchboard/config.toml`);
49
58
  // Initialize library directories for commands/subagents (secure permissions)
50
59
  ensureLibraryDirectories();
51
60
  program
@@ -53,6 +62,7 @@ program
53
62
  .description('Synchronize active MCP servers, rules, commands, subagents, and skills to agent targets')
54
63
  .option('-p, --profile <name>', 'Profile configuration to use')
55
64
  .option('--project <path>', 'Project directory containing .asb.toml')
65
+ .option('--no-update', 'Skip updating remote sources')
56
66
  .action(async (options) => {
57
67
  try {
58
68
  const scope = resolveScope(options);
@@ -62,6 +72,21 @@ program
62
72
  console.log(`${chalk.bgRed.white(' WARNING ')} ${chalk.red('`asb sync` overwrites target files without diff checks.')}`);
63
73
  console.log(chalk.red('Proceeding with synchronization...'));
64
74
  console.log();
75
+ if (options.update !== false) {
76
+ const remoteResults = updateRemoteSources();
77
+ if (remoteResults.length > 0) {
78
+ console.log(chalk.blue('Remote source updates:'));
79
+ for (const result of remoteResults) {
80
+ if (result.status === 'updated') {
81
+ console.log(` ${chalk.green('✓')} ${chalk.cyan(result.namespace)} ${chalk.dim(result.url)}`);
82
+ }
83
+ else {
84
+ console.log(` ${chalk.yellow('⚠')} ${chalk.cyan(result.namespace)} ${chalk.yellow(result.error ?? 'update failed')}`);
85
+ }
86
+ }
87
+ console.log();
88
+ }
89
+ }
65
90
  console.log(chalk.blue('Configuration layers:'));
66
91
  const layerEntries = [
67
92
  { label: 'User', exists: layers.user.exists, path: layers.user.path },
@@ -335,7 +360,7 @@ program
335
360
  });
336
361
  const ruleCommand = program
337
362
  .command('rule')
338
- .description('Manage rule snippets and synchronization')
363
+ .description('Select and order rule snippets interactively, then sync to agents')
339
364
  .option('-p, --profile <name>', 'Profile configuration to use')
340
365
  .option('--project <path>', 'Project directory containing .asb.toml');
341
366
  ruleCommand
@@ -396,6 +421,10 @@ ruleCommand
396
421
  console.log();
397
422
  console.log(chalk.gray(`Unsupported agents (manual update required): ${unsupportedAgents.join(', ')}`));
398
423
  }
424
+ const perFileAgents = listPerFileAgents();
425
+ if (perFileAgents.length > 0) {
426
+ console.log(chalk.gray(`Per-file rules (.mdc): ${perFileAgents.join(', ')}`));
427
+ }
399
428
  const indirectAgents = listIndirectAgents();
400
429
  if (indirectAgents.length > 0) {
401
430
  console.log(chalk.gray(`Indirect rules support (reads CLAUDE.md + AGENTS.md): ${indirectAgents.join(', ')}`));
@@ -474,6 +503,10 @@ ruleCommand.action(async (options) => {
474
503
  console.log();
475
504
  console.log(chalk.gray(`Unsupported agents (manual update required): ${unsupportedAgents.join(', ')}`));
476
505
  }
506
+ const perFileAgents = listPerFileAgents();
507
+ if (perFileAgents.length > 0) {
508
+ console.log(chalk.gray(`Per-file rules (.mdc): ${perFileAgents.join(', ')}`));
509
+ }
477
510
  const indirectAgents = listIndirectAgents();
478
511
  if (indirectAgents.length > 0) {
479
512
  console.log(chalk.gray(`Indirect rules support (reads CLAUDE.md + AGENTS.md): ${indirectAgents.join(', ')}`));
@@ -489,7 +522,7 @@ ruleCommand.action(async (options) => {
489
522
  // Commands library: manage and distribute commands
490
523
  const commandRoot = program
491
524
  .command('command')
492
- .description('Manage command library')
525
+ .description('Select slash commands interactively and distribute to agents')
493
526
  .option('-p, --profile <name>', 'Profile configuration to use')
494
527
  .option('--project <path>', 'Project directory containing .asb.toml');
495
528
  commandRoot.action(async (options) => {
@@ -638,7 +671,7 @@ commandRoot
638
671
  // Subagents library: scaffold, load, list, and interactive distribute
639
672
  const subagentRoot = program
640
673
  .command('subagent')
641
- .description('Manage subagent (persona) library')
674
+ .description('Select subagent definitions interactively and distribute to agents')
642
675
  .option('-p, --profile <name>', 'Profile configuration to use')
643
676
  .option('--project <path>', 'Project directory containing .asb.toml');
644
677
  subagentRoot.action(async (options) => {
@@ -671,6 +704,62 @@ subagentRoot.action(async (options) => {
671
704
  process.exit(1);
672
705
  }
673
706
  });
707
+ subagentRoot
708
+ .command('load')
709
+ .description('Import existing platform files into the subagent library')
710
+ .argument('<platform>', 'claude-code | opencode | cursor')
711
+ .argument('[path]', 'Source file or directory (defaults by platform)')
712
+ .option('-r, --recursive', 'When [path] is a directory, import files recursively')
713
+ .option('-f, --force', 'Overwrite existing library files without confirmation')
714
+ .action(async (platform, srcPath, opts) => {
715
+ try {
716
+ const exts = ['.md', '.markdown'];
717
+ const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultSubagentSourceDir(platform);
718
+ if (!fs.existsSync(source)) {
719
+ console.error(chalk.red(`\n✗ Source not found: ${source}`));
720
+ process.exit(1);
721
+ }
722
+ const inputs = [];
723
+ if (isFile(source)) {
724
+ inputs.push(source);
725
+ }
726
+ else if (isDir(source)) {
727
+ if (!opts.recursive) {
728
+ console.error(chalk.red('\n✗ Source is a directory. Use -r/--recursive to import recursively.'));
729
+ process.exit(1);
730
+ }
731
+ inputs.push(...listFilesRecursively(source, exts));
732
+ }
733
+ if (inputs.length === 0) {
734
+ console.log(chalk.yellow('\n⚠ No files to import.'));
735
+ return;
736
+ }
737
+ const outDir = getSubagentsDir();
738
+ let imported = 0;
739
+ for (const file of inputs) {
740
+ try {
741
+ const { slug, content } = importSubagentFromFile(platform, file);
742
+ const target = path.join(outDir, `${slug}.md`);
743
+ if (!(await confirmOverwrite(target, opts.force)))
744
+ continue;
745
+ writeFileSecure(target, content);
746
+ imported++;
747
+ console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(target)}`);
748
+ }
749
+ catch (error) {
750
+ const msg = error instanceof Error ? error.message : String(error);
751
+ console.log(`${chalk.red('✗')} ${chalk.dim(file)} ${chalk.red(msg)}`);
752
+ }
753
+ }
754
+ console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into subagent library.`);
755
+ }
756
+ catch (error) {
757
+ if (error instanceof Error) {
758
+ console.error(chalk.red(`\n✗ Error: ${error.message}`));
759
+ }
760
+ process.exit(1);
761
+ }
762
+ });
674
763
  subagentRoot
675
764
  .command('list')
676
765
  .description('Display subagent inventory and sync information')
@@ -728,66 +817,10 @@ subagentRoot
728
817
  process.exit(1);
729
818
  }
730
819
  });
731
- subagentRoot
732
- .command('load')
733
- .description('Import existing platform files into the subagent library')
734
- .argument('<platform>', 'claude-code | opencode | cursor')
735
- .argument('[path]', 'Source file or directory (defaults by platform)')
736
- .option('-r, --recursive', 'When [path] is a directory, import files recursively')
737
- .option('-f, --force', 'Overwrite existing library files without confirmation')
738
- .action(async (platform, srcPath, opts) => {
739
- try {
740
- const exts = ['.md', '.markdown'];
741
- const source = srcPath && srcPath.trim().length > 0 ? srcPath : defaultSubagentSourceDir(platform);
742
- if (!fs.existsSync(source)) {
743
- console.error(chalk.red(`\n✗ Source not found: ${source}`));
744
- process.exit(1);
745
- }
746
- const inputs = [];
747
- if (isFile(source)) {
748
- inputs.push(source);
749
- }
750
- else if (isDir(source)) {
751
- if (!opts.recursive) {
752
- console.error(chalk.red('\n✗ Source is a directory. Use -r/--recursive to import recursively.'));
753
- process.exit(1);
754
- }
755
- inputs.push(...listFilesRecursively(source, exts));
756
- }
757
- if (inputs.length === 0) {
758
- console.log(chalk.yellow('\n⚠ No files to import.'));
759
- return;
760
- }
761
- const outDir = getSubagentsDir();
762
- let imported = 0;
763
- for (const file of inputs) {
764
- try {
765
- const { slug, content } = importSubagentFromFile(platform, file);
766
- const target = path.join(outDir, `${slug}.md`);
767
- if (!(await confirmOverwrite(target, opts.force)))
768
- continue;
769
- writeFileSecure(target, content);
770
- imported++;
771
- console.log(`${chalk.green('✓')} ${chalk.cyan(slug)} → ${chalk.dim(target)}`);
772
- }
773
- catch (error) {
774
- const msg = error instanceof Error ? error.message : String(error);
775
- console.log(`${chalk.red('✗')} ${chalk.dim(file)} ${chalk.red(msg)}`);
776
- }
777
- }
778
- console.log(`\n${chalk.green('✓')} Imported ${imported} file(s) into subagent library.`);
779
- }
780
- catch (error) {
781
- if (error instanceof Error) {
782
- console.error(chalk.red(`\n✗ Error: ${error.message}`));
783
- }
784
- process.exit(1);
785
- }
786
- });
787
820
  // Skills library: manage and distribute skill bundles
788
821
  const skillRoot = program
789
822
  .command('skill')
790
- .description('Manage skill library')
823
+ .description('Select skill bundles interactively and distribute to agents')
791
824
  .option('-p, --profile <name>', 'Profile configuration to use')
792
825
  .option('--project <path>', 'Project directory containing .asb.toml');
793
826
  skillRoot.action(async (options) => {
@@ -1007,25 +1040,64 @@ function showSummary(selectedServers, scope) {
1007
1040
  }
1008
1041
  console.log();
1009
1042
  }
1010
- // Library subscription commands
1011
- program
1012
- .command('subscribe')
1013
- .description('Add a library subscription with a namespace')
1014
- .argument('<name>', 'Namespace for this subscription (e.g., "team", "project")')
1015
- .argument('<path>', 'Path to the library directory')
1016
- .action((name, libraryPath) => {
1043
+ // Library source management commands
1044
+ const sourceRoot = program
1045
+ .command('source')
1046
+ .description('Manage external library sources (local paths or git repos)');
1047
+ sourceRoot
1048
+ .command('add')
1049
+ .description('Add a library source (local path or git URL)')
1050
+ .argument('<location>', 'Local path or git URL (e.g., https://github.com/org/repo)')
1051
+ .argument('[name]', 'Namespace (defaults to repo or directory name)')
1052
+ .action((location, nameArg) => {
1017
1053
  try {
1018
- // Validate the path has library structure
1019
- const validation = validateSubscriptionPath(libraryPath);
1020
- if (!validation.valid) {
1021
- console.error(chalk.red(`\n✗ Path does not contain any library folders (rules/, commands/, subagents/, skills/).`));
1022
- process.exit(1);
1054
+ const name = nameArg ?? inferSourceName(location);
1055
+ if (isGitUrl(location)) {
1056
+ const parsed = parseGitUrl(location);
1057
+ const spinner = ora(`Cloning ${parsed.url}...`).start();
1058
+ try {
1059
+ addRemoteSource(name, {
1060
+ url: parsed.url,
1061
+ ref: parsed.ref,
1062
+ subdir: parsed.subdir,
1063
+ });
1064
+ spinner.succeed(chalk.green(`✓ Cloned ${parsed.url}`));
1065
+ }
1066
+ catch (err) {
1067
+ spinner.fail(chalk.red('Failed to clone'));
1068
+ throw err;
1069
+ }
1070
+ let effectivePath = getSourceCacheDir(name);
1071
+ if (parsed.subdir)
1072
+ effectivePath = path.join(effectivePath, parsed.subdir);
1073
+ const validation = validateSourcePath(effectivePath);
1074
+ if (!validation.valid) {
1075
+ removeSource(name);
1076
+ console.error(chalk.red('\n✗ Cloned repository does not contain any library folders (rules/, commands/, subagents/, skills/).'));
1077
+ process.exit(1);
1078
+ }
1079
+ console.log(chalk.green(`\n✓ Added source "${name}" from ${parsed.url}`));
1080
+ if (parsed.ref)
1081
+ console.log(chalk.dim(` Ref: ${parsed.ref}`));
1082
+ if (parsed.subdir)
1083
+ 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(', ')}`));
1087
+ }
1023
1088
  }
1024
- addSubscription(name, libraryPath);
1025
- console.log(chalk.green(`\n✓ Subscribed to "${name}" at ${path.resolve(libraryPath)}`));
1026
- console.log(chalk.dim(` Found: ${validation.found.join(', ')}`));
1027
- if (validation.missing.length > 0) {
1028
- console.log(chalk.dim(` Missing: ${validation.missing.join(', ')}`));
1089
+ else {
1090
+ const validation = validateSourcePath(location);
1091
+ if (!validation.valid) {
1092
+ console.error(chalk.red('\n✗ Path does not contain any library folders (rules/, commands/, subagents/, skills/).'));
1093
+ process.exit(1);
1094
+ }
1095
+ addLocalSource(name, location);
1096
+ 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(', ')}`));
1100
+ }
1029
1101
  }
1030
1102
  console.log();
1031
1103
  console.log(chalk.dim('Library entries will now use the namespace prefix, e.g., ') +
@@ -1038,14 +1110,21 @@ program
1038
1110
  process.exit(1);
1039
1111
  }
1040
1112
  });
1041
- program
1042
- .command('unsubscribe')
1043
- .description('Remove a library subscription by namespace')
1113
+ sourceRoot
1114
+ .command('remove')
1115
+ .description('Remove a library source by namespace')
1044
1116
  .argument('<name>', 'Namespace to remove')
1045
1117
  .action((name) => {
1046
1118
  try {
1047
- removeSubscription(name);
1048
- console.log(chalk.green(`\n✓ Unsubscribed from "${name}"`));
1119
+ const sources = getSources();
1120
+ const source = sources.find((s) => s.namespace === name);
1121
+ removeSource(name);
1122
+ if (source?.remote) {
1123
+ console.log(chalk.green(`\n✓ Removed source "${name}" and cleaned up cache`));
1124
+ }
1125
+ else {
1126
+ console.log(chalk.green(`\n✓ Removed source "${name}"`));
1127
+ }
1049
1128
  }
1050
1129
  catch (error) {
1051
1130
  if (error instanceof Error) {
@@ -1054,32 +1133,45 @@ program
1054
1133
  process.exit(1);
1055
1134
  }
1056
1135
  });
1057
- program
1058
- .command('subscriptions')
1059
- .description('List all library subscriptions')
1136
+ sourceRoot
1137
+ .command('list')
1138
+ .description('List all library sources')
1060
1139
  .option('--json', 'Output as JSON')
1061
1140
  .action((options) => {
1062
1141
  try {
1063
- const subscriptions = getSubscriptions();
1142
+ const sources = getSources();
1064
1143
  if (options.json) {
1065
- console.log(JSON.stringify(subscriptions, null, 2));
1144
+ console.log(JSON.stringify(sources, null, 2));
1066
1145
  return;
1067
1146
  }
1068
- if (subscriptions.length === 0) {
1069
- console.log(chalk.yellow('\n⚠ No library subscriptions configured.'));
1070
- console.log(chalk.dim(' Use `asb subscribe <name> <path>` to add one.'));
1147
+ if (sources.length === 0) {
1148
+ console.log(chalk.yellow('\n⚠ No library sources configured.'));
1149
+ console.log(chalk.dim(' Use `asb source add <location> [name]` to add one.'));
1071
1150
  return;
1072
1151
  }
1073
- console.log(chalk.blue('\nLibrary subscriptions:'));
1074
- const header = ['Namespace', 'Path', 'Status', 'Contains'];
1075
- const rows = subscriptions.map((sub) => {
1076
- const exists = fs.existsSync(sub.path);
1077
- const validation = exists ? validateSubscriptionPath(sub.path) : { found: [], missing: [] };
1078
- const statusPlain = exists ? 'ok' : 'missing';
1152
+ console.log(chalk.blue('\nLibrary sources:'));
1153
+ const header = ['Namespace', 'Type', 'Source', 'Status', 'Contains'];
1154
+ const rows = sources.map((src) => {
1155
+ const isRemote = !!src.remote;
1156
+ const typePlain = isRemote ? 'remote' : 'local';
1157
+ const sourcePlain = isRemote ? (src.remote?.url ?? src.path) : src.path;
1158
+ const exists = fs.existsSync(src.path);
1159
+ const validation = exists ? validateSourcePath(src.path) : { found: [], missing: [] };
1160
+ let statusPlain;
1161
+ if (isRemote) {
1162
+ statusPlain = exists ? 'cached' : 'not cached';
1163
+ }
1164
+ else {
1165
+ statusPlain = exists ? 'ok' : 'missing';
1166
+ }
1079
1167
  const containsPlain = validation.found.length > 0 ? validation.found.join(', ') : '-';
1080
1168
  return [
1081
- { plain: sub.namespace, formatted: chalk.cyan(sub.namespace) },
1082
- { plain: sub.path, formatted: chalk.dim(sub.path) },
1169
+ { plain: src.namespace, formatted: chalk.cyan(src.namespace) },
1170
+ {
1171
+ plain: typePlain,
1172
+ formatted: isRemote ? chalk.blue(typePlain) : chalk.gray(typePlain),
1173
+ },
1174
+ { plain: sourcePlain, formatted: chalk.dim(sourcePlain) },
1083
1175
  {
1084
1176
  plain: statusPlain,
1085
1177
  formatted: exists ? chalk.green(statusPlain) : chalk.red(statusPlain),
@@ -1097,5 +1189,8 @@ program
1097
1189
  process.exit(1);
1098
1190
  }
1099
1191
  });
1192
+ sourceRoot.action(() => {
1193
+ sourceRoot.commands.find((c) => c.name() === 'list')?.parse(process.argv);
1194
+ });
1100
1195
  program.parse(process.argv);
1101
1196
  //# sourceMappingURL=index.js.map