@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.
Files changed (125) 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/bin/cli.cjs +71 -0
  8. package/dist/agent/agent.d.ts +224 -3
  9. package/dist/agent/agent.js +561 -55
  10. package/dist/cli/banner.d.ts +80 -0
  11. package/dist/cli/banner.js +232 -0
  12. package/dist/cli/cliPrompt.d.ts +106 -0
  13. package/dist/cli/cliPrompt.js +314 -0
  14. package/dist/cli/commands/_context.d.ts +3 -1
  15. package/dist/cli/commands/_helpers.d.ts +1 -1
  16. package/dist/cli/commands/_helpers.js +6 -6
  17. package/dist/cli/commands/config.d.ts +46 -0
  18. package/dist/cli/commands/config.js +1042 -0
  19. package/dist/cli/commands/guard.js +75 -10
  20. package/dist/cli/commands/init.d.ts +20 -0
  21. package/dist/cli/commands/init.js +64 -0
  22. package/dist/cli/commands/login.d.ts +13 -0
  23. package/dist/cli/commands/login.js +179 -0
  24. package/dist/cli/commands/mcp.d.ts +19 -0
  25. package/dist/cli/commands/mcp.js +286 -0
  26. package/dist/cli/commands/memory.js +2 -2
  27. package/dist/cli/commands/obs.js +22 -22
  28. package/dist/cli/commands/orchestration.js +18 -0
  29. package/dist/cli/commands/session.js +13 -5
  30. package/dist/cli/commands/ui.js +202 -91
  31. package/dist/cli/commands/workflow.d.ts +20 -0
  32. package/dist/cli/commands/workflow.js +368 -51
  33. package/dist/cli/ink/ChatApp.d.ts +206 -0
  34. package/dist/cli/ink/ChatApp.js +493 -0
  35. package/dist/cli/ink/Frame.d.ts +26 -0
  36. package/dist/cli/ink/Frame.js +5 -0
  37. package/dist/cli/ink/Picker.d.ts +65 -0
  38. package/dist/cli/ink/Picker.js +133 -0
  39. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  40. package/dist/cli/ink/SlashPalette.js +136 -0
  41. package/dist/cli/ink/TextField.d.ts +34 -0
  42. package/dist/cli/ink/TextField.js +47 -0
  43. package/dist/cli/ink/WizardApp.d.ts +7 -0
  44. package/dist/cli/ink/WizardApp.js +422 -0
  45. package/dist/cli/ink/ambientChat.d.ts +34 -0
  46. package/dist/cli/ink/ambientChat.js +7 -0
  47. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  48. package/dist/cli/ink/consoleCapture.js +33 -0
  49. package/dist/cli/ink/markdownRender.d.ts +41 -0
  50. package/dist/cli/ink/markdownRender.js +278 -0
  51. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  52. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  53. package/dist/cli/ink/runChat.d.ts +34 -0
  54. package/dist/cli/ink/runChat.js +571 -0
  55. package/dist/cli/ink/runPicker.d.ts +31 -0
  56. package/dist/cli/ink/runPicker.js +139 -0
  57. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  58. package/dist/cli/ink/runSlashPalette.js +33 -0
  59. package/dist/cli/ink/runWizard.d.ts +22 -0
  60. package/dist/cli/ink/runWizard.js +133 -0
  61. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  62. package/dist/cli/ink/stdinHandoff.js +78 -0
  63. package/dist/cli/ink/toolFormat.d.ts +73 -0
  64. package/dist/cli/ink/toolFormat.js +180 -0
  65. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  66. package/dist/cli/ink/useTerminalSize.js +26 -0
  67. package/dist/cli/repl.d.ts +25 -3
  68. package/dist/cli/repl.js +64 -646
  69. package/dist/cli/slashSuggest.d.ts +32 -0
  70. package/dist/cli/slashSuggest.js +146 -0
  71. package/dist/cli/spinner.d.ts +34 -0
  72. package/dist/cli/spinner.js +36 -0
  73. package/dist/cli/statusline.d.ts +67 -0
  74. package/dist/cli/statusline.js +204 -0
  75. package/dist/cli/theme.d.ts +79 -0
  76. package/dist/cli/theme.js +106 -0
  77. package/dist/cli/whereView.d.ts +81 -0
  78. package/dist/cli/whereView.js +245 -0
  79. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  80. package/dist/cli/wizard/modelsApi.js +166 -0
  81. package/dist/cli/wizard/picker.d.ts +202 -0
  82. package/dist/cli/wizard/picker.js +547 -0
  83. package/dist/cli/wizard/providers.d.ts +86 -0
  84. package/dist/cli/wizard/providers.js +190 -0
  85. package/dist/cli/wizard/runner.d.ts +13 -0
  86. package/dist/cli/wizard/runner.js +488 -0
  87. package/dist/cli/wizard/types.d.ts +122 -0
  88. package/dist/cli/wizard/types.js +109 -0
  89. package/dist/config/config.d.ts +52 -0
  90. package/dist/config/config.js +89 -75
  91. package/dist/index.js +215 -206
  92. package/dist/memory/briefing.d.ts +11 -1
  93. package/dist/memory/briefing.js +69 -1
  94. package/dist/memory/consolidation.d.ts +1 -1
  95. package/dist/orchestration/agentRegistry.d.ts +36 -0
  96. package/dist/orchestration/agentRegistry.js +64 -0
  97. package/dist/orchestration/orchestrator.d.ts +7 -0
  98. package/dist/orchestration/orchestrator.js +2 -0
  99. package/dist/orchestration/tools.d.ts +10 -1
  100. package/dist/orchestration/tools.js +48 -4
  101. package/dist/prompt/breadthHint.d.ts +5 -0
  102. package/dist/prompt/breadthHint.js +44 -0
  103. package/dist/prompt/skillCatalog.d.ts +11 -0
  104. package/dist/prompt/skillCatalog.js +134 -0
  105. package/dist/prompt/skillRunner.d.ts +2 -2
  106. package/dist/prompt/skillRunner.js +2 -31
  107. package/dist/prompt/systemPrompt.d.ts +34 -0
  108. package/dist/prompt/systemPrompt.js +128 -108
  109. package/dist/runtime/dangerousCommand.d.ts +53 -0
  110. package/dist/runtime/dangerousCommand.js +105 -0
  111. package/dist/runtime/mcpClient.d.ts +38 -1
  112. package/dist/runtime/mcpClient.js +104 -13
  113. package/dist/runtime/mcpPool.d.ts +162 -0
  114. package/dist/runtime/mcpPool.js +423 -0
  115. package/dist/runtime/mcpUtils.d.ts +3 -1
  116. package/dist/state/goalStore.d.ts +98 -17
  117. package/dist/state/goalStore.js +132 -42
  118. package/dist/state/preferencesStore.d.ts +67 -3
  119. package/dist/state/preferencesStore.js +84 -1
  120. package/dist/state/workflowArtifacts.d.ts +63 -2
  121. package/dist/state/workflowArtifacts.js +120 -8
  122. package/dist/tests/_helpers.d.ts +31 -0
  123. package/dist/tests/_helpers.js +91 -0
  124. package/package.json +12 -5
  125. package/.env.example +0 -109
