@kinqs/brainrouter-cli 0.3.6 → 0.3.8

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 (129) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/changelog/0.2.0.md +15 -0
  8. package/changelog/0.3.0.md +20 -0
  9. package/changelog/0.3.1.md +22 -0
  10. package/changelog/0.3.2.md +15 -0
  11. package/changelog/0.3.3.md +19 -0
  12. package/changelog/0.3.4.md +20 -0
  13. package/changelog/0.3.5.md +9 -0
  14. package/changelog/0.3.6.md +9 -0
  15. package/changelog/0.3.7.md +20 -0
  16. package/changelog/0.3.8.md +30 -0
  17. package/changelog/README.md +41 -0
  18. package/dist/agent/agent.d.ts +34 -1
  19. package/dist/agent/agent.js +372 -79
  20. package/dist/agent/toolCallRecovery.d.ts +57 -0
  21. package/dist/agent/toolCallRecovery.js +130 -0
  22. package/dist/agent/toolSafety.d.ts +17 -0
  23. package/dist/agent/toolSafety.js +102 -0
  24. package/dist/cli/banner.d.ts +20 -0
  25. package/dist/cli/banner.js +47 -14
  26. package/dist/cli/cliPrompt.d.ts +40 -3
  27. package/dist/cli/cliPrompt.js +117 -25
  28. package/dist/cli/commands/_context.d.ts +3 -1
  29. package/dist/cli/commands/_helpers.d.ts +1 -1
  30. package/dist/cli/commands/config.d.ts +46 -0
  31. package/dist/cli/commands/config.js +1042 -0
  32. package/dist/cli/commands/init.d.ts +20 -0
  33. package/dist/cli/commands/init.js +64 -0
  34. package/dist/cli/commands/login.d.ts +13 -0
  35. package/dist/cli/commands/login.js +179 -0
  36. package/dist/cli/commands/mcp.d.ts +13 -11
  37. package/dist/cli/commands/mcp.js +261 -74
  38. package/dist/cli/commands/mcpInstall.d.ts +20 -0
  39. package/dist/cli/commands/mcpInstall.js +87 -0
  40. package/dist/cli/commands/orchestration.js +51 -0
  41. package/dist/cli/commands/releaseNotes.d.ts +24 -0
  42. package/dist/cli/commands/releaseNotes.js +109 -0
  43. package/dist/cli/commands/schedule.d.ts +18 -0
  44. package/dist/cli/commands/schedule.js +189 -0
  45. package/dist/cli/commands/ui.js +119 -60
  46. package/dist/cli/commands/workflow.d.ts +2 -0
  47. package/dist/cli/commands/workflow.js +54 -8
  48. package/dist/cli/ink/ChatApp.d.ts +206 -0
  49. package/dist/cli/ink/ChatApp.js +493 -0
  50. package/dist/cli/ink/Frame.d.ts +26 -0
  51. package/dist/cli/ink/Frame.js +5 -0
  52. package/dist/cli/ink/Picker.d.ts +71 -0
  53. package/dist/cli/ink/Picker.js +168 -0
  54. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  55. package/dist/cli/ink/SlashPalette.js +136 -0
  56. package/dist/cli/ink/TextField.d.ts +34 -0
  57. package/dist/cli/ink/TextField.js +47 -0
  58. package/dist/cli/ink/WizardApp.d.ts +7 -0
  59. package/dist/cli/ink/WizardApp.js +422 -0
  60. package/dist/cli/ink/ambientChat.d.ts +34 -0
  61. package/dist/cli/ink/ambientChat.js +7 -0
  62. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  63. package/dist/cli/ink/consoleCapture.js +33 -0
  64. package/dist/cli/ink/markdownRender.d.ts +41 -0
  65. package/dist/cli/ink/markdownRender.js +278 -0
  66. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  67. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  68. package/dist/cli/ink/runChat.d.ts +34 -0
  69. package/dist/cli/ink/runChat.js +682 -0
  70. package/dist/cli/ink/runPicker.d.ts +31 -0
  71. package/dist/cli/ink/runPicker.js +139 -0
  72. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  73. package/dist/cli/ink/runSlashPalette.js +33 -0
  74. package/dist/cli/ink/runWizard.d.ts +22 -0
  75. package/dist/cli/ink/runWizard.js +133 -0
  76. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  77. package/dist/cli/ink/stdinHandoff.js +78 -0
  78. package/dist/cli/ink/toolFormat.d.ts +75 -0
  79. package/dist/cli/ink/toolFormat.js +206 -0
  80. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  81. package/dist/cli/ink/useTerminalSize.js +26 -0
  82. package/dist/cli/repl.d.ts +25 -3
  83. package/dist/cli/repl.js +52 -714
  84. package/dist/cli/slashSuggest.d.ts +32 -0
  85. package/dist/cli/slashSuggest.js +146 -0
  86. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  87. package/dist/cli/wizard/modelsApi.js +166 -0
  88. package/dist/cli/wizard/picker.d.ts +202 -0
  89. package/dist/cli/wizard/picker.js +547 -0
  90. package/dist/cli/wizard/providers.d.ts +86 -0
  91. package/dist/cli/wizard/providers.js +190 -0
  92. package/dist/cli/wizard/runner.d.ts +13 -0
  93. package/dist/cli/wizard/runner.js +488 -0
  94. package/dist/cli/wizard/types.d.ts +122 -0
  95. package/dist/cli/wizard/types.js +109 -0
  96. package/dist/config/config.d.ts +13 -1
  97. package/dist/config/config.js +45 -3
  98. package/dist/index.js +157 -206
  99. package/dist/memory/briefing.d.ts +1 -1
  100. package/dist/memory/briefing.js +4 -4
  101. package/dist/memory/consolidation.d.ts +1 -1
  102. package/dist/orchestration/agentRegistry.d.ts +36 -0
  103. package/dist/orchestration/agentRegistry.js +64 -0
  104. package/dist/orchestration/orchestrator.d.ts +7 -0
  105. package/dist/orchestration/orchestrator.js +2 -0
  106. package/dist/orchestration/tools.d.ts +105 -3
  107. package/dist/orchestration/tools.js +167 -8
  108. package/dist/prompt/skillCatalog.d.ts +11 -0
  109. package/dist/prompt/skillCatalog.js +134 -0
  110. package/dist/prompt/skillRunner.d.ts +2 -2
  111. package/dist/prompt/skillRunner.js +2 -31
  112. package/dist/prompt/systemPrompt.js +7 -2
  113. package/dist/runtime/anthropicAdapter.d.ts +100 -0
  114. package/dist/runtime/anthropicAdapter.js +293 -0
  115. package/dist/runtime/cronParser.d.ts +23 -0
  116. package/dist/runtime/cronParser.js +122 -0
  117. package/dist/runtime/mcpClient.js +14 -11
  118. package/dist/runtime/mcpPool.d.ts +170 -0
  119. package/dist/runtime/mcpPool.js +442 -0
  120. package/dist/runtime/mcpUtils.d.ts +17 -1
  121. package/dist/runtime/mcpUtils.js +23 -0
  122. package/dist/runtime/scheduleTicker.d.ts +33 -0
  123. package/dist/runtime/scheduleTicker.js +99 -0
  124. package/dist/runtime/vendorSnippets.d.ts +45 -0
  125. package/dist/runtime/vendorSnippets.js +153 -0
  126. package/dist/state/scheduleStore.d.ts +37 -0
  127. package/dist/state/scheduleStore.js +64 -0
  128. package/package.json +14 -5
  129. package/.env.example +0 -116
