@kinqs/brainrouter-cli 0.3.5 → 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/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- 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 +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- 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 +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -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 +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -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/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- 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.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
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
|
|
13
|
+
*
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
import chalk from 'chalk';
|
|
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
|
+
export async function tryHandleMcpCommand(ctx) {
|
|
26
|
+
const { command, args, mcpClient, config } = ctx;
|
|
27
|
+
if (command !== '/mcp')
|
|
28
|
+
return false;
|
|
29
|
+
const sub = (args[0] ?? 'list').toLowerCase();
|
|
30
|
+
const targetName = args[1]?.trim();
|
|
31
|
+
if (sub === 'tools') {
|
|
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();
|
|
41
|
+
try {
|
|
42
|
+
const res = await mcpClient.listTools();
|
|
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);
|
|
54
|
+
}
|
|
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
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (sub === 'list') {
|
|
79
|
+
const profiles = Object.keys(config.servers ?? {});
|
|
80
|
+
if (profiles.length === 0) {
|
|
81
|
+
console.log(chalk.yellow('\nNo MCP profiles configured. Run `/login` or `brainrouter config` to set one up.\n'));
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
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) => {
|
|
89
|
+
const profile = config.servers[name];
|
|
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}`);
|
|
112
|
+
}
|
|
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'));
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (sub === 'reconnect') {
|
|
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();
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
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}"…`));
|
|
150
|
+
try {
|
|
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
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(chalk.red(`✗ "${targetName}" remained ${s?.status ?? 'offline'} — ${s?.error ?? 'unknown'}\n`));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
console.log(chalk.red(`✗ Reconnect failed: ${err?.message ?? err}\n`));
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
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);
|
|
273
|
+
return true;
|
|
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
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
|
-
import
|
|
14
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
15
15
|
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
16
16
|
import { extractMemories, renderMemoryCards } from '../../memory/formatters.js';
|
|
17
17
|
import { consolidateMemories } from '../../memory/consolidation.js';
|
|
@@ -237,7 +237,7 @@ export async function tryHandleMemoryCommand(ctx) {
|
|
|
237
237
|
return true;
|
|
238
238
|
}
|
|
239
239
|
if (sub === 'consolidate') {
|
|
240
|
-
const spinner =
|
|
240
|
+
const spinner = makeSpinner(chalk.gray('Consolidating memories from MCP into filesystem artifacts...')).start();
|
|
241
241
|
try {
|
|
242
242
|
const result = await consolidateMemories(mcpClient, agent.workspaceRoot, { sessionKey: agent.sessionKey });
|
|
243
243
|
spinner.succeed(chalk.green(`Consolidated ${result.totalRecords} records.`));
|
package/dist/cli/commands/obs.js
CHANGED
|
@@ -98,21 +98,23 @@ export async function tryHandleObsCommand(ctx) {
|
|
|
98
98
|
{
|
|
99
99
|
const session = agent.sessionUsage;
|
|
100
100
|
const metrics = agent.memoryMetrics;
|
|
101
|
-
|
|
101
|
+
// Scope to the live parent: sessions.json is workspace-wide and
|
|
102
|
+
// persists across CLI restarts, so an unfiltered list mixes in every
|
|
103
|
+
// child spawned by every prior CLI process. Filtering by
|
|
104
|
+
// parentSessionKey limits the row to children spawned by THIS parent.
|
|
105
|
+
const children = listSessions(agent.workspaceRoot).filter((s) => s.usage && s.parentSessionKey === agent.sessionKey);
|
|
102
106
|
const childPrompt = children.reduce((acc, c) => acc + (c.usage?.promptTokens ?? 0), 0);
|
|
103
107
|
const childCompletion = children.reduce((acc, c) => acc + (c.usage?.completionTokens ?? 0), 0);
|
|
104
108
|
const childCalls = children.reduce((acc, c) => acc + (c.usage?.calls ?? 0), 0);
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const offloadSavings = Math.round(metrics.offloadCharsAvoided / 4);
|
|
115
|
-
const totalSaved = recallSavings + offloadSavings;
|
|
109
|
+
// What we can actually measure:
|
|
110
|
+
// - offload: bytes of child output that did NOT land in the parent's
|
|
111
|
+
// context. These are real tokens not spent on the parent. We
|
|
112
|
+
// subtract the preview that DID land (OFFLOAD_PREVIEW_CHARS is
|
|
113
|
+
// already netted out in tools.ts before recordOffload fires).
|
|
114
|
+
// - briefing tokens: cost, not savings. They're already counted in
|
|
115
|
+
// session.promptTokens. We report them so the user can see how
|
|
116
|
+
// much of the prompt budget memory is consuming.
|
|
117
|
+
const offloadSavedTokens = Math.round(metrics.offloadCharsAvoided / 4);
|
|
116
118
|
const totalSpent = session.promptTokens + session.completionTokens + childPrompt + childCompletion;
|
|
117
119
|
console.log(chalk.bold('\nToken usage — this session'));
|
|
118
120
|
console.log(` Parent: ${chalk.cyan(session.promptTokens.toLocaleString())}↑ ${chalk.cyan(session.completionTokens.toLocaleString())}↓ ${chalk.gray(`(${session.turns} turn${session.turns === 1 ? '' : 's'}, ${session.calls} LLM call${session.calls === 1 ? '' : 's'})`)}`);
|
|
@@ -126,17 +128,15 @@ export async function tryHandleObsCommand(ctx) {
|
|
|
126
128
|
console.log(chalk.gray(` …and ${children.length - 5} more (see /agents)`));
|
|
127
129
|
}
|
|
128
130
|
console.log(` Total this session: ${chalk.bold.cyan(totalSpent.toLocaleString())} tokens`);
|
|
129
|
-
console.log(chalk.bold('\nMemory
|
|
130
|
-
console.log(` Briefing tokens injected:
|
|
131
|
-
console.log(`
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const ratio = totalSaved / totalSpent;
|
|
137
|
-
console.log(chalk.gray(` Ratio: for every 1 token spent, memory saved ~${ratio.toFixed(2)} tokens of context.`));
|
|
131
|
+
console.log(chalk.bold('\nMemory'));
|
|
132
|
+
console.log(` Briefing tokens injected: ${chalk.gray(metrics.briefingTokensInjected.toLocaleString())} ${chalk.gray(`(${metrics.recallRecordsConsulted} records consulted — already included in parent ↑)`)}`);
|
|
133
|
+
console.log(` Child output offloaded: ${chalk.gray(metrics.offloadCharsAvoided.toLocaleString())} chars ${chalk.gray(`(≈${offloadSavedTokens.toLocaleString()} parent tokens not spent)`)}`);
|
|
134
|
+
if (offloadSavedTokens > 0 && totalSpent > 0) {
|
|
135
|
+
const ratio = offloadSavedTokens / totalSpent;
|
|
136
|
+
const display = ratio >= 0.01 ? ratio.toFixed(2) : '<0.01';
|
|
137
|
+
console.log(chalk.gray(` Offload ratio: ~${display} saved per token spent.`));
|
|
138
138
|
}
|
|
139
|
-
console.log(chalk.gray('\n (
|
|
139
|
+
console.log(chalk.gray('\n (Offload is measured; briefing tokens are an information-gain stat, not a savings number.)\n'));
|
|
140
140
|
return true;
|
|
141
141
|
}
|
|
142
142
|
case '/feedback':
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import
|
|
7
|
+
import { spinner as makeSpinner } from '../spinner.js';
|
|
8
8
|
import { marked } from 'marked';
|
|
9
9
|
import { listTranscripts, loadTranscript } from '../../state/sessionStore.js';
|
|
10
10
|
import { readGoal, resumeGoal } from '../../state/goalStore.js';
|
|
@@ -51,6 +51,11 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
agent.sessionKey = sessionKey;
|
|
54
|
+
// The persisted transcript doesn't record per-call token usage, so
|
|
55
|
+
// we can't reconstruct counters for the resumed session — start
|
|
56
|
+
// counting from this point forward instead of carrying over the
|
|
57
|
+
// pre-resume parent counts (which were for a different session).
|
|
58
|
+
agent.resetSessionCounters();
|
|
54
59
|
const loaded = agent.loadHistory(entries);
|
|
55
60
|
console.log(chalk.green(`\n✓ Resumed session ${chalk.cyan(sessionKey)} with ${loaded} prior messages.`));
|
|
56
61
|
// If the resumed session has a goal that was suspended (paused,
|
|
@@ -73,9 +78,12 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
73
78
|
// before/after. Just unpause and kick off the next iteration.
|
|
74
79
|
const reactivated = resumeGoal(agent.workspaceRoot, sessionKey);
|
|
75
80
|
if (reactivated) {
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
81
|
+
// 9d: pre-9d this branch had to drop a `goal-budget-steering`
|
|
82
|
+
// tagged system message left over from a budget-trigger pause.
|
|
83
|
+
// That message no longer exists — the wrap-up directive is
|
|
84
|
+
// folded into the goal-anchor and the anchor is re-rendered
|
|
85
|
+
// by the next runTurn. `refreshSystemPrompt` is still useful
|
|
86
|
+
// here to rebuild any overlays that depend on the active goal.
|
|
79
87
|
agent.refreshSystemPrompt();
|
|
80
88
|
console.log(chalk.green(`\n▶ Goal resumed (${reactivated.budget.iterationsUsed}/${reactivated.budget.maxIterations} used). Starting next iteration…\n`));
|
|
81
89
|
ctx.repl.runAgentTurn(buildGoalKickoffPrompt(reactivated, 'resume'));
|
|
@@ -123,7 +131,7 @@ export async function tryHandleSessionCommand(ctx) {
|
|
|
123
131
|
}
|
|
124
132
|
case '/compact':
|
|
125
133
|
{
|
|
126
|
-
const spinner =
|
|
134
|
+
const spinner = makeSpinner(chalk.gray('Summarizing conversation for compaction...')).start();
|
|
127
135
|
try {
|
|
128
136
|
const result = await agent.compactHistory();
|
|
129
137
|
if (!result) {
|