@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.
Files changed (96) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/dist/agent/agent.d.ts +12 -1
  8. package/dist/agent/agent.js +134 -18
  9. package/dist/cli/banner.d.ts +20 -0
  10. package/dist/cli/banner.js +47 -14
  11. package/dist/cli/cliPrompt.d.ts +40 -3
  12. package/dist/cli/cliPrompt.js +52 -25
  13. package/dist/cli/commands/_context.d.ts +3 -1
  14. package/dist/cli/commands/_helpers.d.ts +1 -1
  15. package/dist/cli/commands/config.d.ts +46 -0
  16. package/dist/cli/commands/config.js +1042 -0
  17. package/dist/cli/commands/init.d.ts +20 -0
  18. package/dist/cli/commands/init.js +64 -0
  19. package/dist/cli/commands/login.d.ts +13 -0
  20. package/dist/cli/commands/login.js +179 -0
  21. package/dist/cli/commands/mcp.d.ts +13 -11
  22. package/dist/cli/commands/mcp.js +239 -74
  23. package/dist/cli/commands/orchestration.js +18 -0
  24. package/dist/cli/commands/ui.js +117 -58
  25. package/dist/cli/commands/workflow.d.ts +2 -0
  26. package/dist/cli/commands/workflow.js +54 -8
  27. package/dist/cli/ink/ChatApp.d.ts +206 -0
  28. package/dist/cli/ink/ChatApp.js +493 -0
  29. package/dist/cli/ink/Frame.d.ts +26 -0
  30. package/dist/cli/ink/Frame.js +5 -0
  31. package/dist/cli/ink/Picker.d.ts +65 -0
  32. package/dist/cli/ink/Picker.js +133 -0
  33. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  34. package/dist/cli/ink/SlashPalette.js +136 -0
  35. package/dist/cli/ink/TextField.d.ts +34 -0
  36. package/dist/cli/ink/TextField.js +47 -0
  37. package/dist/cli/ink/WizardApp.d.ts +7 -0
  38. package/dist/cli/ink/WizardApp.js +422 -0
  39. package/dist/cli/ink/ambientChat.d.ts +34 -0
  40. package/dist/cli/ink/ambientChat.js +7 -0
  41. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  42. package/dist/cli/ink/consoleCapture.js +33 -0
  43. package/dist/cli/ink/markdownRender.d.ts +41 -0
  44. package/dist/cli/ink/markdownRender.js +278 -0
  45. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  46. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  47. package/dist/cli/ink/runChat.d.ts +34 -0
  48. package/dist/cli/ink/runChat.js +571 -0
  49. package/dist/cli/ink/runPicker.d.ts +31 -0
  50. package/dist/cli/ink/runPicker.js +139 -0
  51. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  52. package/dist/cli/ink/runSlashPalette.js +33 -0
  53. package/dist/cli/ink/runWizard.d.ts +22 -0
  54. package/dist/cli/ink/runWizard.js +133 -0
  55. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  56. package/dist/cli/ink/stdinHandoff.js +78 -0
  57. package/dist/cli/ink/toolFormat.d.ts +73 -0
  58. package/dist/cli/ink/toolFormat.js +180 -0
  59. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  60. package/dist/cli/ink/useTerminalSize.js +26 -0
  61. package/dist/cli/repl.d.ts +25 -3
  62. package/dist/cli/repl.js +43 -712
  63. package/dist/cli/slashSuggest.d.ts +32 -0
  64. package/dist/cli/slashSuggest.js +146 -0
  65. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  66. package/dist/cli/wizard/modelsApi.js +166 -0
  67. package/dist/cli/wizard/picker.d.ts +202 -0
  68. package/dist/cli/wizard/picker.js +547 -0
  69. package/dist/cli/wizard/providers.d.ts +86 -0
  70. package/dist/cli/wizard/providers.js +190 -0
  71. package/dist/cli/wizard/runner.d.ts +13 -0
  72. package/dist/cli/wizard/runner.js +488 -0
  73. package/dist/cli/wizard/types.d.ts +122 -0
  74. package/dist/cli/wizard/types.js +109 -0
  75. package/dist/config/config.d.ts +12 -0
  76. package/dist/config/config.js +45 -3
  77. package/dist/index.js +148 -206
  78. package/dist/memory/briefing.d.ts +1 -1
  79. package/dist/memory/consolidation.d.ts +1 -1
  80. package/dist/orchestration/agentRegistry.d.ts +36 -0
  81. package/dist/orchestration/agentRegistry.js +64 -0
  82. package/dist/orchestration/orchestrator.d.ts +7 -0
  83. package/dist/orchestration/orchestrator.js +2 -0
  84. package/dist/orchestration/tools.d.ts +10 -1
  85. package/dist/orchestration/tools.js +48 -4
  86. package/dist/prompt/skillCatalog.d.ts +11 -0
  87. package/dist/prompt/skillCatalog.js +134 -0
  88. package/dist/prompt/skillRunner.d.ts +2 -2
  89. package/dist/prompt/skillRunner.js +2 -31
  90. package/dist/prompt/systemPrompt.js +5 -1
  91. package/dist/runtime/mcpClient.js +14 -11
  92. package/dist/runtime/mcpPool.d.ts +162 -0
  93. package/dist/runtime/mcpPool.js +423 -0
  94. package/dist/runtime/mcpUtils.d.ts +3 -1
  95. package/package.json +8 -2
  96. package/.env.example +0 -116