@@ -1,53 +1,88 @@
1
1
  /**
2
- * 0.3.6 item 11: `/mcp` slash-command surface. Scope-limited foundation
3
- * the full multi-MCP federation (parallel cross-MCP tool calls, MCP
4
- * marketplace, capability tiers) is deferred to 0.4.0. What ships here:
2
+ * `/mcp` slash-command surface. 0.3.7 multi-MCP rewrite third-party
3
+ * MCPs connect concurrently while BrainRouter MCP profiles are mutually
4
+ * exclusive so exactly one brain is active at a time.
5
5
  *
6
- * /mcp show status of the active MCP (alias for /mcp list)
7
- * /mcp list list every configured profile with identity + status
8
- * /mcp reconnectreconnect the currently-active profile
9
- * /mcp tools — list MCP tools grouped by namespace (pre-Item-11
10
- * `/mcp` no-arg behaviour, moved here verbatim)
6
+ * /mcp — alias for /mcp list
7
+ * /mcp list — every configured profile + per-server status
8
+ * /mcp tools [server] MCP tools grouped by `mcp_<server>_*`
9
+ * namespace; pass a server id to scope
10
+ * /mcp connect <name> — connect a configured server that's idle/offline
11
+ * /mcp disconnect <name> — close one server's transport (config preserved)
12
+ * /mcp reconnect [name] — reconnect ONE (when name given) or the selected pool
11
13
  *
12
- * The reconnect path leans on `mcpClient.close()` + `mcpClient.connect()`
13
- * with the same config the CLI launched against no plumbing required
14
- * for the user beyond typing the command.
14
+ * Backwards-compat: `/mcp reconnect` with no arg reconnects the same
15
+ * selected set used at boot: all third-party MCPs plus the active
16
+ * BrainRouter MCP. Pass an explicit name to target one.
15
17
  */
16
18
  import chalk from 'chalk';
17
19
  import { spinner as makeSpinner } from '../spinner.js';
