@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.
- package/README.md +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/changelog/0.2.0.md +15 -0
- package/changelog/0.3.0.md +20 -0
- package/changelog/0.3.1.md +22 -0
- package/changelog/0.3.2.md +15 -0
- package/changelog/0.3.3.md +19 -0
- package/changelog/0.3.4.md +20 -0
- package/changelog/0.3.5.md +9 -0
- package/changelog/0.3.6.md +9 -0
- package/changelog/0.3.7.md +20 -0
- package/changelog/0.3.8.md +30 -0
- package/changelog/README.md +41 -0
- package/dist/agent/agent.d.ts +34 -1
- package/dist/agent/agent.js +372 -79
- package/dist/agent/toolCallRecovery.d.ts +57 -0
- package/dist/agent/toolCallRecovery.js +130 -0
- package/dist/agent/toolSafety.d.ts +17 -0
- package/dist/agent/toolSafety.js +102 -0
- package/dist/cli/banner.d.ts +20 -0
- package/dist/cli/banner.js +47 -14
- package/dist/cli/cliPrompt.d.ts +40 -3
- package/dist/cli/cliPrompt.js +117 -25
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +13 -11
- package/dist/cli/commands/mcp.js +261 -74
- package/dist/cli/commands/mcpInstall.d.ts +20 -0
- package/dist/cli/commands/mcpInstall.js +87 -0
- package/dist/cli/commands/orchestration.js +51 -0
- package/dist/cli/commands/releaseNotes.d.ts +24 -0
- package/dist/cli/commands/releaseNotes.js +109 -0
- package/dist/cli/commands/schedule.d.ts +18 -0
- package/dist/cli/commands/schedule.js +189 -0
- package/dist/cli/commands/ui.js +119 -60
- package/dist/cli/commands/workflow.d.ts +2 -0
- package/dist/cli/commands/workflow.js +54 -8
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +71 -0
- package/dist/cli/ink/Picker.js +168 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +682 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +75 -0
- package/dist/cli/ink/toolFormat.js +206 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +52 -714
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +13 -1
- package/dist/config/config.js +45 -3
- package/dist/index.js +157 -206
- package/dist/memory/briefing.d.ts +1 -1
- package/dist/memory/briefing.js +4 -4
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +105 -3
- package/dist/orchestration/tools.js +167 -8
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.js +7 -2
- package/dist/runtime/anthropicAdapter.d.ts +100 -0
- package/dist/runtime/anthropicAdapter.js +293 -0
- package/dist/runtime/cronParser.d.ts +23 -0
- package/dist/runtime/cronParser.js +122 -0
- package/dist/runtime/mcpClient.js +14 -11
- package/dist/runtime/mcpPool.d.ts +170 -0
- package/dist/runtime/mcpPool.js +442 -0
- package/dist/runtime/mcpUtils.d.ts +17 -1
- package/dist/runtime/mcpUtils.js +23 -0
- package/dist/runtime/scheduleTicker.d.ts +33 -0
- package/dist/runtime/scheduleTicker.js +99 -0
- package/dist/runtime/vendorSnippets.d.ts +45 -0
- package/dist/runtime/vendorSnippets.js +153 -0
- package/dist/state/scheduleStore.d.ts +37 -0
- package/dist/state/scheduleStore.js +64 -0
- package/package.json +14 -5
- package/.env.example +0 -116
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -1,53 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
7
|
-
* /mcp list
|
|
8
|
-
* /mcp
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
40
|
-
spinner.succeed(chalk.green(`${tools.length}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
48
|
-
console.log(`\n
|
|
49
|
-
for (const
|
|
50
|
-
|
|
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
|
|
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('\
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 "${
|
|
166
|
+
console.log(chalk.gray(`Reconnecting "${targetName}"…`));
|
|
100
167
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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;
|