@@ -5,7 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import chalk from 'chalk';
8
- import { readPreferences, writePreferences } from '../../state/preferencesStore.js';
8
+ import { applyYoloOff, applyYoloOn, readPreferences, writePreferences } from '../../state/preferencesStore.js';
9
9
  import { addHook, readHooks, removeHook, setHookEnabled } from '../../state/hooksStore.js';
10
10
  import { createHookifyRule, deleteHookifyRule, listHookifyRules, toggleHookifyRule } from '../../state/hookifyStore.js';
11
11
  export async function tryHandleGuardCommand(ctx) {
@@ -79,26 +79,91 @@ export async function tryHandleGuardCommand(ctx) {
79
79
  console.log(chalk.red('\nUsage: /hooks [list | add <event> <cmd> | remove <id> | enable <id> | disable <id>]\n'));
80
80
  return true;
81
81
  }
82
- case '/yolo':
82
+ case '/mode':
83
+ {
84
+ const prefs = readPreferences(agent.workspaceRoot);
85
+ const arg = (args[0] ?? '').toLowerCase();
86
+ if (!arg) {
87
+ console.log(chalk.bold(`\nExecution mode: ${chalk.cyan(prefs.executionMode)}`));
88
+ console.log(chalk.gray(' planning — run_command asks before executing; agent leans toward clarify-before-act. (default)'));
89
+ console.log(chalk.gray(' fast — run_command auto-approves safe commands (dangerous ones still ask); agent jumps to implementation.'));
90
+ console.log(chalk.gray(' Toggle with: /mode planning | /mode fast\n'));
91
+ return true;
92
+ }
93
+ if (arg !== 'planning' && arg !== 'fast') {
94
+ console.log(chalk.red(`\nUnknown mode "${arg}". Choose: planning | fast\n`));
95
+ return true;
96
+ }
97
+ writePreferences(agent.workspaceRoot, { executionMode: arg });
98
+ agent.refreshSystemPrompt();
99
+ ctx.repl.refreshPromptForMode();
100
+ if (arg === 'fast') {
101
+ console.log(chalk.yellow(`\n✓ /mode fast — run_command auto-approves safe commands.`));
102
+ console.log(chalk.gray(' Dangerous commands (rm -rf, sudo, force-push, …) still prompt for confirmation.'));
103
+ console.log(chalk.gray(' Pair with /permissions write (no shell) or BRAINROUTER_SANDBOX=on for tighter guardrails.\n'));
104
+ }
105
+ else {
106
+ console.log(chalk.green(`\n✓ /mode planning — run_command asks before each shell call.\n`));
107
+ }
108
+ return true;
109
+ }
110
+ case '/review-policy':
83
111
  {
84
112
  const prefs = readPreferences(agent.workspaceRoot);
85
113
  const arg = (args[0] ?? '').toLowerCase();
86
114
  if (!arg) {
87
- console.log(chalk.bold(`\nAuto-approve shell: ${prefs.autoApproveShell ? chalk.red('ON') : chalk.green('off')}`));
88
- console.log(chalk.gray(' When ON, run_command skips the per-call confirmation prompt and executes immediately.'));
89
- console.log(chalk.gray(' Pair with BRAINROUTER_SANDBOX=on if you still want a safety net.'));
115
+ console.log(chalk.bold(`\nReview policy: ${chalk.cyan(prefs.reviewPolicy)}`));
116
+ console.log(chalk.gray(' request — at workflow/multi-file gates, agent surfaces the plan and waits for /approve. (default)'));
117
+ console.log(chalk.gray(' proceed — agent applies the plan and reports after; use /approve manually for explicit gates.'));
118
+ console.log(chalk.gray(' Toggle with: /review-policy request | /review-policy proceed\n'));
119
+ return true;
120
+ }
121
+ if (arg !== 'request' && arg !== 'proceed') {
122
+ console.log(chalk.red(`\nUnknown policy "${arg}". Choose: request | proceed\n`));
123
+ return true;
124
+ }
125
+ writePreferences(agent.workspaceRoot, { reviewPolicy: arg });
126
+ agent.refreshSystemPrompt();
127
+ if (arg === 'proceed') {
128
+ console.log(chalk.yellow(`\n✓ /review-policy proceed — agent will apply plans without halting for prose approval.`));
129
+ console.log(chalk.gray(' /approve still works as an explicit gesture for workflows that need one.\n'));
130
+ }
131
+ else {
132
+ console.log(chalk.green(`\n✓ /review-policy request — agent will summarize and ask before applying multi-file changes.\n`));
133
+ }
134
+ return true;
135
+ }
136
+ case '/yolo':
137
+ {
138
+ // /yolo is a one-release alias for `/mode fast` + `/review-policy proceed`.
139
+ // We keep it because the muscle memory is established; new docs point to
140
+ // the two split commands for finer control.
141
+ const arg = (args[0] ?? '').toLowerCase();
142
+ if (!arg) {
143
+ const prefs = readPreferences(agent.workspaceRoot);
144
+ const yoloOn = prefs.executionMode === 'fast' && prefs.reviewPolicy === 'proceed';
145
+ console.log(chalk.bold(`\nYolo (alias): ${yoloOn ? chalk.red('ON') : chalk.green('off')}`));
146
+ console.log(chalk.gray(' Shorthand for `/mode fast` + `/review-policy proceed` — flip both axes at once.'));
147
+ console.log(chalk.gray(` Current state: mode=${prefs.executionMode}, review-policy=${prefs.reviewPolicy}`));
148
+ console.log(chalk.gray(' Use /mode and /review-policy directly for finer control.'));
90
149
  console.log(chalk.gray(' Toggle with: /yolo on | /yolo off\n'));
91
150
  return true;
92
151
  }
93
152
  const next = arg === 'on' || arg === 'true' || arg === '1';
94
- writePreferences(agent.workspaceRoot, { autoApproveShell: next });
95
153
  if (next) {
96
- console.log(chalk.red('\n⚠ /yolo ON — run_command will now execute without asking.'));
97
- console.log(chalk.gray(' You are in access mode "shell" so the agent CAN call shell commands.'));
98
- console.log(chalk.gray(' Lower the risk with /permissions write (no shell), or set BRAINROUTER_SANDBOX=on.\n'));
154
+ applyYoloOn(agent.workspaceRoot);
155
+ agent.refreshSystemPrompt();
156
+ ctx.repl.refreshPromptForMode();
157
+ console.log(chalk.red('\n⚠ /yolo ON — shorthand for `/mode fast` + `/review-policy proceed`.'));
158
+ console.log(chalk.gray(' run_command will auto-approve safe commands; dangerous ones still prompt.'));
159
+ console.log(chalk.gray(' Agent will apply multi-file plans without the prose "ready?" pause.'));
160
+ console.log(chalk.gray(' Use /mode and /review-policy for finer control next time.\n'));
99
161
  }
100
162
  else {
101
- console.log(chalk.green('\n✓ /yolo off — run_command will prompt for confirmation again.\n'));
163
+ applyYoloOff(agent.workspaceRoot);
164
+ agent.refreshSystemPrompt();
165
+ ctx.repl.refreshPromptForMode();
166
+ console.log(chalk.green('\n✓ /yolo off — restored /mode planning + /review-policy request.\n'));
102
167
  }
103
168
  return true;
104
169
  }
@@ -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
+ }
@@ -0,0 +1,19 @@
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 type { CommandContext } from './_context.js';
19
+ export declare function tryHandleMcpCommand(ctx: CommandContext): Promise<boolean>;