20
+ import { saveConfig } from '../../config/config.js';
21
+ import { resolveIdentityFromConfig } from '../../runtime/mcpClient.js';
22
+ import { selectMcpServerIds } from '../../runtime/mcpPool.js';
23
+ import { buildBannerInputs, renderBanner } from '../banner.js';
24
+ import { resolveTheme } from '../theme.js';
25
+ import { runMcpInstall } from './mcpInstall.js';
18
26
  export async function tryHandleMcpCommand(ctx) {
19
27
  const { command, args, mcpClient, config } = ctx;
20
28
  if (command !== '/mcp')
21
29
  return false;
22
30
  const sub = (args[0] ?? 'list').toLowerCase();
31
+ const targetName = args[1]?.trim();
23
32
  if (sub === 'tools') {
24
- // Pre-Item-11 `/mcp` no-arg behaviour: namespace-grouped tool listing.
25
- // Kept verbatim under a subcommand so the muscle memory survives.
26
- const profileName = config.activeServer;
27
- const server = config.servers[profileName];
28
- console.log(chalk.bold('\nMCP server'));
29
- console.log(` Profile: ${chalk.green(profileName)} (${chalk.cyan(server?.type ?? 'unknown')})`);
30
- if (server?.type === 'http') {
31
- console.log(` URL: ${chalk.blue(server.url ?? '')}`);
32
- }
33
- else if (server?.type === 'stdio') {
34
- console.log(` Cmd: ${chalk.blue(server.command ?? '')} ${server.args?.join(' ') || ''}`);
35
- }
36
- const spinner = makeSpinner(chalk.gray('Fetching MCP tool surface...')).start();
33
+ const onlyServer = targetName;
34
+ console.log(chalk.bold('\nMCP tools (pooled)'));
35
+ const statuses = mcpClient.getStatuses();
36
+ const connected = statuses.filter((s) => s.status === 'connected');
37
+ if (connected.length === 0) {
38
+ console.log(chalk.yellow(' No MCP servers connected. Try /mcp reconnect.\n'));
39
+ return true;
40
+ }
41
+ const spinner = makeSpinner(chalk.gray('Fetching pooled tool surface...')).start();
37
42
  try {
38
43
  const res = await mcpClient.listTools();
39
- const tools = res.tools || [];
40
- spinner.succeed(chalk.green(`${tools.length} MCP tools available`));
41
- const namespaces = {};
42
- for (const t of tools) {
43
- const parts = (t.name || '').split('_');
44
- const ns = parts.length > 1 ? parts[0] : 'misc';
45
- (namespaces[ns] ||= []).push(t.name);
44
+ const allTools = res.tools || [];
45
+ spinner.succeed(chalk.green(`${allTools.length} tools across ${connected.length} server${connected.length === 1 ? '' : 's'}`));
46
+ // Pool tools are exposed as `mcp_<serverId>_<rawTool>`. Group by serverId.
47
+ const knownIds = new Set(connected.map((s) => s.serverId));
48
+ const byServer = {};
49
+ for (const t of allTools) {
50
+ let serverId = '__unknown__';
51
+ let raw = t.name;
52
+ if (t.name.startsWith('mcp_')) {
53
+ const rest = t.name.slice('mcp_'.length);
54
+ // Server ids may contain underscores; match the longest known id.
55
+ const id = [...knownIds].sort((a, b) => b.length - a.length).find((k) => rest.startsWith(`${k}_`));
56
+ if (id) {
57
+ serverId = id;
58
+ raw = rest.slice(id.length + 1);
59
+ }
60
+ else {
61
+ const idx = rest.indexOf('_');
62
+ if (idx > 0) {
63
+ serverId = rest.slice(0, idx);
64
+ raw = rest.slice(idx + 1);
65
+ }
66
+ }
67
+ }
68
+ if (onlyServer && serverId !== onlyServer)
69
+ continue;
70
+ (byServer[serverId] ||= []).push(raw);
71
+ }
72
+ const serverIds = Object.keys(byServer).sort();
73
+ if (serverIds.length === 0) {
74
+ console.log(chalk.gray(`\n No tools for server "${onlyServer}".\n`));
75
+ return true;
46
76
  }
47
- for (const ns of Object.keys(namespaces).sort()) {
48
- console.log(`\n ${chalk.bold.cyan(ns)} (${namespaces[ns].length})`);
49
- for (const name of namespaces[ns].sort()) {
50
- console.log(` ${chalk.gray('')} ${name}`);
77
+ for (const section of groupServerIdsByEcosystem(serverIds, (id) => mcpClient.getStatus(id)?.identity ?? 'unknown')) {
78
+ console.log(`\n${section.title}`);
79
+ for (const id of section.ids) {
80
+ const ident = mcpClient.getStatus(id)?.identity ?? 'unknown';
81
+ const identTag = formatIdentityTag(ident);
82
+ console.log(` ${chalk.bold.green(id)} ${identTag} (${byServer[id].length})`);
83
+ for (const name of byServer[id].sort()) {
84
+ console.log(` ${chalk.gray('•')} ${name} ${chalk.gray(`mcp_${id}_${name}`)}`);
85
+ }
51
86
  }
52
87
  }
53
88
  }
@@ -58,64 +93,216 @@ export async function tryHandleMcpCommand(ctx) {
58
93
  return true;
59
94
  }
60
95
  if (sub === 'list') {
61
- const activeName = config.activeServer;
62
96
  const profiles = Object.keys(config.servers ?? {});
63
97
  if (profiles.length === 0) {
64
- console.log(chalk.yellow('\nNo MCP profiles configured. Run `brainrouter login` or `brainrouter config` to set one up.\n'));
98
+ console.log(chalk.yellow('\nNo MCP profiles configured. Run `/login` or `brainrouter config` to set one up.\n'));
65
99
  return true;
66
100
  }
67
- console.log(chalk.bold('\nConfigured MCP profiles'));
68
- for (const name of profiles) {
101
+ console.log(chalk.bold('\nMCP servers'));
102
+ const statuses = mcpClient.getStatuses();
103
+ const statusById = new Map(statuses.map((s) => [s.serverId, s]));
104
+ const activeName = config.activeServer;
105
+ for (const section of groupServerIdsByEcosystem(profiles, (name) => {
69
106
  const profile = config.servers[name];
70
- const isActive = name === activeName;
71
- // Identity: explicit config field > live wrapper > 'unknown'.
72
- let identity = profile.identity ?? 'unknown';
73
- if (isActive && typeof mcpClient.getIdentity === 'function') {
74
- identity = mcpClient.getIdentity();
107
+ return statusById.get(name)?.identity ?? profile?.identity ?? 'unknown';
108
+ })) {
109
+ console.log(`\n${section.title}`);
110
+ for (const name of section.ids) {
111
+ const profile = config.servers[name];
112
+ const poolStatus = statusById.get(name);
113
+ // Identity: pool live > config metadata > 'unknown'.
114
+ const identity = poolStatus?.identity ?? profile.identity ?? 'unknown';
115
+ // Status: pool live > 'not in pool' (configured but never tried).
116
+ const liveStatus = poolStatus?.status;
117
+ const statusLabel = liveStatus === 'connected' ? chalk.green('online') :
118
+ liveStatus === 'failed' ? chalk.red('failed') :
119
+ liveStatus === 'connecting' ? chalk.yellow('connecting') :
120
+ liveStatus === 'offline' ? chalk.gray('disconnected') :
121
+ chalk.gray('idle');
122
+ const idLabel = formatIdentityTag(identity);
123
+ const marker = name === activeName ? chalk.bold('★ ') : ' ';
124
+ const transport = profile.type;
125
+ const target = profile.type === 'http' ? profile.url ?? '<no url>' : profile.command ?? '<no command>';
126
+ const toolTag = poolStatus?.toolCount != null ? chalk.gray(`${poolStatus.toolCount} tools`) : '';
127
+ const errTag = liveStatus === 'failed' && poolStatus?.error ? chalk.red(` · ${poolStatus.error}`) : '';
128
+ console.log(`${marker}${chalk.bold(name)} ${idLabel} ${transport} ${statusLabel} ${chalk.gray(target)} ${toolTag}${errTag}`);
75
129
  }
76
- const onlineLabel = isActive
77
- ? (mcpClient.isConnected() ? chalk.green('online') : chalk.red('offline'))
78
- : chalk.gray('idle');
79
- const idLabel = identity === 'brainrouter'
80
- ? chalk.cyan('brainrouter')
81
- : identity === 'third-party'
82
- ? chalk.yellow('third-party')
83
- : chalk.gray('unknown');
84
- const marker = isActive ? chalk.bold('★ ') : ' ';
85
- const transport = profile.type;
86
- const target = profile.type === 'http' ? profile.url ?? '<no url>' : profile.command ?? '<no command>';
87
- console.log(`${marker}${chalk.bold(name)} ${idLabel} ${transport} ${onlineLabel} ${chalk.gray(target)}`);
88
- }
89
- console.log(chalk.gray('\n★ = active profile. /mcp reconnect to refresh the active connection.\n'));
130
+ }
131
+ console.log(chalk.gray('\n★ = highlighted profile in the banner.'));
132
+ console.log(chalk.gray(' Multi-MCP: third-party MCPs connect together; only one BrainRouter MCP is active. Use /mcp connect|disconnect|reconnect <name> to manage.\n'));
90
133
  return true;
91
134
  }
92
135
  if (sub === 'reconnect') {
93
- const activeName = config.activeServer;
94
- const profile = config.servers?.[activeName];
95
- if (!profile) {
96
- console.log(chalk.red(`\nNo active MCP profile to reconnect (\`activeServer\` = ${JSON.stringify(activeName)}).\n`));
136
+ if (!targetName) {
137
+ // No name → reconnect every configured server in the pool.
138
+ const ids = selectMcpServerIds(config.servers ?? {}, config.activeServer);
139
+ if (ids.length === 0) {
140
+ console.log(chalk.red(`\nNo MCP profiles configured.\n`));
141
+ return true;
142
+ }
143
+ console.log(chalk.gray(`Reconnecting all servers (${ids.length})…`));
144
+ await Promise.allSettled(ids.map(async (id) => {
145
+ try {
146
+ await mcpClient.reconnectOne(id);
147
+ const s = mcpClient.getStatus(id);
148
+ if (s?.status === 'connected') {
149
+ console.log(chalk.green(` ✓ ${id}`));
150
+ }
151
+ else {
152
+ console.log(chalk.red(` ✗ ${id} — ${s?.error ?? 'failed'}`));
153
+ }
154
+ }
155
+ catch (err) {
156
+ console.log(chalk.red(` ✗ ${id} — ${err?.message ?? err}`));
157
+ }
158
+ }));
159
+ console.log();
160
+ return true;
161
+ }
162
+ if (!config.servers?.[targetName]) {
163
+ console.log(chalk.red(`\nNo profile named "${targetName}".\n`));
97
164
  return true;
98
165
  }
99
- console.log(chalk.gray(`Reconnecting "${activeName}"…`));
166
+ console.log(chalk.gray(`Reconnecting "${targetName}"…`));
100
167
  try {
101
- try {
102
- await mcpClient.close();
168
+ await disconnectOtherBrainrouterServers(ctx, targetName);
169
+ await mcpClient.reconnectOne(targetName);
170
+ const s = mcpClient.getStatus(targetName);
171
+ if (s?.status === 'connected') {
172
+ const activated = activateBrainrouterProfile(ctx, targetName);
173
+ console.log(chalk.green(`✓ Reconnected to "${targetName}".\n`));
174
+ if (activated) {
175
+ console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
176
+ printRefreshedBanner(ctx);
177
+ }
103
178
  }
104
- catch { /* idempotent */ }
105
- await mcpClient.connect(profile, config.llm, activeName);
106
- // Re-probe tools so identity tagging and the prompt's tool list refresh.
107
- try {
108
- await mcpClient.listTools();
179
+ else {
180
+ console.log(chalk.red(`✗ "${targetName}" remained ${s?.status ?? 'offline'} — ${s?.error ?? 'unknown'}\n`));
109
181
  }
110
- catch { /* tool-list failure is non-fatal */ }
111
- console.log(chalk.green(`✓ Reconnected to "${activeName}" (${profile.type}).\n`));
112
182
  }
113
183
  catch (err) {
114
184
  console.log(chalk.red(`✗ Reconnect failed: ${err?.message ?? err}\n`));
115
- console.log(chalk.gray('The CLI stays in offline mode. Check the MCP server, then try `/mcp reconnect` again.\n'));
116
185
  }
117
186
  return true;
118
187
  }
119
- console.log(chalk.red(`\nUnknown /mcp subcommand "${sub}". Usage: /mcp list | /mcp reconnect | /mcp tools\n`));
188
+ if (sub === 'connect') {
189
+ if (!targetName) {
190
+ console.log(chalk.red('\nUsage: /mcp connect <name>\n'));
191
+ return true;
192
+ }
193
+ const profile = config.servers?.[targetName];
194
+ if (!profile) {
195
+ console.log(chalk.red(`\nNo profile named "${targetName}". Available: ${Object.keys(config.servers ?? {}).join(', ') || '(none)'}.\n`));
196
+ return true;
197
+ }
198
+ console.log(chalk.gray(`Connecting "${targetName}"…`));
199
+ try {
200
+ await disconnectOtherBrainrouterServers(ctx, targetName);
201
+ await mcpClient.connectOne(targetName, profile, config.llm, 5_000);
202
+ const s = mcpClient.getStatus(targetName);
203
+ if (s?.status === 'connected') {
204
+ const activated = activateBrainrouterProfile(ctx, targetName);
205
+ console.log(chalk.green(`✓ "${targetName}" online (${s.toolCount ?? 0} tools).\n`));
206
+ if (activated) {
207
+ console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
208
+ printRefreshedBanner(ctx);
209
+ }
210
+ }
211
+ else {
212
+ console.log(chalk.red(`✗ "${targetName}" failed — ${s?.error ?? 'unknown'}\n`));
213
+ }
214
+ }
215
+ catch (err) {
216
+ console.log(chalk.red(`✗ Connect failed: ${err?.message ?? err}\n`));
217
+ }
218
+ return true;
219
+ }
220
+ if (sub === 'install') {
221
+ const result = runMcpInstall(args.slice(1), config);
222
+ console.log(result.output);
223
+ return true;
224
+ }
225
+ if (sub === 'disconnect') {
226
+ if (!targetName) {
227
+ console.log(chalk.red('\nUsage: /mcp disconnect <name>\n'));
228
+ return true;
229
+ }
230
+ if (!mcpClient.getStatus(targetName)) {
231
+ console.log(chalk.yellow(`\n"${targetName}" is not in the pool.\n`));
232
+ return true;
233
+ }
234
+ try {
235
+ await mcpClient.disconnectOne(targetName);
236
+ console.log(chalk.green(`✓ "${targetName}" disconnected. Config preserved — /mcp connect ${targetName} to bring it back.\n`));
237
+ }
238
+ catch (err) {
239
+ console.log(chalk.red(`✗ Disconnect failed: ${err?.message ?? err}\n`));
240
+ }
241
+ return true;
242
+ }
243
+ console.log(chalk.red(`\nUnknown /mcp subcommand "${sub}". Usage: /mcp list | /mcp tools [server] | /mcp connect <name> | /mcp disconnect <name> | /mcp reconnect [name] | /mcp install <vendor>|list\n`));
244
+ return true;
245
+ }
246
+ function formatIdentityTag(identity) {
247
+ return identity === 'brainrouter' ? chalk.cyan('brainrouter') :
248
+ identity === 'third-party' ? chalk.yellow('third-party') :
249
+ chalk.gray('unknown');
250
+ }
251
+ function groupServerIdsByEcosystem(ids, identityFor) {
252
+ const brainrouter = [];
253
+ const other = [];
254
+ for (const id of ids.slice().sort()) {
255
+ if (identityFor(id) === 'brainrouter')
256
+ brainrouter.push(id);
257
+ else
258
+ other.push(id);
259
+ }
260
+ const sections = [];
261
+ if (brainrouter.length > 0) {
262
+ sections.push({ title: chalk.bold.cyan('BrainRouter MCP (Our Ecosystem)'), ids: brainrouter });
263
+ }
264
+ if (other.length > 0) {
265
+ sections.push({ title: chalk.bold.yellow('Third-party MCPs (Other)'), ids: other });
266
+ }
267
+ return sections;
268
+ }
269
+ async function disconnectOtherBrainrouterServers(ctx, targetName) {
270
+ const targetProfile = ctx.config.servers?.[targetName];
271
+ if (!targetProfile)
272
+ return;
273
+ if (resolveIdentityFromConfig(targetProfile, targetName) !== 'brainrouter')
274
+ return;
275
+ for (const [id, profile] of Object.entries(ctx.config.servers ?? {})) {
276
+ if (id === targetName)
277
+ continue;
278
+ if (resolveIdentityFromConfig(profile, id) !== 'brainrouter')
279
+ continue;
280
+ const status = ctx.mcpClient.getStatus(id);
281
+ if (status?.status === 'connected' || status?.status === 'connecting') {
282
+ await ctx.mcpClient.disconnectOne(id);
283
+ }
284
+ }
285
+ }
286
+ function activateBrainrouterProfile(ctx, targetName) {
287
+ const status = ctx.mcpClient.getStatus(targetName);
288
+ const profile = ctx.config.servers?.[targetName];
289
+ const isBrainrouter = status?.identity === 'brainrouter' ||
290
+ (profile ? resolveIdentityFromConfig(profile, targetName) === 'brainrouter' : false);
291
+ if (!isBrainrouter)
292
+ return false;
293
+ ctx.config.activeServer = targetName;
294
+ saveConfig(ctx.config);
120
295
  return true;
121
296
  }
297
+ function printRefreshedBanner(ctx) {
298
+ const theme = resolveTheme(ctx.agent.workspaceRoot);
299
+ const banner = renderBanner(buildBannerInputs(ctx.config, ctx.agent, ctx.mcpClient), theme);
300
+ if (ctx.repl.replaceBanner) {
301
+ ctx.repl.replaceBanner('\n' + banner);
302
+ }
303
+ else {
304
+ console.log(chalk.gray('Updated active BrainRouter banner:'));
305
+ console.log(banner);
306
+ console.log();
307
+ }
308
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `/mcp install` — generates per-vendor MCP config snippets for non-CLI
3
+ * hosts (Claude Desktop, Cursor, Windsurf, VS Code Continue, Zed, Cline).
4
+ *
5
+ * Pattern: print-only. We do NOT write to vendor config files — the user
6
+ * pastes the block themselves. Direct-write is a future enhancement
7
+ * (roadmap: tracked under post-0.4.0 polish; intentionally not a follow-up).
8
+ *
9
+ * Adapted from semble's per-agent install docs pattern
10
+ * (openSrc/semble/src/semble/agents/) — one focused entry per vendor.
11
+ */
12
+ import type { Config } from '../../config/config.js';
13
+ export interface RenderResult {
14
+ ok: boolean;
15
+ output: string;
16
+ }
17
+ export interface RunOpts {
18
+ platform?: NodeJS.Platform;
19
+ }
20
+ export declare function runMcpInstall(args: string[], config: Config, opts?: RunOpts): RenderResult;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `/mcp install` — generates per-vendor MCP config snippets for non-CLI
3
+ * hosts (Claude Desktop, Cursor, Windsurf, VS Code Continue, Zed, Cline).
4
+ *
5
+ * Pattern: print-only. We do NOT write to vendor config files — the user
6
+ * pastes the block themselves. Direct-write is a future enhancement
7
+ * (roadmap: tracked under post-0.4.0 polish; intentionally not a follow-up).
8
+ *
9
+ * Adapted from semble's per-agent install docs pattern
10
+ * (openSrc/semble/src/semble/agents/) — one focused entry per vendor.
11
+ */
12
+ import chalk from 'chalk';
13
+ import { displayPath, getVendor, listVendors, renderSnippet, VENDORS } from '../../runtime/vendorSnippets.js';
14
+ /**
15
+ * Resolve the active BrainRouter profile from config. Returns null when
16
+ * the user hasn't logged in yet — caller prints a `/login` hint.
17
+ */
18
+ function resolveActiveBrainrouter(config) {
19
+ const name = config.activeServer;
20
+ if (!name)
21
+ return null;
22
+ const profile = config.servers?.[name];
23
+ if (!profile)
24
+ return null;
25
+ if (profile.type !== 'http')
26
+ return null;
27
+ const url = profile.url?.trim();
28
+ const apiKey = profile.apiKey?.trim();
29
+ if (!url || !apiKey)
30
+ return null;
31
+ return { url, apiKey };
32
+ }
33
+ export function runMcpInstall(args, config, opts = {}) {
34
+ const platform = opts.platform ?? process.platform;
35
+ const sub = (args[0] ?? '').toLowerCase();
36
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
37
+ return {
38
+ ok: true,
39
+ output: `${chalk.bold('Usage')}\n` +
40
+ ` /mcp install list — list supported vendors\n` +
41
+ ` /mcp install <vendor> — print paste-ready snippet for one vendor\n\n` +
42
+ `Vendors: ${listVendors().map((v) => v.id).join(', ')}\n`,
43
+ };
44
+ }
45
+ if (sub === 'list') {
46
+ const lines = [chalk.bold('\nSupported MCP hosts')];
47
+ for (const v of listVendors()) {
48
+ const p = displayPath(v.configPath(platform), platform);
49
+ lines.push(` ${chalk.bold.cyan(v.id.padEnd(18))} ${chalk.gray(v.label.padEnd(28))} ${chalk.gray(p)}`);
50
+ }
51
+ lines.push('', chalk.gray(`Run "/mcp install <id>" for a paste-ready snippet.`), '');
52
+ return { ok: true, output: lines.join('\n') };
53
+ }
54
+ const entry = getVendor(sub);
55
+ if (!entry) {
56
+ return {
57
+ ok: false,
58
+ output: chalk.red(`\nUnknown vendor "${sub}".\n`) +
59
+ chalk.gray(`Known: ${Object.keys(VENDORS).join(', ')}\n`),
60
+ };
61
+ }
62
+ const active = resolveActiveBrainrouter(config);
63
+ if (!active) {
64
+ return {
65
+ ok: false,
66
+ output: chalk.red('\nNo active BrainRouter profile with URL + API key.\n') +
67
+ chalk.gray('Run `/login` to configure one, then re-run this command.\n'),
68
+ };
69
+ }
70
+ const snippet = renderSnippet(entry, { url: active.url, apiKey: active.apiKey });
71
+ const configPath = displayPath(entry.configPath(platform), platform);
72
+ const out = [];
73
+ out.push('');
74
+ out.push(chalk.bold.cyan(`${entry.label} (${entry.id})`));
75
+ out.push(chalk.gray(`Config file: ${configPath}`));
76
+ if (entry.note)
77
+ out.push(chalk.gray(`Note: ${entry.note}`));
78
+ out.push('');
79
+ out.push(chalk.yellow('⚠ This block contains your live API key — paste into your vendor config and do not commit.'));
80
+ out.push('');
81
+ out.push(snippet);
82
+ out.push('');
83
+ out.push(chalk.gray(`Restart: ${entry.restart}`));
84
+ out.push(chalk.gray('Web reference: brainrouter-docs/mcp-install.md'));
85
+ out.push('');
86
+ return { ok: true, output: out.join('\n') };
87
+ }
@@ -5,6 +5,7 @@
5
5
  import chalk from 'chalk';
6
6
  import { childSessionKey } from '../../runtime/mcpUtils.js';
7
7
  import { listRoles } from '../../orchestration/roles.js';
8
+ import { listAll as listAgentDefs } from '../../orchestration/agentRegistry.js';
8
9
  import { formatSessionSummary, getSession, listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
9
10
  import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
10
11
  import { readTranscriptEntries } from '../../state/sessionStore.js';
@@ -26,6 +27,56 @@ export async function tryHandleOrchestrationCommand(ctx) {
26
27
  }
27
28
  case '/agents':
28
29
  {
30
+ if (args[0] === 'defs') {
31
+ const defs = listAgentDefs(agent.workspaceRoot);
32
+ console.log(chalk.bold('\nAgent Definitions:'));
33
+ const ID_W = Math.max(...defs.map((l) => l.def.id.length), 4) + 2;
34
+ const TIER_W = 12;
35
+ const SRC_W = 10;
36
+ console.log(chalk.gray(` ${'ID'.padEnd(ID_W)}${'TIER'.padEnd(TIER_W)}${'SOURCE'.padEnd(SRC_W)}PATH`));
37
+ for (const loaded of defs) {
38
+ const idStr = chalk.cyan(loaded.def.id.padEnd(ID_W));
39
+ const tierColor = loaded.def.tier === 'reasoning' ? chalk.blue : chalk.yellow;
40
+ const tierStr = tierColor(loaded.def.tier.padEnd(TIER_W));
41
+ const srcStr = chalk.gray(loaded.source.padEnd(SRC_W));
42
+ console.log(` ${idStr}${tierStr}${srcStr}${chalk.gray(loaded.filePath)}`);
43
+ }
44
+ console.log();
45
+ return true;
46
+ }
47
+ // `--watch`: poll the same data shape every second and re-render the
48
+ // running-children list inline. Same shape as `/agents` and the Ink
49
+ // status row so the user gets a single mental model (roadmap §3).
50
+ if (args.includes('--watch')) {
51
+ const intervalMs = 1000;
52
+ const maxTicks = 600; // ~10 min safety cap; Ctrl-C exits early.
53
+ let ticks = 0;
54
+ console.log(chalk.bold('\nWatching child agents (Ctrl-C to stop)…'));
55
+ await new Promise((resolve) => {
56
+ const handle = setInterval(() => {
57
+ reconcileStale(agent.workspaceRoot);
58
+ const running = listSessions(agent.workspaceRoot)
59
+ .filter((s) => s.status === 'pending' || s.status === 'running');
60
+ const stamp = new Date().toISOString().slice(11, 19);
61
+ if (running.length === 0) {
62
+ process.stdout.write(`\r[${stamp}] no running children${' '.repeat(40)}`);
63
+ }
64
+ else {
65
+ const parts = running.map((s) => `${s.id.slice(0, 14)} (${s.role})`).join(', ');
66
+ process.stdout.write(`\r[${stamp}] running: ${parts}${' '.repeat(10)}`);
67
+ }
68
+ if (++ticks >= maxTicks) {
69
+ clearInterval(handle);
70
+ process.stdout.write('\n');
71
+ resolve();
72
+ }
73
+ }, intervalMs);
74
+ const onSig = () => { clearInterval(handle); process.stdout.write('\n'); process.off('SIGINT', onSig); resolve(); };
75
+ process.once('SIGINT', onSig);
76
+ });
77
+ console.log();
78
+ return true;
79
+ }
29
80
  reconcileStale(agent.workspaceRoot);
30
81
  const sessions = listSessions(agent.workspaceRoot);
31
82
  // `--json` for scripting. Emits a single JSON line on stdout so
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `/release-notes` slash command — show the changelog for the running CLI version.
3
+ *
4
+ * /release-notes → current version's notes
5
+ * /release-notes <version> → specific version
6
+ * /release-notes list → every shipped version, sorted descending
7
+ *
8
+ * Changelog files ship inside the published package at `changelog/<version>.md`.
9
+ * The repo-root `brainrouter-changelog/` is copied into `brainrouter-cli/changelog/`
10
+ * by `prepublishOnly` so users who install via npm see them.
11
+ */
12
+ import type { CommandContext } from './_context.js';
13
+ export interface ReleaseNotesDeps {
14
+ /** Override the changelog directory (tests). Defaults to bundled `changelog/`. */
15
+ changelogDir?: string;
16
+ /** Override the current version (tests). Defaults to package.json#version. */
17
+ currentVersion?: string;
18
+ }
19
+ export declare function tryHandleReleaseNotesCommand(ctx: CommandContext, deps?: ReleaseNotesDeps): Promise<boolean>;
20
+ /**
21
+ * Pure handler — returns the rendered string. Split from `tryHandle*` so unit
22
+ * tests can assert on the output without capturing stdout.
23
+ */
24
+ export declare function runReleaseNotes(args: string[], deps?: ReleaseNotesDeps): string;