@@ -0,0 +1,423 @@
1
+ import { McpClientWrapper, resolveIdentityFromConfig } from './mcpClient.js';
2
+ /**
3
+ * Choose which configured MCP profiles should connect for a normal CLI run.
4
+ *
5
+ * BrainRouter profiles are special: users may store several BrainRouter MCPs
6
+ * (local, staging, remote, self-hosted), but only one should be active at a
7
+ * time because the BrainRouter MCP is the memory/brain plane. Third-party MCPs
8
+ * are additive tools, so they all connect concurrently.
9
+ *
10
+ * `requestedProfile` is the explicit escape hatch (`--profile <name>`): it
11
+ * scopes the run to exactly that profile, matching the existing single-server
12
+ * mode.
13
+ */
14
+ export function selectMcpServerIds(servers, activeServer, requestedProfile) {
15
+ const ids = Object.keys(servers);
16
+ if (requestedProfile)
17
+ return servers[requestedProfile] ? [requestedProfile] : [];
18
+ const brainrouterIds = ids.filter((id) => resolveIdentityFromConfig(servers[id], id) === 'brainrouter');
19
+ const activeBrainrouter = activeServer && brainrouterIds.includes(activeServer)
20
+ ? activeServer
21
+ : brainrouterIds[0];
22
+ return ids.filter((id) => {
23
+ const identity = resolveIdentityFromConfig(servers[id], id);
24
+ return identity !== 'brainrouter' || id === activeBrainrouter;
25
+ });
26
+ }
27
+ function isBrainrouterOwnedTool(name) {
28
+ return name.startsWith('memory_') ||
29
+ [
30
+ 'list_skills',
31
+ 'get_skill',
32
+ 'search_skills',
33
+ 'create_skill',
34
+ 'update_skill',
35
+ 'get_persona',
36
+ 'get_reference',
37
+ 'list_template_docs',
38
+ 'get_template_doc',
39
+ ].includes(name);
40
+ }
41
+ export class McpClientPool {
42
+ /** serverId → connected wrapper. */
43
+ clients = new Map();
44
+ /** serverId → status entry (kept even for failed/offline servers so /mcp can render them). */
45
+ statuses = new Map();
46
+ /**
47
+ * Unprefixed tool name → owning serverId. Sentinel `__COLLISION__`
48
+ * marks tool names exposed by multiple servers (must be addressed
49
+ * via the prefixed form).
50
+ */
51
+ toolToServer = new Map();
52
+ /** Prefixed form (`mcp_<serverId>_<tool>`) → `{serverId, tool}` for fast dispatch. */
53
+ prefixedToServer = new Map();
54
+ /** LLM config from the last connectAll — needed for reconnect calls. */
55
+ currentLlmConfig;
56
+ /** Raw server configs from the last connectAll — needed for /mcp reconnect <id>. */
57
+ serverConfigs = new Map();
58
+ /** serverId → prefix used in tool names. Brainrouter servers get "brainrouter"; others keep their config key. */
59
+ prefixIds = new Map();
60
+ /** Reverse: prefixId → serverId (needed to dispatch `mcp_brainrouter_X` back to the real key). */
61
+ prefixToServerId = new Map();
62
+ getPrefixId(serverId) {
63
+ return this.prefixIds.get(serverId) ?? serverId;
64
+ }
65
+ assignPrefixId(serverId, wrapper) {
66
+ const id = wrapper.getIdentity() === 'brainrouter' ? 'brainrouter' : serverId;
67
+ this.prefixIds.set(serverId, id);
68
+ this.prefixToServerId.set(id, serverId);
69
+ }
70
+ /**
71
+ * Connect to every entry in `servers` concurrently. Each connect
72
+ * gets its own timeout; offline servers don't block the others.
73
+ * Returns the status array after all connects settle.
74
+ */
75
+ async connectAll(servers, llmConfig, options) {
76
+ this.currentLlmConfig = llmConfig;
77
+ const entries = Object.entries(servers);
78
+ // Stash configs first so `/mcp reconnect <id>` can find them later.
79
+ for (const [serverId, cfg] of entries)
80
+ this.serverConfigs.set(serverId, cfg);
81
+ const tasks = entries.map(([serverId, cfg]) => this.connectOne(serverId, cfg, llmConfig, options?.timeoutMs).then(() => {
82
+ const s = this.statuses.get(serverId);
83
+ if (s && options?.onStatusChange)
84
+ options.onStatusChange(s);
85
+ }));
86
+ await Promise.allSettled(tasks);
87
+ await this.refreshToolIndex();
88
+ return this.getStatuses();
89
+ }
90
+ /**
91
+ * Connect a single server. Used both by `connectAll` and by
92
+ * `/mcp connect <id>` for late-joining servers. Idempotent — if
93
+ * the server is already connected, closes the previous wrapper first.
94
+ */
95
+ async connectOne(serverId, config, llmConfig, timeoutMs = 5_000) {
96
+ if (this.clients.has(serverId)) {
97
+ try {
98
+ await this.clients.get(serverId).close();
99
+ }
100
+ catch { /* ignore */ }
101
+ this.clients.delete(serverId);
102
+ }
103
+ this.serverConfigs.set(serverId, config);
104
+ this.statuses.set(serverId, { serverId, identity: 'unknown', status: 'connecting' });
105
+ const wrapper = new McpClientWrapper();
106
+ try {
107
+ await Promise.race([
108
+ wrapper.connect(config, llmConfig ?? this.currentLlmConfig, serverId),
109
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs)),
110
+ ]);
111
+ this.clients.set(serverId, wrapper);
112
+ this.assignPrefixId(serverId, wrapper);
113
+ this.statuses.set(serverId, {
114
+ serverId,
115
+ identity: wrapper.getIdentity(),
116
+ status: 'connected',
117
+ });
118
+ }
119
+ catch (err) {
120
+ this.statuses.set(serverId, {
121
+ serverId,
122
+ identity: 'unknown',
123
+ status: 'failed',
124
+ error: err?.message ?? String(err),
125
+ });
126
+ try {
127
+ await wrapper.close();
128
+ }
129
+ catch { /* ignore */ }
130
+ }
131
+ await this.refreshToolIndex();
132
+ }
133
+ /** Tear down a single server. Removes it from the pool and rebuilds the tool index. */
134
+ async disconnectOne(serverId) {
135
+ const wrapper = this.clients.get(serverId);
136
+ if (wrapper) {
137
+ try {
138
+ await wrapper.close();
139
+ }
140
+ catch { /* ignore */ }
141
+ }
142
+ this.clients.delete(serverId);
143
+ const oldPid = this.prefixIds.get(serverId);
144
+ if (oldPid)
145
+ this.prefixToServerId.delete(oldPid);
146
+ this.prefixIds.delete(serverId);
147
+ const prev = this.statuses.get(serverId);
148
+ this.statuses.set(serverId, {
149
+ serverId,
150
+ identity: prev?.identity ?? 'unknown',
151
+ status: 'offline',
152
+ });
153
+ await this.refreshToolIndex();
154
+ }
155
+ /** Reconnect: close + connect again using the stashed config. */
156
+ async reconnectOne(serverId) {
157
+ const config = this.serverConfigs.get(serverId);
158
+ if (!config) {
159
+ throw new Error(`No stored config for serverId "${serverId}". Add it to ~/.config/brainrouter/config.json first.`);
160
+ }
161
+ await this.disconnectOne(serverId);
162
+ await this.connectOne(serverId, config, this.currentLlmConfig);
163
+ }
164
+ /**
165
+ * Walk every connected client and rebuild the tool→server indices.
166
+ * Called after every connect / disconnect / reconnect so the
167
+ * dispatch path stays correct without re-fetching tools on every
168
+ * `callTool`.
169
+ */
170
+ async refreshToolIndex() {
171
+ this.toolToServer.clear();
172
+ this.prefixedToServer.clear();
173
+ for (const [serverId, wrapper] of this.clients) {
174
+ if (!wrapper.isConnected())
175
+ continue;
176
+ try {
177
+ const res = await wrapper.listTools();
178
+ const tools = res.tools ?? [];
179
+ const status = this.statuses.get(serverId);
180
+ if (status) {
181
+ status.toolCount = tools.length;
182
+ status.identity = wrapper.getIdentity();
183
+ }
184
+ for (const tool of tools) {
185
+ const rawName = tool.name;
186
+ const pid = this.getPrefixId(serverId);
187
+ const prefixed = `mcp_${pid}_${rawName}`;
188
+ this.prefixedToServer.set(prefixed, { serverId, tool: rawName });
189
+ const existing = this.toolToServer.get(rawName);
190
+ if (existing && existing !== serverId) {
191
+ // Two servers expose the same unprefixed tool name. Mark
192
+ // collision so the raw-name resolver knows to require the
193
+ // prefix.
194
+ this.toolToServer.set(rawName, '__COLLISION__');
195
+ }
196
+ else if (!existing) {
197
+ this.toolToServer.set(rawName, serverId);
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // Server is connected but listTools failed — its tools won't
203
+ // appear this turn. Will retry on the next refresh.
204
+ }
205
+ }
206
+ }
207
+ /**
208
+ * Concatenated tool list across every connected server, with names
209
+ * prefixed `mcp_<serverId>_<toolName>` (Claude Code style). The
210
+ * agent calls this once per turn and hands it to the LLM.
211
+ */
212
+ async listTools() {
213
+ const all = [];
214
+ for (const [serverId, wrapper] of this.clients) {
215
+ if (!wrapper.isConnected())
216
+ continue;
217
+ try {
218
+ const res = await wrapper.listTools();
219
+ const tools = res.tools ?? [];
220
+ const status = this.statuses.get(serverId);
221
+ if (status) {
222
+ status.identity = wrapper.getIdentity();
223
+ }
224
+ const pid = this.getPrefixId(serverId);
225
+ for (const tool of tools) {
226
+ all.push({
227
+ ...tool,
228
+ name: `mcp_${pid}_${tool.name}`,
229
+ __serverId: serverId,
230
+ __rawName: tool.name,
231
+ });
232
+ }
233
+ }
234
+ catch {
235
+ // listTools failed for this server — drop its tools this turn.
236
+ }
237
+ }
238
+ return { tools: all };
239
+ }
240
+ /**
241
+ * Route a tool call to the right server. Accepts both name forms:
242
+ *
243
+ * - `mcp_<serverId>_<tool>` — the canonical form the LLM sees
244
+ * in the inventory. Stripped + dispatched directly.
245
+ * - `<tool>` raw form — back-compat for prompts/skills that
246
+ * hardcode `memory_recall`-style names. Routed to the unique
247
+ * server providing that tool. Returns a helpful error if two
248
+ * servers expose the same name (caller must use the prefix).
249
+ */
250
+ async callTool(name, args) {
251
+ const resolved = this.resolveToolCall(name);
252
+ if (!resolved) {
253
+ // Distinguish the two failure modes — gives the LLM (and humans
254
+ // tailing logs) actionable feedback.
255
+ if (this.toolToServer.get(name) === '__COLLISION__') {
256
+ const prefixes = [...this.clients.keys()]
257
+ .filter((id) => {
258
+ const w = this.clients.get(id);
259
+ return w && w.isConnected() && this.prefixedToServer.has(`mcp_${this.getPrefixId(id)}_${name}`);
260
+ })
261
+ .map((id) => this.getPrefixId(id));
262
+ return {
263
+ isError: true,
264
+ content: [{
265
+ type: 'text',
266
+ text: `Ambiguous tool name "${name}" — exposed by ${prefixes.length} MCP servers: ${prefixes.join(', ')}. Use the prefixed form, e.g. mcp_${prefixes[0]}_${name}.`,
267
+ }],
268
+ };
269
+ }
270
+ return {
271
+ isError: true,
272
+ content: [{ type: 'text', text: `Tool "${name}" not found on any connected MCP server.` }],
273
+ };
274
+ }
275
+ const wrapper = this.clients.get(resolved.serverId);
276
+ if (!wrapper || !wrapper.isConnected()) {
277
+ return {
278
+ isError: true,
279
+ content: [{
280
+ type: 'text',
281
+ text: `MCP server "${resolved.serverId}" is offline; tool "${resolved.tool}" cannot be reached. Try /mcp reconnect ${resolved.serverId}.`,
282
+ }],
283
+ };
284
+ }
285
+ return wrapper.callTool(resolved.tool, args);
286
+ }
287
+ /** Internal — map a name (prefixed OR raw) to a concrete server + tool. */
288
+ resolveToolCall(name) {
289
+ // Fast path: exact prefixed form match in the index.
290
+ if (name.startsWith('mcp_')) {
291
+ const direct = this.prefixedToServer.get(name);
292
+ if (direct)
293
+ return direct;
294
+ // Lenient parse: walk prefix IDs first (covers "brainrouter"
295
+ // alias), then raw server IDs as fallback.
296
+ const rest = name.slice('mcp_'.length);
297
+ for (const [pid, realId] of this.prefixToServerId) {
298
+ const prefix = `${pid}_`;
299
+ if (rest.startsWith(prefix)) {
300
+ return { serverId: realId, tool: rest.slice(prefix.length) };
301
+ }
302
+ }
303
+ for (const serverId of this.clients.keys()) {
304
+ const prefix = `${serverId}_`;
305
+ if (rest.startsWith(prefix)) {
306
+ return { serverId, tool: rest.slice(prefix.length) };
307
+ }
308
+ }
309
+ return undefined;
310
+ }
311
+ if (isBrainrouterOwnedTool(name)) {
312
+ for (const [serverId, wrapper] of this.clients) {
313
+ if (wrapper.isConnected() &&
314
+ wrapper.getIdentity() === 'brainrouter' &&
315
+ this.prefixedToServer.has(`mcp_${serverId}_${name}`)) {
316
+ return { serverId, tool: name };
317
+ }
318
+ }
319
+ }
320
+ // Raw-name fallback.
321
+ const owner = this.toolToServer.get(name);
322
+ if (!owner || owner === '__COLLISION__')
323
+ return undefined;
324
+ return { serverId: owner, tool: name };
325
+ }
326
+ // ----- Facade methods that match McpClientWrapper's public surface -----
327
+ /** True iff at least one server is connected. */
328
+ isConnected() {
329
+ for (const w of this.clients.values()) {
330
+ if (w.isConnected())
331
+ return true;
332
+ }
333
+ return false;
334
+ }
335
+ /**
336
+ * Identity precedence: any connected `brainrouter` > any connected
337
+ * `third-party` > `unknown`. The CLI banner + offline prompt swap
338
+ * branch on this — "BrainRouter is offline" makes sense only when
339
+ * we expected one and didn't get one.
340
+ */
341
+ getIdentity() {
342
+ for (const w of this.clients.values()) {
343
+ if (w.isConnected() && w.getIdentity() === 'brainrouter')
344
+ return 'brainrouter';
345
+ }
346
+ for (const w of this.clients.values()) {
347
+ if (w.isConnected() && w.getIdentity() === 'third-party')
348
+ return 'third-party';
349
+ }
350
+ return 'unknown';
351
+ }
352
+ /**
353
+ * Human-readable summary for the banner/statusline. Single-server
354
+ * pools render just the server name; multi-server pools render
355
+ * a count + the first few names.
356
+ */
357
+ getServerName() {
358
+ const connected = [...this.clients.entries()]
359
+ .filter(([_, w]) => w.isConnected())
360
+ .map(([id]) => id);
361
+ if (connected.length === 0)
362
+ return undefined;
363
+ if (connected.length === 1)
364
+ return connected[0];
365
+ const head = connected.slice(0, 3).join(', ');
366
+ return connected.length > 3 ? `${connected.length} servers (${head}, …)` : `${connected.length} servers (${head})`;
367
+ }
368
+ /**
369
+ * Look up a wrapper by serverId. Used by `/mcp tools <server>` and
370
+ * similar commands that want to talk to one specific server.
371
+ */
372
+ getClient(serverId) {
373
+ return this.clients.get(serverId);
374
+ }
375
+ /**
376
+ * Find the connected wrapper whose identity is 'brainrouter'. Some
377
+ * code paths (memory capture, working-memory offload) specifically
378
+ * need the canonical brain regardless of how many third-party MCPs
379
+ * the user added.
380
+ */
381
+ getBrainrouterClient() {
382
+ for (const w of this.clients.values()) {
383
+ if (w.isConnected() && w.getIdentity() === 'brainrouter')
384
+ return w;
385
+ }
386
+ return undefined;
387
+ }
388
+ /** Server id for the currently connected BrainRouter MCP, if one is active. */
389
+ getActiveBrainrouterServerId() {
390
+ for (const [serverId, wrapper] of this.clients) {
391
+ if (wrapper.isConnected() && wrapper.getIdentity() === 'brainrouter')
392
+ return serverId;
393
+ }
394
+ return undefined;
395
+ }
396
+ /** Status snapshot for every server the pool has tried to connect to. */
397
+ getStatuses() {
398
+ return [...this.statuses.values()];
399
+ }
400
+ /** Status for one server (returns undefined if the pool has never seen it). */
401
+ getStatus(serverId) {
402
+ return this.statuses.get(serverId);
403
+ }
404
+ /** List of serverIds currently held by the pool (connected or not). */
405
+ getServerIds() {
406
+ return [...this.statuses.keys()];
407
+ }
408
+ /** Close every wrapper. Used on CLI exit. */
409
+ async close() {
410
+ for (const wrapper of this.clients.values()) {
411
+ try {
412
+ await wrapper.close();
413
+ }
414
+ catch { /* ignore */ }
415
+ }
416
+ this.clients.clear();
417
+ this.toolToServer.clear();
418
+ this.prefixedToServer.clear();
419
+ this.prefixIds.clear();
420
+ this.prefixToServerId.clear();
421
+ // Keep `statuses` so a `getStatuses()` after close still shows what was there.
422
+ }
423
+ }
@@ -1,4 +1,6 @@
1
1
  import type { McpClientWrapper } from './mcpClient.js';
2
+ import type { McpClientPool } from './mcpPool.js';
3
+ export type McpClient = McpClientWrapper | McpClientPool;
2
4
  /**
3
5
  * Centralized helpers for talking to the BrainRouter MCP server.
4
6
  *
@@ -27,7 +29,7 @@ export interface McpCallResult<T = any> {
27
29
  * Network and protocol errors are converted to `{ isError: true, text: errorMessage }`
28
30
  * so callers can branch on a single shape instead of mixing try/catch with isError checks.
29
31
  */
30
- export declare function callMcpTool<T = any>(client: McpClientWrapper, name: string, args: Record<string, unknown>): Promise<McpCallResult<T>>;
32
+ export declare function callMcpTool<T = any>(client: McpClient, name: string, args: Record<string, unknown>): Promise<McpCallResult<T>>;
31
33
  /**
32
34
  * Canonical convention for naming a child agent's session key relative to its
33
35
  * parent: `<parent>:child:<id>`. Centralized so a future change (e.g. switching
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinqs/brainrouter-cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Memory-native terminal coding agent. Talks to the BrainRouter MCP cognitive engine for recall, skills, capture, persona, focus scenes, and contradiction tracking.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  "brainrouter": "bin/cli.cjs"
9
9
  },
10
10
  "files": [
11
+ "agents",
11
12
  "bin",
12
13
  "dist",
13
14
  "README.md",
@@ -28,15 +29,20 @@
28
29
  "chalk": "^5.3.0",
29
30
  "commander": "^12.1.0",
30
31
  "dotenv": "^16.4.5",
32
+ "ink": "^7.0.3",
33
+ "ink-spinner": "^5.0.0",
34
+ "ink-text-input": "^6.0.0",
31
35
  "inquirer": "^9.3.2",
32
36
  "marked": "^12.0.1",
33
37
  "marked-terminal": "^7.0.0",
34
- "ora": "^8.0.1"
38
+ "ora": "^8.0.1",
39
+ "react": "^19.2.6"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@types/inquirer": "^9.0.7",
38
43
  "@types/marked": "^4.0.8",
39
44
  "@types/node": "^22.0.0",
45
+ "@types/react": "^19.2.15",
40
46
  "tsx": "^4.7.0",
41
47
  "typescript": "^5.5.4"
42
48
  },
package/.env.example DELETED
@@ -1,116 +0,0 @@
1
- # BrainRouter CLI agent — environment template
2
- #
3
- # Copy to `brainrouter-cli/.env`. Loaded by the CLI at startup.
4
- #
5
- # This file is for CLI-AGENT concerns only:
6
- # 1. Chat LLM (the model the terminal agent talks to)
7
- # 2. Tool runtime (loop limit, result clamp, MCP timeout, auto-compact)
8
- # 3. Sandbox (run_command wrapping)
9
- # 4. Workspace (root override + shared state root)
10
- # 5. Web search (custom backend for the web_search tool)
11
- # 6. Observability (trace log path)
12
- #
13
- # MCP-server concerns (cognitive extraction, embeddings, reranker, judge,
14
- # memory engine knobs, server auth) live in `brainrouter/.env.example`.
15
- #
16
- # Why split: the MCP and the CLI are separate processes with different
17
- # concerns. The CLI's chat LLM can be a smart cloud model while the MCP's
18
- # cognitive extractor is a cheap local one. Their concurrency caps differ
19
- # too (CLI default 4, MCP default 2). Keep them independent.
20
- #
21
- # All values in this template are blank placeholders. Fill in only what
22
- # you actually need — most settings have sensible defaults.
23
-
24
-
25
- # =============================================================================
26
- # 1. Chat LLM (for the agent's own conversation)
27
- # =============================================================================
28
- # Same var names as the MCP, but a separate process — set them here for the
29
- # CLI's chat model. Falls back to OPENAI_API_KEY.
30
- #
31
- # If you don't set BRAINROUTER_LLM_API_KEY here, the CLI also reads it from
32
- # `brainrouter/.env` as a transitional fallback. Setting it here makes the
33
- # CLI's choice explicit and lets you use a different chat model than the
34
- # MCP's extractor (e.g. gpt-4o for chat, gpt-4o-mini for extraction).
35
- BRAINROUTER_LLM_API_KEY=
36
-
37
- # OpenAI-compatible chat-completions endpoint.
38
- # Examples:
39
- # OpenAI: https://api.openai.com/v1/chat/completions
40
- # OpenRouter: https://openrouter.ai/api/v1/chat/completions
41
- # Anthropic via OpenRouter: model id "anthropic/claude-sonnet-4"
42
- # LM Studio: http://localhost:1234/v1/chat/completions
43
- BRAINROUTER_LLM_ENDPOINT=https://api.openai.com/v1/chat/completions
44
- BRAINROUTER_LLM_MODEL=gpt-4o-mini
45
-
46
- # Per-call timeout for the CLI's chat LLM. Default 120000 (2 min).
47
- # BRAINROUTER_LLM_TIMEOUT_MS=120000
48
-
49
- # Cap on concurrent in-flight chat LLM calls FROM THE CLI PROCESS.
50
- # Default 4 (separate from the MCP's own cap). Set to 1 for consumer-grade
51
- # local backends; raise to 16+ for cloud APIs.
52
- # BRAINROUTER_LLM_MAX_CONCURRENT=4
53
-
54
-
55
- # =============================================================================
56
- # 2. Tool runtime
57
- # =============================================================================
58
- # Per-tool timeout for CLI → MCP requests. Default 60000.
59
- # BRAINROUTER_MCP_TIMEOUT_MS=60000
60
-
61
- # LLM-visible clamp on a single tool-result body (full text still recorded
62
- # in the transcript on disk). Default 8000.
63
- # BRAINROUTER_MAX_TOOL_RESULT_CHARS=8000
64
-
65
- # Hard ceiling on tool-call iterations per turn. Default 60.
66
- # BRAINROUTER_MAX_TOOL_LOOPS=60
67
-
68
- # Estimated history-size trigger for auto-`/compact`. Default 80000 tokens.
69
- # BRAINROUTER_AUTO_COMPACT_TOKENS=80000
70
-
71
-
72
- # =============================================================================
73
- # 3. Sandbox (run_command)
74
- # =============================================================================
75
- # Wrap shell commands in the platform sandbox:
76
- # macOS: sandbox-exec
77
- # Linux: bwrap (preferred) or firejail
78
- # Set `on` to enable. Off by default.
79
- # BRAINROUTER_SANDBOX=on
80
-
81
- # Allow outbound network from sandboxed commands. Off by default.
82
- # BRAINROUTER_SANDBOX_NETWORK=off
83
-
84
- # Colon-separated read/write path allowlists.
85
- # BRAINROUTER_SANDBOX_READ_PATHS=/usr/local:/opt
86
- # BRAINROUTER_SANDBOX_WRITE_PATHS=/tmp
87
-
88
-
89
- # =============================================================================
90
- # 4. Workspace
91
- # =============================================================================
92
- # Override the workspace root the CLI uses for file tools + session key.
93
- # Most users let the CLI auto-detect via git / closest package.json.
94
- # BRAINROUTER_WORKSPACE=/path/to/project
95
-
96
- # Override per-user state root. Default: ~/.brainrouter.
97
- # Both the CLI and MCP honor this — set it once and both processes use it.
98
- # BRAINROUTER_HOME=/path/to/state
99
-
100
-
101
- # =============================================================================
102
- # 5. Web search
103
- # =============================================================================
104
- # Custom search backend for the web_search tool. Must accept
105
- # POST { query, maxResults } → { results: [{ title, url, snippet }] }
106
- # Falls back to DuckDuckGo's Instant Answer API when unset.
107
- # Compatible with Brave Search API wrappers, Tavily, SerpAPI proxies, etc.
108
- # BRAINROUTER_WEB_SEARCH_ENDPOINT=https://your-search-proxy.example.com/search
109
-
110
-
111
- # =============================================================================
112
- # 6. Observability
113
- # =============================================================================
114
- # Path for OTEL-style JSONL turn traces. One line per turn/tool span.
115
- # Toggle at runtime with /trace on|off.
116
- # BRAINROUTER_TRACE_LOG=/path/to/trace.jsonl