@kinqs/brainrouter-cli 0.3.6 → 0.3.7
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/dist/agent/agent.d.ts +12 -1
- package/dist/agent/agent.js +134 -18
- 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 +52 -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 +239 -74
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/ui.js +117 -58
- 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 +65 -0
- package/dist/cli/ink/Picker.js +133 -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 +571 -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 +73 -0
- package/dist/cli/ink/toolFormat.js +180 -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 +43 -712
- 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 +12 -0
- package/dist/config/config.js +45 -3
- package/dist/index.js +148 -206
- package/dist/memory/briefing.d.ts +1 -1
- 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 +10 -1
- package/dist/orchestration/tools.js +48 -4
- 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 +5 -1
- package/dist/runtime/mcpClient.js +14 -11
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/package.json +8 -2
- package/.env.example +0 -116
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -1,53 +1,71 @@
|
|
|
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';
|
|
18
25
|
export async function tryHandleMcpCommand(ctx) {
|
|
19
26
|
const { command, args, mcpClient, config } = ctx;
|
|
20
27
|
if (command !== '/mcp')
|
|
21
28
|
return false;
|
|
22
29
|
const sub = (args[0] ?? 'list').toLowerCase();
|
|
30
|
+
const targetName = args[1]?.trim();
|
|
23
31
|
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();
|
|
32
|
+
const onlyServer = targetName;
|
|
33
|
+
console.log(chalk.bold('\nMCP tools (pooled)'));
|
|
34
|
+
const statuses = mcpClient.getStatuses();
|
|
35
|
+
const connected = statuses.filter((s) => s.status === 'connected');
|
|
36
|
+
if (connected.length === 0) {
|
|
37
|
+
console.log(chalk.yellow(' No MCP servers connected. Try /mcp reconnect.\n'));
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const spinner = makeSpinner(chalk.gray('Fetching pooled tool surface...')).start();
|
|
37
41
|
try {
|
|
38
42
|
const res = await mcpClient.listTools();
|
|
39
|
-
const
|
|
40
|
-
spinner.succeed(chalk.green(`${tools.length}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
43
|
+
const allTools = res.tools || [];
|
|
44
|
+
spinner.succeed(chalk.green(`${allTools.length} tools across ${connected.length} server${connected.length === 1 ? '' : 's'}`));
|
|
45
|
+
// Pool tools are exposed as `mcp__<serverId>__<rawTool>`. Group by serverId.
|
|
46
|
+
const byServer = {};
|
|
47
|
+
for (const t of allTools) {
|
|
48
|
+
const m = /^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/.exec(t.name);
|
|
49
|
+
const serverId = m?.[1] ?? '__unknown__';
|
|
50
|
+
const raw = m?.[2] ?? t.name;
|
|
51
|
+
if (onlyServer && serverId !== onlyServer)
|
|
52
|
+
continue;
|
|
53
|
+
(byServer[serverId] ||= []).push(raw);
|
|
46
54
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const serverIds = Object.keys(byServer).sort();
|
|
56
|
+
if (serverIds.length === 0) {
|
|
57
|
+
console.log(chalk.gray(`\n No tools for server "${onlyServer}".\n`));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
for (const section of groupServerIdsByEcosystem(serverIds, (id) => mcpClient.getStatus(id)?.identity ?? 'unknown')) {
|
|
61
|
+
console.log(`\n${section.title}`);
|
|
62
|
+
for (const id of section.ids) {
|
|
63
|
+
const ident = mcpClient.getStatus(id)?.identity ?? 'unknown';
|
|
64
|
+
const identTag = formatIdentityTag(ident);
|
|
65
|
+
console.log(` ${chalk.bold.green(id)} ${identTag} (${byServer[id].length})`);
|
|
66
|
+
for (const name of byServer[id].sort()) {
|
|
67
|
+
console.log(` ${chalk.gray('•')} ${name} ${chalk.gray(`mcp__${id}__${name}`)}`);
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
}
|
|
@@ -58,64 +76,211 @@ export async function tryHandleMcpCommand(ctx) {
|
|
|
58
76
|
return true;
|
|
59
77
|
}
|
|
60
78
|
if (sub === 'list') {
|
|
61
|
-
const activeName = config.activeServer;
|
|
62
79
|
const profiles = Object.keys(config.servers ?? {});
|
|
63
80
|
if (profiles.length === 0) {
|
|
64
|
-
console.log(chalk.yellow('\nNo MCP profiles configured. Run
|
|
81
|
+
console.log(chalk.yellow('\nNo MCP profiles configured. Run `/login` or `brainrouter config` to set one up.\n'));
|
|
65
82
|
return true;
|
|
66
83
|
}
|
|
67
|
-
console.log(chalk.bold('\
|
|
68
|
-
|
|
84
|
+
console.log(chalk.bold('\nMCP servers'));
|
|
85
|
+
const statuses = mcpClient.getStatuses();
|
|
86
|
+
const statusById = new Map(statuses.map((s) => [s.serverId, s]));
|
|
87
|
+
const activeName = config.activeServer;
|
|
88
|
+
for (const section of groupServerIdsByEcosystem(profiles, (name) => {
|
|
69
89
|
const profile = config.servers[name];
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
return statusById.get(name)?.identity ?? profile?.identity ?? 'unknown';
|
|
91
|
+
})) {
|
|
92
|
+
console.log(`\n${section.title}`);
|
|
93
|
+
for (const name of section.ids) {
|
|
94
|
+
const profile = config.servers[name];
|
|
95
|
+
const poolStatus = statusById.get(name);
|
|
96
|
+
// Identity: pool live > config metadata > 'unknown'.
|
|
97
|
+
const identity = poolStatus?.identity ?? profile.identity ?? 'unknown';
|
|
98
|
+
// Status: pool live > 'not in pool' (configured but never tried).
|
|
99
|
+
const liveStatus = poolStatus?.status;
|
|
100
|
+
const statusLabel = liveStatus === 'connected' ? chalk.green('online') :
|
|
101
|
+
liveStatus === 'failed' ? chalk.red('failed') :
|
|
102
|
+
liveStatus === 'connecting' ? chalk.yellow('connecting') :
|
|
103
|
+
liveStatus === 'offline' ? chalk.gray('disconnected') :
|
|
104
|
+
chalk.gray('idle');
|
|
105
|
+
const idLabel = formatIdentityTag(identity);
|
|
106
|
+
const marker = name === activeName ? chalk.bold('★ ') : ' ';
|
|
107
|
+
const transport = profile.type;
|
|
108
|
+
const target = profile.type === 'http' ? profile.url ?? '<no url>' : profile.command ?? '<no command>';
|
|
109
|
+
const toolTag = poolStatus?.toolCount != null ? chalk.gray(`${poolStatus.toolCount} tools`) : '';
|
|
110
|
+
const errTag = liveStatus === 'failed' && poolStatus?.error ? chalk.red(` · ${poolStatus.error}`) : '';
|
|
111
|
+
console.log(`${marker}${chalk.bold(name)} ${idLabel} ${transport} ${statusLabel} ${chalk.gray(target)} ${toolTag}${errTag}`);
|
|
75
112
|
}
|
|
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'));
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk.gray('\n★ = highlighted profile in the banner.'));
|
|
115
|
+
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
116
|
return true;
|
|
91
117
|
}
|
|
92
118
|
if (sub === 'reconnect') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
if (!targetName) {
|
|
120
|
+
// No name → reconnect every configured server in the pool.
|
|
121
|
+
const ids = selectMcpServerIds(config.servers ?? {}, config.activeServer);
|
|
122
|
+
if (ids.length === 0) {
|
|
123
|
+
console.log(chalk.red(`\nNo MCP profiles configured.\n`));
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
console.log(chalk.gray(`Reconnecting all servers (${ids.length})…`));
|
|
127
|
+
await Promise.allSettled(ids.map(async (id) => {
|
|
128
|
+
try {
|
|
129
|
+
await mcpClient.reconnectOne(id);
|
|
130
|
+
const s = mcpClient.getStatus(id);
|
|
131
|
+
if (s?.status === 'connected') {
|
|
132
|
+
console.log(chalk.green(` ✓ ${id}`));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(chalk.red(` ✗ ${id} — ${s?.error ?? 'failed'}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.log(chalk.red(` ✗ ${id} — ${err?.message ?? err}`));
|
|
140
|
+
}
|
|
141
|
+
}));
|
|
142
|
+
console.log();
|
|
97
143
|
return true;
|
|
98
144
|
}
|
|
99
|
-
|
|
145
|
+
if (!config.servers?.[targetName]) {
|
|
146
|
+
console.log(chalk.red(`\nNo profile named "${targetName}".\n`));
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
console.log(chalk.gray(`Reconnecting "${targetName}"…`));
|
|
100
150
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
await disconnectOtherBrainrouterServers(ctx, targetName);
|
|
152
|
+
await mcpClient.reconnectOne(targetName);
|
|
153
|
+
const s = mcpClient.getStatus(targetName);
|
|
154
|
+
if (s?.status === 'connected') {
|
|
155
|
+
const activated = activateBrainrouterProfile(ctx, targetName);
|
|
156
|
+
console.log(chalk.green(`✓ Reconnected to "${targetName}".\n`));
|
|
157
|
+
if (activated) {
|
|
158
|
+
console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
|
|
159
|
+
printRefreshedBanner(ctx);
|
|
160
|
+
}
|
|
103
161
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Re-probe tools so identity tagging and the prompt's tool list refresh.
|
|
107
|
-
try {
|
|
108
|
-
await mcpClient.listTools();
|
|
162
|
+
else {
|
|
163
|
+
console.log(chalk.red(`✗ "${targetName}" remained ${s?.status ?? 'offline'} — ${s?.error ?? 'unknown'}\n`));
|
|
109
164
|
}
|
|
110
|
-
catch { /* tool-list failure is non-fatal */ }
|
|
111
|
-
console.log(chalk.green(`✓ Reconnected to "${activeName}" (${profile.type}).\n`));
|
|
112
165
|
}
|
|
113
166
|
catch (err) {
|
|
114
167
|
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
168
|
}
|
|
117
169
|
return true;
|
|
118
170
|
}
|
|
119
|
-
|
|
171
|
+
if (sub === 'connect') {
|
|
172
|
+
if (!targetName) {
|
|
173
|
+
console.log(chalk.red('\nUsage: /mcp connect <name>\n'));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const profile = config.servers?.[targetName];
|
|
177
|
+
if (!profile) {
|
|
178
|
+
console.log(chalk.red(`\nNo profile named "${targetName}". Available: ${Object.keys(config.servers ?? {}).join(', ') || '(none)'}.\n`));
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
console.log(chalk.gray(`Connecting "${targetName}"…`));
|
|
182
|
+
try {
|
|
183
|
+
await disconnectOtherBrainrouterServers(ctx, targetName);
|
|
184
|
+
await mcpClient.connectOne(targetName, profile, config.llm, 5_000);
|
|
185
|
+
const s = mcpClient.getStatus(targetName);
|
|
186
|
+
if (s?.status === 'connected') {
|
|
187
|
+
const activated = activateBrainrouterProfile(ctx, targetName);
|
|
188
|
+
console.log(chalk.green(`✓ "${targetName}" online (${s.toolCount ?? 0} tools).\n`));
|
|
189
|
+
if (activated) {
|
|
190
|
+
console.log(chalk.gray(` Active BrainRouter profile saved as "${targetName}" for this and future sessions.\n`));
|
|
191
|
+
printRefreshedBanner(ctx);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
console.log(chalk.red(`✗ "${targetName}" failed — ${s?.error ?? 'unknown'}\n`));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.log(chalk.red(`✗ Connect failed: ${err?.message ?? err}\n`));
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (sub === 'disconnect') {
|
|
204
|
+
if (!targetName) {
|
|
205
|
+
console.log(chalk.red('\nUsage: /mcp disconnect <name>\n'));
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (!mcpClient.getStatus(targetName)) {
|
|
209
|
+
console.log(chalk.yellow(`\n"${targetName}" is not in the pool.\n`));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await mcpClient.disconnectOne(targetName);
|
|
214
|
+
console.log(chalk.green(`✓ "${targetName}" disconnected. Config preserved — /mcp connect ${targetName} to bring it back.\n`));
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.log(chalk.red(`✗ Disconnect failed: ${err?.message ?? err}\n`));
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
console.log(chalk.red(`\nUnknown /mcp subcommand "${sub}". Usage: /mcp list | /mcp tools [server] | /mcp connect <name> | /mcp disconnect <name> | /mcp reconnect [name]\n`));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
function formatIdentityTag(identity) {
|
|
225
|
+
return identity === 'brainrouter' ? chalk.cyan('brainrouter') :
|
|
226
|
+
identity === 'third-party' ? chalk.yellow('third-party') :
|
|
227
|
+
chalk.gray('unknown');
|
|
228
|
+
}
|
|
229
|
+
function groupServerIdsByEcosystem(ids, identityFor) {
|
|
230
|
+
const brainrouter = [];
|
|
231
|
+
const other = [];
|
|
232
|
+
for (const id of ids.slice().sort()) {
|
|
233
|
+
if (identityFor(id) === 'brainrouter')
|
|
234
|
+
brainrouter.push(id);
|
|
235
|
+
else
|
|
236
|
+
other.push(id);
|
|
237
|
+
}
|
|
238
|
+
const sections = [];
|
|
239
|
+
if (brainrouter.length > 0) {
|
|
240
|
+
sections.push({ title: chalk.bold.cyan('BrainRouter MCP (Our Ecosystem)'), ids: brainrouter });
|
|
241
|
+
}
|
|
242
|
+
if (other.length > 0) {
|
|
243
|
+
sections.push({ title: chalk.bold.yellow('Third-party MCPs (Other)'), ids: other });
|
|
244
|
+
}
|
|
245
|
+
return sections;
|
|
246
|
+
}
|
|
247
|
+
async function disconnectOtherBrainrouterServers(ctx, targetName) {
|
|
248
|
+
const targetProfile = ctx.config.servers?.[targetName];
|
|
249
|
+
if (!targetProfile)
|
|
250
|
+
return;
|
|
251
|
+
if (resolveIdentityFromConfig(targetProfile, targetName) !== 'brainrouter')
|
|
252
|
+
return;
|
|
253
|
+
for (const [id, profile] of Object.entries(ctx.config.servers ?? {})) {
|
|
254
|
+
if (id === targetName)
|
|
255
|
+
continue;
|
|
256
|
+
if (resolveIdentityFromConfig(profile, id) !== 'brainrouter')
|
|
257
|
+
continue;
|
|
258
|
+
const status = ctx.mcpClient.getStatus(id);
|
|
259
|
+
if (status?.status === 'connected' || status?.status === 'connecting') {
|
|
260
|
+
await ctx.mcpClient.disconnectOne(id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function activateBrainrouterProfile(ctx, targetName) {
|
|
265
|
+
const status = ctx.mcpClient.getStatus(targetName);
|
|
266
|
+
const profile = ctx.config.servers?.[targetName];
|
|
267
|
+
const isBrainrouter = status?.identity === 'brainrouter' ||
|
|
268
|
+
(profile ? resolveIdentityFromConfig(profile, targetName) === 'brainrouter' : false);
|
|
269
|
+
if (!isBrainrouter)
|
|
270
|
+
return false;
|
|
271
|
+
ctx.config.activeServer = targetName;
|
|
272
|
+
saveConfig(ctx.config);
|
|
120
273
|
return true;
|
|
121
274
|
}
|
|
275
|
+
function printRefreshedBanner(ctx) {
|
|
276
|
+
const theme = resolveTheme(ctx.agent.workspaceRoot);
|
|
277
|
+
const banner = renderBanner(buildBannerInputs(ctx.config, ctx.agent, ctx.mcpClient), theme);
|
|
278
|
+
if (ctx.repl.replaceBanner) {
|
|
279
|
+
ctx.repl.replaceBanner('\n' + banner);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
console.log(chalk.gray('Updated active BrainRouter banner:'));
|
|
283
|
+
console.log(banner);
|
|
284
|
+
console.log();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -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,23 @@ 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
|
+
}
|
|
29
47
|
reconcileStale(agent.workspaceRoot);
|
|
30
48
|
const sessions = listSessions(agent.workspaceRoot);
|
|
31
49
|
// `--json` for scripting. Emits a single JSON line on stdout so
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -12,10 +12,17 @@ import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
|
12
12
|
import { listSessions, reconcileStale } from '../../orchestration/orchestrator.js';
|
|
13
13
|
import { readPreferences, resolveEffort, writePreferences } from '../../state/preferencesStore.js';
|
|
14
14
|
import { readPlan } from '../../state/taskStore.js';
|
|
15
|
-
|
|
15
|
+
// initAgentMd usage moved to commands/init.ts (0.3.7 wizard). The
|
|
16
|
+
// legacy /config + /init switch cases here are gone — the dispatcher
|
|
17
|
+
// in repl.ts routes them to the new handlers first. getConfigPath
|
|
18
|
+
// stays in scope because /doctor still surfaces the path.
|
|
19
|
+
import { getConfigPath, saveConfig } from '../../config/config.js';
|
|
16
20
|
import { copyToClipboard } from '../../runtime/clipboard.js';
|
|
17
|
-
import { initAgentMd } from '../../prompt/initAgentMd.js';
|
|
18
21
|
import { completeWorkspacePath, renderHelp } from '../repl.js';
|
|
22
|
+
import { PROVIDER_CATALOG, findProvider } from '../wizard/providers.js';
|
|
23
|
+
import { selectModel } from '../wizard/modelsApi.js';
|
|
24
|
+
import { buildTheme } from '../theme.js';
|
|
25
|
+
import { listFilesystemSkills } from '../../prompt/skillCatalog.js';
|
|
19
26
|
export async function tryHandleUiCommand(ctx) {
|
|
20
27
|
const { command, args, agent, mcpClient, config, rl, repl } = ctx;
|
|
21
28
|
// 'ctx' alias to keep references to the old ReplContext name working
|
|
@@ -78,27 +85,10 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
78
85
|
console.log();
|
|
79
86
|
return true;
|
|
80
87
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Print config without API keys
|
|
86
|
-
const scrubbedConfig = JSON.parse(JSON.stringify(config));
|
|
87
|
-
if (scrubbedConfig.llm?.apiKey) {
|
|
88
|
-
scrubbedConfig.llm.apiKey = 'br_••••••••••••••••';
|
|
89
|
-
}
|
|
90
|
-
for (const s of Object.values(scrubbedConfig.servers)) {
|
|
91
|
-
const srv = s;
|
|
92
|
-
if (srv.apiKey)
|
|
93
|
-
srv.apiKey = 'br_••••••••••••••••';
|
|
94
|
-
if (srv.env?.BRAINROUTER_API_KEY) {
|
|
95
|
-
srv.env.BRAINROUTER_API_KEY = 'br_••••••••••••••••';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
console.log(chalk.gray(JSON.stringify(scrubbedConfig, null, 2)));
|
|
99
|
-
console.log();
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
88
|
+
// /config now lives in commands/config.ts (0.3.7 settings home panel
|
|
89
|
+
// + verb-overloaded get/set). The dispatcher in repl.ts routes it
|
|
90
|
+
// before this case, so leaving anything here is dead — removed.
|
|
91
|
+
// Use `/config raw` if you want the old scrubbed-JSON dump.
|
|
102
92
|
case '/doctor':
|
|
103
93
|
{
|
|
104
94
|
console.log(chalk.bold('\nBrainRouter Doctor:'));
|
|
@@ -178,30 +168,69 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
178
168
|
console.log();
|
|
179
169
|
return true;
|
|
180
170
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (result.status === 'created') {
|
|
185
|
-
console.log(chalk.green(`\n✓ Created ${result.path}`));
|
|
186
|
-
console.log(chalk.gray('Edit it to describe your project, conventions, and boundaries — any AGENT.md-aware coding agent will read it.\n'));
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
console.log(chalk.yellow(`\nFile already exists: ${result.path}`));
|
|
190
|
-
console.log(chalk.gray('Open it and edit by hand if you want to refresh it.\n'));
|
|
191
|
-
}
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
171
|
+
// /init is now the onboarding-wizard entrypoint (commands/init.ts).
|
|
172
|
+
// The AGENT.md-only path lives behind `/init agentmd` for back-compat.
|
|
173
|
+
// Routed before this case in repl.ts; no fall-through handler needed.
|
|
194
174
|
case '/model':
|
|
195
175
|
{
|
|
196
176
|
const newModel = args[0];
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
177
|
+
const previous = agent.getModel();
|
|
178
|
+
// Direct-switch form `/model <name>` stays for scripts and muscle
|
|
179
|
+
// memory. No-arg opens the picker (0.3.7).
|
|
180
|
+
if (newModel) {
|
|
181
|
+
agent.setModel(newModel);
|
|
182
|
+
if (config.llm) {
|
|
183
|
+
config.llm.model = newModel;
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
}
|
|
186
|
+
console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(newModel)}\n`));
|
|
200
187
|
return true;
|
|
201
188
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
// No-arg → open the picker. Resolves provider by matching the
|
|
190
|
+
// saved endpoint against PROVIDER_CATALOG; falls back to the
|
|
191
|
+
// OpenAI entry when nothing matches (the agent loop also
|
|
192
|
+
// defaults to OpenAI-compatible shapes).
|
|
193
|
+
const themeMode = readPreferences(agent.workspaceRoot).theme;
|
|
194
|
+
const theme = buildTheme(themeMode === 'mono' ? 'mono' : themeMode === 'light' ? 'light' : 'dark');
|
|
195
|
+
const llm = config.llm;
|
|
196
|
+
const provider = (llm?.endpoint && PROVIDER_CATALOG.find((p) => p.endpoint.replace(/\/$/, '') === (llm.endpoint ?? '').replace(/\/$/, ''))) ||
|
|
197
|
+
findProvider('openai');
|
|
198
|
+
const result = await selectModel({
|
|
199
|
+
theme,
|
|
200
|
+
provider,
|
|
201
|
+
apiKey: llm?.apiKey ?? '',
|
|
202
|
+
endpointOverride: llm?.endpoint,
|
|
203
|
+
currentModel: previous,
|
|
204
|
+
title: '/model — quick-swap',
|
|
205
|
+
badge: provider.label,
|
|
206
|
+
});
|
|
207
|
+
if (!result) {
|
|
208
|
+
console.log(chalk.yellow('\n /model cancelled.\n'));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
if (result.model === previous) {
|
|
212
|
+
console.log(chalk.gray(`\n Model unchanged (${previous}).\n`));
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
// Cross-provider sanity check — if the picked model looks like
|
|
216
|
+
// a different vendor's namespace (anthropic/*, google/*, etc.)
|
|
217
|
+
// and the active provider isn't a multi-vendor gateway, warn so
|
|
218
|
+
// the user doesn't hit a confusing 404 on the next turn.
|
|
219
|
+
if (looksLikeForeignModel(result.model, provider)) {
|
|
220
|
+
console.log(chalk.yellow(`\n ⚠ "${result.model}" looks like a different provider's namespace. ` +
|
|
221
|
+
`Active endpoint: ${provider.label}.` +
|
|
222
|
+
`\n Run /config provider <id> to switch endpoints, or /model again to pick a native model.\n`));
|
|
223
|
+
}
|
|
224
|
+
agent.setModel(result.model);
|
|
225
|
+
if (config.llm) {
|
|
226
|
+
config.llm.model = result.model;
|
|
227
|
+
saveConfig(config);
|
|
228
|
+
}
|
|
229
|
+
const sourceTag = result.source === 'live' ? `live · ${result.liveCount} models` :
|
|
230
|
+
result.source === 'fallback' ? `offline · static catalog (${result.liveError ?? 'unknown'})` :
|
|
231
|
+
'static catalog';
|
|
232
|
+
console.log(chalk.green(`\n✓ Model switched: ${chalk.gray(previous)} → ${chalk.cyan(result.model)}`));
|
|
233
|
+
console.log(chalk.gray(` Source: ${sourceTag}\n`));
|
|
205
234
|
return true;
|
|
206
235
|
}
|
|
207
236
|
// /mcp moved to its own command file (commands/mcp.ts) as part of 0.3.6
|
|
@@ -410,12 +439,22 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
410
439
|
console.log(chalk.gray(' Drop a folder under skills/<category>/<name>/SKILL.md to register one.\n'));
|
|
411
440
|
return true;
|
|
412
441
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
console.log(chalk.cyan(`
|
|
442
|
+
const skills = listFilesystemSkills(agent.workspaceRoot);
|
|
443
|
+
if (skills.length > 0) {
|
|
444
|
+
console.log(chalk.gray(' Skills'));
|
|
445
|
+
for (const skill of skills) {
|
|
446
|
+
const category = skill.category ? `${skill.category}/` : '';
|
|
447
|
+
console.log(` • ${chalk.cyan(`${category}${skill.name}`)} (${chalk.gray(skill.scope ?? 'filesystem')})`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (fs.existsSync(pluginsRoot)) {
|
|
451
|
+
const entries = fs.readdirSync(pluginsRoot, { withFileTypes: true });
|
|
452
|
+
const pluginDirs = entries.filter((entry) => entry.isDirectory());
|
|
453
|
+
if (pluginDirs.length > 0) {
|
|
454
|
+
console.log(chalk.gray(' Plugin folders'));
|
|
455
|
+
for (const entry of pluginDirs) {
|
|
456
|
+
console.log(` • ${chalk.cyan(path.relative(agent.workspaceRoot, path.join(pluginsRoot, entry.name)))}`);
|
|
457
|
+
}
|
|
419
458
|
}
|
|
420
459
|
}
|
|
421
460
|
console.log();
|
|
@@ -496,23 +535,19 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
496
535
|
case '/where':
|
|
497
536
|
{
|
|
498
537
|
const { gatherWhereInputs, renderWhere } = await import('../whereView.js');
|
|
538
|
+
const { resolveDisplayedMcpState } = await import('../banner.js');
|
|
499
539
|
const { resolveTheme } = await import('../theme.js');
|
|
500
540
|
const theme = resolveTheme(agent.workspaceRoot);
|
|
501
|
-
const
|
|
502
|
-
const server = config.servers[profileName];
|
|
541
|
+
const displayedMcp = resolveDisplayedMcpState(config, mcpClient);
|
|
503
542
|
const briefing = agent.getLastBriefing();
|
|
504
543
|
const inputs = gatherWhereInputs({
|
|
505
544
|
workspaceRoot: agent.workspaceRoot,
|
|
506
545
|
sessionKey: agent.sessionKey,
|
|
507
546
|
model: agent.getModel(),
|
|
508
|
-
mcpProfile:
|
|
509
|
-
mcpTransport:
|
|
510
|
-
mcpOnline:
|
|
511
|
-
|
|
512
|
-
// config field when present, otherwise 'unknown'.
|
|
513
|
-
mcpIdentity: typeof mcpClient.getIdentity === 'function'
|
|
514
|
-
? mcpClient.getIdentity()
|
|
515
|
-
: (server?.identity ?? 'unknown'),
|
|
547
|
+
mcpProfile: displayedMcp.profile,
|
|
548
|
+
mcpTransport: displayedMcp.transport,
|
|
549
|
+
mcpOnline: displayedMcp.online,
|
|
550
|
+
mcpIdentity: displayedMcp.identity,
|
|
516
551
|
accessMode: agent.getAccessMode(),
|
|
517
552
|
recalledRecords: agent.getRecalledRecords(),
|
|
518
553
|
briefingSources: briefing.sources,
|
|
@@ -527,3 +562,27 @@ export async function tryHandleUiCommand(ctx) {
|
|
|
527
562
|
}
|
|
528
563
|
return false;
|
|
529
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Heuristic — does the picked model id look like it belongs to a
|
|
567
|
+
* different vendor than the active provider's endpoint? Catches the
|
|
568
|
+
* common foot-gun of picking `anthropic/claude-*` while pointed at
|
|
569
|
+
* OpenAI direct, where the request 404s at the endpoint and the user
|
|
570
|
+
* has no obvious "you needed to switch endpoints" signal.
|
|
571
|
+
*
|
|
572
|
+
* Returns false for gateway providers (OpenRouter, "anthropic-via-gateway")
|
|
573
|
+
* since multi-vendor namespaces are expected there.
|
|
574
|
+
*/
|
|
575
|
+
function looksLikeForeignModel(model, provider) {
|
|
576
|
+
// Gateways are vendor-agnostic by design.
|
|
577
|
+
if (provider.id === 'openrouter' || provider.id === 'anthropic-via-gateway')
|
|
578
|
+
return false;
|
|
579
|
+
const FOREIGN_PREFIXES = {
|
|
580
|
+
openai: ['anthropic/', 'google/', 'meta/', 'mistralai/', 'qwen/', 'deepseek/'],
|
|
581
|
+
deepseek: ['anthropic/', 'google/', 'openai/', 'meta/', 'mistralai/'],
|
|
582
|
+
gemini: ['anthropic/', 'openai/', 'meta/', 'mistralai/', 'deepseek/'],
|
|
583
|
+
lmstudio: [],
|
|
584
|
+
ollama: [],
|
|
585
|
+
};
|
|
586
|
+
const list = FOREIGN_PREFIXES[provider.id] ?? [];
|
|
587
|
+
return list.some((prefix) => model.startsWith(prefix));
|
|
588
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Hand-tune imports if the compiler complains.
|
|
4
4
|
*/
|
|
5
5
|
import type { CommandContext } from './_context.js';
|
|
6
|
+
import { type SkillListItem } from '../../prompt/skillCatalog.js';
|
|
6
7
|
/**
|
|
7
8
|
* Decide whether `/grill-me` should refuse to fire because the current
|
|
8
9
|
* workflow already has a written `spec.md`. The clarifying pass is meant to
|
|
@@ -22,3 +23,4 @@ export declare function shouldSkipGrillMe(workspaceRoot: string, force: boolean,
|
|
|
22
23
|
specPath?: string;
|
|
23
24
|
};
|
|
24
25
|
export declare function tryHandleWorkflowCommand(ctx: CommandContext): Promise<boolean>;
|
|
26
|
+
export declare function normalizeSkillsList(payload: any): SkillListItem[] | undefined;
|