@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,20 @@
1
+ import type { CommandContext } from './_context.js';
2
+ /**
3
+ * `/init` slash command — 0.3.7 redesign.
4
+ *
5
+ * Two behaviours under one verb, picked by the first argument:
6
+ *
7
+ * - `/init` (bare) — re-run the onboarding wizard inside the REPL.
8
+ * The REPL already owns the readline, so the wizard reuses it
9
+ * (`ownsReadline: false`). Aborting at any step leaves disk
10
+ * untouched.
11
+ * - `/init agentmd` — back-compat alias for the 0.3.6 behaviour
12
+ * that only scaffolded AGENT.md (the wizard now folds this in as
13
+ * its final step, but users with muscle memory keep the lever).
14
+ *
15
+ * The auto-trigger on REPL start (when no `~/.config/brainrouter/config.json`
16
+ * exists) calls `runWizard` directly from `index.ts` with
17
+ * `ownsReadline: true`. That's a separate entry point — this slash
18
+ * handler is only for the in-REPL invocation.
19
+ */
20
+ export declare function tryHandleInitCommand(ctx: CommandContext): Promise<boolean>;
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+ import { initAgentMd } from '../../prompt/initAgentMd.js';
3
+ import { runWizard } from '../ink/runWizard.js';
4
+ /**
5
+ * `/init` slash command — 0.3.7 redesign.
6
+ *
7
+ * Two behaviours under one verb, picked by the first argument:
8
+ *
9
+ * - `/init` (bare) — re-run the onboarding wizard inside the REPL.
10
+ * The REPL already owns the readline, so the wizard reuses it
11
+ * (`ownsReadline: false`). Aborting at any step leaves disk
12
+ * untouched.
13
+ * - `/init agentmd` — back-compat alias for the 0.3.6 behaviour
14
+ * that only scaffolded AGENT.md (the wizard now folds this in as
15
+ * its final step, but users with muscle memory keep the lever).
16
+ *
17
+ * The auto-trigger on REPL start (when no `~/.config/brainrouter/config.json`
18
+ * exists) calls `runWizard` directly from `index.ts` with
19
+ * `ownsReadline: true`. That's a separate entry point — this slash
20
+ * handler is only for the in-REPL invocation.
21
+ */
22
+ export async function tryHandleInitCommand(ctx) {
23
+ const { command, args, agent, repl } = ctx;
24
+ if (command !== '/init')
25
+ return false;
26
+ // Back-compat: explicit subcommand keeps the 0.3.6 one-shot behaviour.
27
+ if (args[0]?.toLowerCase() === 'agentmd' || args[0]?.toLowerCase() === 'agent') {
28
+ const result = initAgentMd(agent.workspaceRoot);
29
+ if (result.status === 'created') {
30
+ console.log(chalk.green(`\n✓ Created ${result.path}`));
31
+ console.log(chalk.gray(' Edit it to describe your project — any AGENT.md-aware agent will read it.\n'));
32
+ }
33
+ else {
34
+ console.log(chalk.yellow(`\nFile already exists: ${result.path}`));
35
+ console.log(chalk.gray(' Run `/init agentmd --overwrite` if you really want to start fresh (TODO).\n'));
36
+ }
37
+ return true;
38
+ }
39
+ // Wizard mode. Ink owns stdin while the wizard is mounted; once it
40
+ // unmounts the REPL's readline resumes naturally because we kept the
41
+ // process.stdin handle around (Ink restores raw mode on exit).
42
+ try {
43
+ const result = await runWizard({
44
+ workspaceRoot: agent.workspaceRoot,
45
+ });
46
+ if (result.config?.llm) {
47
+ // Live-update the in-flight agent so the next turn uses the new
48
+ // model / endpoint without forcing a restart. Keep the wrapper's
49
+ // existing MCP connection — switching MCP needs a restart for
50
+ // now (next item on the polish list).
51
+ const llm = result.config.llm;
52
+ agent.setModel(llm.model);
53
+ // The agent's internal openai client cached the endpoint at
54
+ // construction time — repl users may need a fresh CLI process
55
+ // for endpoint changes to fully take effect.
56
+ console.log(chalk.gray(' (note: endpoint / API-key changes apply on the next CLI restart)\n'));
57
+ }
58
+ repl.refreshPromptForMode();
59
+ }
60
+ catch (err) {
61
+ console.error(chalk.red(`\n/init failed: ${err?.message ?? err}\n`));
62
+ }
63
+ return true;
64
+ }
@@ -0,0 +1,13 @@
1
+ import type { CommandContext } from './_context.js';
2
+ /**
3
+ * `/login` slash command — 0.3.7 redesign on the new internal picker.
4
+ *
5
+ * Opens a small modal that picks a transport (stdio / local-http /
6
+ * remote-http), gathers fields via the framed text prompt, runs a
7
+ * single 5s reachability probe, and saves the profile. Probe failure
8
+ * offers "save anyway / try a different transport / cancel".
9
+ *
10
+ * The legacy `brainrouter login` subcommand stays for users who
11
+ * scripted it.
12
+ */
13
+ export declare function tryHandleLoginCommand(ctx: CommandContext): Promise<boolean>;
@@ -0,0 +1,179 @@
1
+ import chalk from 'chalk';
2
+ import { saveConfig } from '../../config/config.js';
3
+ import { McpClientWrapper } from '../../runtime/mcpClient.js';
4
+ import { maskApiKey } from '../wizard/providers.js';
5
+ // 0.3.7 — picker / prompt moved to Ink (see commands/config.ts for the
6
+ // full rationale on why the raw-stdout primitives were retired).
7
+ import { runPicker, runTextField } from '../ink/runPicker.js';
8
+ const pickFromList = runPicker;
9
+ const promptText = runTextField;
10
+ import { buildTheme } from '../theme.js';
11
+ import { readPreferences } from '../../state/preferencesStore.js';
12
+ import { editLlm, promptBrainrouterApiKey } from './config.js';
13
+ /**
14
+ * `/login` slash command — 0.3.7 redesign on the new internal picker.
15
+ *
16
+ * Opens a small modal that picks a transport (stdio / local-http /
17
+ * remote-http), gathers fields via the framed text prompt, runs a
18
+ * single 5s reachability probe, and saves the profile. Probe failure
19
+ * offers "save anyway / try a different transport / cancel".
20
+ *
21
+ * The legacy `brainrouter login` subcommand stays for users who
22
+ * scripted it.
23
+ */
24
+ export async function tryHandleLoginCommand(ctx) {
25
+ if (ctx.command !== '/login')
26
+ return false;
27
+ const theme = buildTheme(readPreferences(ctx.agent.workspaceRoot).theme === 'mono' ? 'mono' : readPreferences(ctx.agent.workspaceRoot).theme === 'light' ? 'light' : 'dark');
28
+ while (true) {
29
+ const transport = await pickFromList({
30
+ theme,
31
+ title: '/login — MCP profile',
32
+ subtitle: 'Pick how this CLI reaches the BrainRouter MCP.',
33
+ rows: [
34
+ { id: 'local-stdio', label: 'Local stdio', value: 'brainrouter-mcp', description: 'No HTTP server needed' },
35
+ { id: 'local-http', label: 'Local HTTP', value: 'localhost:3747', description: 'Connect to a brainrouter-mcp HTTP server running locally' },
36
+ { id: 'remote-http', label: 'Remote HTTP', value: 'custom URL', description: 'Hosted MCP server (URL + optional key)' },
37
+ ],
38
+ });
39
+ if (transport.kind !== 'pick') {
40
+ console.log(chalk.yellow('\n /login cancelled.\n'));
41
+ return true;
42
+ }
43
+ let serverConfig;
44
+ let profileName = '';
45
+ if (transport.id === 'local-stdio') {
46
+ serverConfig = { type: 'stdio', command: 'brainrouter-mcp', args: [], identity: 'brainrouter' };
47
+ profileName = 'local-stdio';
48
+ }
49
+ else if (transport.id === 'local-http') {
50
+ // 0.3.7 — collect the BrainRouter API key even for local-http.
51
+ // brainrouter-mcp HTTP servers can require auth (BRAINROUTER_API_KEY
52
+ // set in their server.env). Pre-fill from the env var; blank is OK
53
+ // for unauthenticated dev servers.
54
+ const apiKey = await promptBrainrouterApiKey(theme, 'local', ctx.config.servers['local-http']?.apiKey);
55
+ if (apiKey === undefined) {
56
+ console.log(chalk.yellow('\n /login cancelled.\n'));
57
+ return true;
58
+ }
59
+ serverConfig = {
60
+ type: 'http',
61
+ url: 'http://localhost:3747/mcp',
62
+ apiKey: apiKey || undefined,
63
+ identity: 'brainrouter',
64
+ };
65
+ profileName = 'local-http';
66
+ }
67
+ else {
68
+ const urlResult = await promptText({
69
+ theme,
70
+ title: 'Remote MCP URL',
71
+ subtitle: 'Paste the full URL (e.g. https://brainrouter.example.com/mcp).',
72
+ prefilled: ctx.config.servers['remote']?.url ?? '',
73
+ placeholder: 'https://...',
74
+ validate: (raw) => {
75
+ const v = raw.trim();
76
+ if (!v)
77
+ return 'URL required';
78
+ try {
79
+ new URL(v);
80
+ }
81
+ catch {
82
+ return 'not a valid URL';
83
+ }
84
+ return undefined;
85
+ },
86
+ });
87
+ if (urlResult.kind !== 'accept') {
88
+ console.log(chalk.yellow('\n /login cancelled.\n'));
89
+ return true;
90
+ }
91
+ const url = urlResult.text.trim();
92
+ const apiKey = await promptBrainrouterApiKey(theme, 'remote', ctx.config.servers['remote']?.apiKey);
93
+ if (apiKey === undefined) {
94
+ console.log(chalk.yellow('\n /login cancelled.\n'));
95
+ return true;
96
+ }
97
+ serverConfig = { type: 'http', url, apiKey: apiKey || undefined, identity: 'brainrouter' };
98
+ profileName = 'remote';
99
+ }
100
+ const probe = await probeMcpProfile(serverConfig, profileName);
101
+ if (!probe.ok) {
102
+ const choice = await pickFromList({
103
+ theme,
104
+ title: 'MCP probe failed',
105
+ subtitle: probe.error,
106
+ rows: [
107
+ { id: 'save', label: 'Save anyway', description: 'Persist the profile; run /mcp reconnect once the server is up' },
108
+ { id: 'retry', label: 'Try a different transport', description: 'Re-open the picker' },
109
+ { id: 'cancel', label: 'Cancel', description: 'Discard — nothing written' },
110
+ ],
111
+ });
112
+ if (choice.kind !== 'pick' || choice.id === 'cancel') {
113
+ console.log(chalk.yellow('\n /login cancelled.\n'));
114
+ return true;
115
+ }
116
+ if (choice.id === 'retry')
117
+ continue;
118
+ }
119
+ else {
120
+ console.log(chalk.green(`\n ✓ Probe succeeded (${probe.latencyMs}ms).`));
121
+ }
122
+ ctx.config.servers[profileName] = serverConfig;
123
+ ctx.config.activeServer = profileName;
124
+ saveConfig(ctx.config);
125
+ const apiKeyDisplay = serverConfig.apiKey ? maskApiKey(serverConfig.apiKey) : '(no key)';
126
+ console.log(chalk.green(` ✓ MCP profile "${profileName}" saved as active. ${apiKeyDisplay}`));
127
+ console.log(chalk.gray(' Run /mcp reconnect to pick up the new transport without restarting.\n'));
128
+ // 0.3.7 — follow-on LLM credential step. Pre-0.3.7 `/login`
129
+ // *only* handled the MCP profile; users who wanted to refresh
130
+ // their LLM API key in the same flow had to bounce out to
131
+ // `/config` or `/init`. Now we offer it inline.
132
+ //
133
+ // Always offer (per user direction); default-No when LLM
134
+ // creds are already populated, default-Yes when missing.
135
+ const hasLlm = Boolean(ctx.config.llm?.apiKey?.trim()) && Boolean(ctx.config.llm?.endpoint);
136
+ const promptSubtitle = hasLlm
137
+ ? `Current: ${ctx.config.llm?.model ?? '(unset)'} @ ${ctx.config.llm?.endpoint ?? '(no endpoint)'} · key ${maskApiKey(ctx.config.llm?.apiKey ?? '')}`
138
+ : 'No LLM credentials saved yet — set them now so the next turn works.';
139
+ const llmChoice = await pickFromList({
140
+ theme,
141
+ title: 'Update LLM credentials?',
142
+ subtitle: promptSubtitle,
143
+ rows: hasLlm ? [
144
+ { id: 'skip', label: 'Skip — keep current LLM config', description: 'Press ENTER to exit /login' },
145
+ { id: 'update', label: 'Update LLM', description: 'Switch provider / paste a new key / change model' },
146
+ ] : [
147
+ { id: 'update', label: 'Set LLM now', description: 'Provider → API key → Model' },
148
+ { id: 'skip', label: 'Skip — set up later via /config', description: 'Exit /login without LLM config' },
149
+ ],
150
+ initialCursor: 0,
151
+ });
152
+ if (llmChoice.kind === 'pick' && llmChoice.id === 'update') {
153
+ const ok = await editLlm(ctx);
154
+ if (!ok) {
155
+ console.log(chalk.yellow(' /login — LLM step cancelled; MCP profile saved.\n'));
156
+ }
157
+ }
158
+ return true;
159
+ }
160
+ }
161
+ async function probeMcpProfile(serverConfig, name) {
162
+ const wrapper = new McpClientWrapper();
163
+ const start = Date.now();
164
+ try {
165
+ await Promise.race([
166
+ wrapper.connect(serverConfig, undefined, name),
167
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timed out after 5s')), 5_000)),
168
+ ]);
169
+ await wrapper.close();
170
+ return { ok: true, latencyMs: Date.now() - start };
171
+ }
172
+ catch (err) {
173
+ try {
174
+ await wrapper.close();
175
+ }
176
+ catch { /* ignore */ }
177
+ return { ok: false, error: String(err?.message ?? err) };
178
+ }
179
+ }
@@ -1,17 +1,19 @@
1
1
  /**
2
- * 0.3.6 item 11: `/mcp` slash-command surface. Scope-limited foundation
3
- * the full multi-MCP federation (parallel cross-MCP tool calls, MCP
4
- * marketplace, capability tiers) is deferred to 0.4.0. What ships here:
2
+ * `/mcp` slash-command surface. 0.3.7 multi-MCP rewrite third-party
3
+ * MCPs connect concurrently while BrainRouter MCP profiles are mutually
4
+ * exclusive so exactly one brain is active at a time.
5
5
  *
6
- * /mcp show status of the active MCP (alias for /mcp list)
7
- * /mcp list list every configured profile with identity + status
8
- * /mcp reconnectreconnect the currently-active profile
9
- * /mcp tools — list MCP tools grouped by namespace (pre-Item-11
10
- * `/mcp` no-arg behaviour, moved here verbatim)
6
+ * /mcp — alias for /mcp list
7
+ * /mcp list — every configured profile + per-server status
8
+ * /mcp tools [server] MCP tools grouped by `mcp__<server>__*`
9
+ * namespace; pass a server id to scope
10
+ * /mcp connect <name> — connect a configured server that's idle/offline
11
+ * /mcp disconnect <name> — close one server's transport (config preserved)
12
+ * /mcp reconnect [name] — reconnect ONE (when name given) or the selected pool
11
13
  *
12
- * The reconnect path leans on `mcpClient.close()` + `mcpClient.connect()`
13
- * with the same config the CLI launched against no plumbing required
14
- * for the user beyond typing the command.
14
+ * Backwards-compat: `/mcp reconnect` with no arg reconnects the same
15
+ * selected set used at boot: all third-party MCPs plus the active
16
+ * BrainRouter MCP. Pass an explicit name to target one.
15
17
  */
16
18
  import type { CommandContext } from './_context.js';
17
19
  export declare function tryHandleMcpCommand(ctx: CommandContext): Promise<boolean>;