@kinqs/brainrouter-mcp-server 0.3.6 → 0.3.8

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 CHANGED
@@ -34,7 +34,7 @@ Verify:
34
34
 
35
35
  ```bash
36
36
  which brainrouter-mcp
37
- brainrouter-mcp --version # prints 0.3.5
37
+ brainrouter-mcp --version # prints 0.3.7
38
38
  ```
39
39
 
40
40
  ---
@@ -37,9 +37,14 @@ export function verifyJwt(token, secret) {
37
37
  if (parts.length !== 3)
38
38
  return null;
39
39
  const [header, claims, signature] = parts;
40
- const expected = createHmac("sha256", secret).update(`${header}.${claims}`).digest();
41
- const actual = Buffer.from(signature, "base64url");
42
- if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
40
+ // Compare in base64url string space so single-character changes to padding
41
+ // bits (e.g. "A" → "B" at the last position of a 32-byte HMAC) are caught.
42
+ // Raw-byte comparison misses these because base64url decoding ignores the
43
+ // bottom 2 bits of the final character.
44
+ const expected = createHmac("sha256", secret).update(`${header}.${claims}`).digest("base64url");
45
+ const expBuf = Buffer.from(expected);
46
+ const actBuf = Buffer.from(signature);
47
+ if (expBuf.length !== actBuf.length || !timingSafeEqual(expBuf, actBuf))
43
48
  return null;
44
49
  try {
45
50
  const payload = JSON.parse(base64UrlDecode(claims).toString("utf8"));
package/dist/index.js CHANGED
@@ -102,7 +102,7 @@ const PORT = parseInt(parseFlag('--port') ?? '3747', 10);
102
102
  function buildMcpServer(registry, options) {
103
103
  const defaultUserId = options?.defaultUserId ?? STDIO_DEFAULT_USER_ID;
104
104
  const isAdmin = options?.isAdmin ?? false;
105
- const server = new Server({ name: 'brainrouter-mcp-server', version: '0.3.5' }, { capabilities: { tools: {} } });
105
+ const server = new Server({ name: 'brainrouter-mcp-server', version: '0.3.7' }, { capabilities: { tools: {} } });
106
106
  // ── Tool list ──────────────────────────────────────────────────────────────
107
107
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
108
108
  tools: [
@@ -0,0 +1,259 @@
1
+ # Spec: 0.3.7 Item 6 — Terminal UI redesign + in-terminal config wizard
2
+
3
+ > Status: drafted, awaiting implementation. See [Tasks.md](../../Tasks.md)
4
+ > for live progress. Pairs with [`brainrouter-roadmap/0.3.7.md`](../../brainrouter-roadmap/0.3.7.md)
5
+ > as the headline feature of the 0.3.7 cycle.
6
+
7
+ ## Objective
8
+
9
+ Make BrainRouter's CLI feel like Claude Code / OpenCode / Codex / Grok-CLI
10
+ / DeepSeek-TUI — **especially around in-terminal, picker-driven
11
+ configuration**. Today a first-run user has to:
12
+
13
+ 1. Run `brainrouter login` outside the REPL → answers a sequence of
14
+ `inquirer` text prompts → exits.
15
+ 2. Run `brainrouter config` outside the REPL → answers another sequence
16
+ of text prompts → exits.
17
+ 3. Then start `brainrouter` (the chat REPL) and discover everything else
18
+ (`/theme`, `/personality`, `/effort`, `/mode`, statusline) via
19
+ `/help` or by hand-editing JSON.
20
+
21
+ Peer CLIs do this differently:
22
+
23
+ - **Claude Code** — `/config` opens a tabbed settings panel; `/theme`,
24
+ `/model`, `/login` are arrow-key pickers; first-run shows a welcome
25
+ card → onboarding wizard with a step counter.
26
+ - **Codex** — `Step::{Welcome, Auth, TrustDirectory}` state machine
27
+ drives the onboarding; one modal owns multiple sub-states (`PickMode`
28
+ → `ApiKeyEntry`) via an enum.
29
+ - **DeepSeek-TUI** — `/config` is verb-overloaded: bare opens a picker,
30
+ `<key>` shows, `<key> <val>` sets session, `<key> <val> --save`
31
+ persists. Provider picker uses a single modal with `Stage::{List,
32
+ KeyEntry}`; selecting an un-configured provider transitions the same
33
+ modal into key entry.
34
+ - **Grok-CLI** — first-run is a render-time `ApiKeyModal` overlay
35
+ inside the REPL, not a separate `login` subcommand. Each settings
36
+ surface (`/models`, `/mcp`, `/sandbox`, `/wallet`) is its own modal
37
+ that merges through a single `saveUserSettings(partial)`.
38
+
39
+ We adopt the **render-time modal** + **state-enum** + **verb-overloaded
40
+ slash command** patterns, on top of our existing
41
+ `brainrouter-cli/src/cli/cliPrompt.ts` picker primitive.
42
+
43
+ ## Non-goals
44
+
45
+ - A full Ratatui/Ink-style fullscreen renderer. That's the 0.5.0 cycle.
46
+ - Replacing `readline` as the REPL composer. We extend the existing
47
+ raw-mode picker; we do not rewrite the input loop.
48
+ - Multi-provider OAuth flows. v0.3.7 keeps API-key auth; OAuth /
49
+ device-code can be a follow-up once the wizard skeleton lands.
50
+ - Touching the MCP server's config (`~/.config/brainrouter/server.env`).
51
+ The wizard reads/writes only the CLI-owned files:
52
+ `~/.config/brainrouter/config.json` and
53
+ `<workspace>/.brainrouter/cli/preferences.json`.
54
+
55
+ ## Tech stack
56
+
57
+ - Same as the rest of `brainrouter-cli/`: TypeScript on Node 22+, ESM,
58
+ Vitest, `chalk` for color, our own `cliPrompt.askChoice` for the
59
+ arrow-key picker.
60
+ - `inquirer` is deliberately **removed from the new flows** — its
61
+ competing readline interface is the bug that motivated `cliPrompt.ts`
62
+ in the first place. Legacy `brainrouter login` / `brainrouter config`
63
+ subcommands stay (for back-compat) but the docs steer users at the
64
+ new in-REPL flows.
65
+
66
+ ## Slash command surface
67
+
68
+ Following the verb-overload pattern from DeepSeek-TUI:
69
+
70
+ | Command | Behaviour |
71
+ | --- | --- |
72
+ | `/init` | Re-runs the first-run onboarding wizard (theme → provider → model → MCP → done). Idempotent; safe to re-run. **Aliased from the old `/init` which only wrote `AGENT.md`** — that one-shot is folded into the wizard's final step as an opt-in toggle so we don't regress for users who relied on it. |
73
+ | `/config` | Bare — opens the **settings home panel** (picker over: LLM, MCP, Theme, Statusline, Effort, Mode, Quiet, Personality, Editor mode, View raw config). |
74
+ | `/config <key>` | Show current value for `<key>` (e.g. `/config theme` → `theme: dark (preference)`). |
75
+ | `/config <key> <value>` | Set `<key>` to `<value>` and persist (writes to `~/.config/brainrouter/config.json` for LLM/MCP knobs, `<workspace>/.brainrouter/cli/preferences.json` for prefs). |
76
+ | `/login` | NEW slash alias for the old `brainrouter login` flow — opens the **MCP profile editor** in-REPL (transport picker → fields → reachability test → save). The standalone `brainrouter login` CLI subcommand stays for first-run-before-REPL use. |
77
+ | `/logout` | Already exists; unchanged. Clears API keys from the active profile. |
78
+ | `/theme` | Already exists. Extended with `/theme` (bare) opening the picker (live-preview the banner + prompt accent on cursor-change), confirming via ENTER, restoring on Esc. |
79
+ | `/model` | Already exists. Extended with bare-`/model` opening a picker over a curated short-list (gpt-4o-mini, gpt-4o, gpt-5, claude-sonnet-4, deepseek-v4, qwen3-coder, …) + "Other" for free-text. |
80
+
81
+ The bare `/config` panel is the **settings home** — it's the screen
82
+ that lets users discover every knob without needing to memorise the
83
+ slash vocabulary.
84
+
85
+ ## Onboarding wizard (`/init`)
86
+
87
+ State machine — same shape as Codex `Step` and DeepSeek
88
+ `OnboardingState`:
89
+
90
+ ```
91
+ Welcome → Theme → Provider → ApiKey → Model → MCP → AgentMd → Done
92
+ ```
93
+
94
+ - **Welcome** — a single boxed card (~5 lines) introducing
95
+ BrainRouter, what gets configured, and where (path of
96
+ `~/.config/brainrouter/config.json`). ENTER continues, q quits
97
+ without writing.
98
+ - **Theme** — arrow-key picker over `dark / light / mono / auto`,
99
+ with **live preview**: on cursor-change, redraw the banner accent so
100
+ the user sees the change before confirming. Persists to
101
+ `preferences.theme` on confirm.
102
+ - **Provider** — picker over a curated provider list:
103
+ `OpenAI · DeepSeek · OpenRouter · Anthropic (via gateway) · Gemini
104
+ (OpenAI-compat) · LM Studio (local) · Ollama (local) · Custom…`.
105
+ Each row shows a one-line hint (endpoint, "(local)" / "(cloud)",
106
+ needs-key indicator). Pre-detects from env vars
107
+ (`OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, etc.)
108
+ — pre-selects the row whose key is already present in the shell.
109
+ - **ApiKey** — free-text entry (re-uses the picker's `awaitingOther`
110
+ free-text mode). Pre-filled from env when available. Validation
111
+ tier `Accept{warning?}` / `Reject(reason)`: empty for local
112
+ endpoints is OK; non-empty with `sk-`-shape prefix or vendor-known
113
+ shape is OK; everything else is **accepted with a warning** (not
114
+ rejected — vendors invent new prefixes all the time). Mask to last
115
+ 4 chars after entry.
116
+ - **Model** — picker over the provider's curated short-list +
117
+ "Other" for free-text. Defaults: OpenAI → `gpt-4o-mini`, DeepSeek
118
+ → `deepseek-chat`, OpenRouter → `anthropic/claude-sonnet-4`,
119
+ LM Studio → blank (user picks from loaded models), etc.
120
+ - **MCP** — picker over `Local stdio (brainrouter-mcp on PATH) ·
121
+ Local HTTP (http://localhost:3747/mcp) · Remote HTTP… (free-text
122
+ URL) · Skip (offline-only)`. On confirm, run a single
123
+ `mcpClient.listTools()` reachability test (5s timeout); on success
124
+ save the profile and mark it active; on failure offer "save
125
+ anyway" / "try a different transport" / "skip".
126
+ - **AgentMd** — yes/no: "Write AGENT.md to this workspace? (helps
127
+ agents understand your repo conventions)". Defaults to **yes** if
128
+ no `AGENT.md` / `CLAUDE.md` exists yet, **no** otherwise. This is
129
+ the toggle that absorbs the old `/init` behaviour.
130
+ - **Done** — summary card showing the saved config (with API keys
131
+ masked) + next steps (`/help`, `/config`, `/where`). Marker file
132
+ written to `~/.config/brainrouter/.onboarded` so subsequent CLI
133
+ starts skip the wizard. Re-running `/init` is always allowed.
134
+
135
+ **Skip semantics.** At any step `Esc` backs out one state; `q`
136
+ aborts the whole wizard with **no changes saved** (everything is
137
+ journalled in-memory until the Done step commits the write).
138
+
139
+ **Auto-trigger.** When the user runs `brainrouter` (the chat command)
140
+ and no `~/.config/brainrouter/config.json` exists, instead of the
141
+ current "Run \`brainrouter login\` …" error-and-exit, we drop them
142
+ **straight into the wizard inside the REPL** (no separate subcommand).
143
+ This is the grok-cli `ApiKeyModal` pattern — the modal IS the
144
+ onboarding.
145
+
146
+ ## Settings home panel (`/config`)
147
+
148
+ Picker over the settings categories. Each row shows the current value
149
+ on the right so the user can scan the state at a glance:
150
+
151
+ ```
152
+ ┌─ ⚙️ BrainRouter Config ────────────────────────────────────────┐
153
+ │ │
154
+ │ ▶ LLM provider openai · gpt-4o-mini │
155
+ │ MCP profile default · http · 🟢 online │
156
+ │ Theme dark │
157
+ │ Statusline mode,branch,workflow,goal │
158
+ │ Reasoning effort medium │
159
+ │ Execution mode planning │
160
+ │ Review policy request │
161
+ │ Quiet mode off │
162
+ │ Personality standard │
163
+ │ Editor mode emacs │
164
+ │ │
165
+ │ View raw config │
166
+ │ Quit (Esc) │
167
+ │ │
168
+ │ ↑/↓ navigate · ENTER edit · Esc to quit │
169
+ └─────────────────────────────────────────────────────────────────┘
170
+ ```
171
+
172
+ Selecting a row opens the appropriate sub-picker (provider picker,
173
+ theme picker, etc.). On confirm, returns to the home panel with the
174
+ new value visible. Esc backs out one level (sub-picker → home → exit).
175
+
176
+ The "View raw config" row prints the scrubbed JSON (today's bare
177
+ `/config` behaviour) so users who prefer the JSON view can still get
178
+ it without leaving the panel.
179
+
180
+ ## Persistence contract
181
+
182
+ One central writer per file. No scattered `fs.writeFileSync` calls.
183
+
184
+ | File | Wrapper | Owns |
185
+ | --- | --- | --- |
186
+ | `~/.config/brainrouter/config.json` | `saveConfig(partial)` in [`config/config.ts`](../../brainrouter-cli/src/config/config.ts) — existing function, called with the merged result. | LLM provider, MCP profiles, active profile. |
187
+ | `<workspace>/.brainrouter/cli/preferences.json` | `writePreferences(workspaceRoot, partial)` in [`state/preferencesStore.ts`](../../brainrouter-cli/src/state/preferencesStore.ts) — existing function; already merges. | Theme, statusline, effort, exec mode, review policy, quiet, personality, editor, sandbox grants. |
188
+ | `~/.config/brainrouter/.onboarded` | New `markOnboarded()` helper alongside `config/config.ts`. | Empty file; presence = wizard has run at least once. |
189
+
190
+ The wizard accumulates intent in an in-memory `Draft` object across
191
+ all steps; the commit happens **once** at the Done step. Aborting via
192
+ `q` discards the draft.
193
+
194
+ ## Picker primitive — additions to `cliPrompt.ts`
195
+
196
+ Today's `askChoice` is sufficient for confirm-only pickers; the
197
+ redesign needs two new behaviours we can fold in as optional fields:
198
+
199
+ 1. **`onCursorChange(index)` callback** — fired by `reducePicker` after
200
+ an `up`/`down` keystroke if the option list moved. Theme picker
201
+ uses it to repaint the banner accent live.
202
+ 2. **`prefilledOther?: string`** — when the wizard wants to drop the
203
+ user straight into the `awaitingOther` free-text input pre-filled
204
+ with the env-var value (so ENTER accepts the env-derived default,
205
+ editing overrides it).
206
+
207
+ Both are additive; existing callers keep working with `undefined`.
208
+
209
+ ## Boundary rules (per `spec-driven-skill`)
210
+
211
+ - **Always:** route all settings persistence through
212
+ `saveConfig` / `writePreferences`; mask API keys to last 4 chars on
213
+ every render; pause the parent `rl` while a picker is active; close
214
+ with the cursor visible.
215
+ - **Ask first:** changing `~/.config/brainrouter/config.json` shape
216
+ (any new top-level key) — needs explicit user sign-off because it
217
+ surfaces in `/debug-config`.
218
+ - **Never:** write API keys to disk in plain text under the workspace
219
+ (only under `~/.config/brainrouter/`); silently overwrite an
220
+ existing config without confirmation; default to option 1 in
221
+ non-TTY mode (mirrors the existing `NoTTYError` contract).
222
+
223
+ ## Definition of Done
224
+
225
+ 1. `brainrouter` started against a clean `$HOME` (no `~/.config/brainrouter/`)
226
+ drops into the wizard automatically — no `brainrouter login`
227
+ needed.
228
+ 2. `/init` re-runs the wizard at any time inside the REPL.
229
+ 3. `/config` (bare) opens the settings home panel; arrow-keys
230
+ navigate; ENTER opens a sub-picker; Esc backs out.
231
+ 4. `/config theme dark` sets theme to dark and prints the new value
232
+ without opening any picker.
233
+ 5. Theme picker live-previews on cursor-change and restores the
234
+ original theme if the user presses Esc.
235
+ 6. Tests: pure-function tests for the wizard reducer (`stepReducer`,
236
+ `Draft` shape, validation tier), `Vitest` tests for the
237
+ `/config` argument parser (bare / get / set), and one
238
+ `cliPrompt.ts` test for the new `onCursorChange` callback.
239
+ 7. Docs: `brainrouter-docs/cli.md` and `brainrouter-docs/configuration.md`
240
+ are updated end-to-end. README's "First-time setup" section
241
+ collapses to "run `brainrouter`; the wizard takes over."
242
+ 8. No regressions: `npm run test --workspace brainrouter-cli` stays
243
+ green; the legacy `brainrouter login` / `brainrouter config`
244
+ subcommands still work for users who scripted around them.
245
+
246
+ ## Open questions
247
+
248
+ - **Should `/init` skip the wizard if the marker file exists**, or
249
+ always re-run? Current answer: always re-run when invoked
250
+ explicitly; only **auto-trigger on REPL start** when the marker is
251
+ missing. This matches Codex's "explicit `--login` always honoured"
252
+ contract.
253
+ - **Should the MCP step probe both stdio and HTTP**, or just the
254
+ user's pick? Current answer: just the pick. Probing both adds 5s
255
+ to the wizard for unclear payoff. Users can re-run `/init` if they
256
+ want to try the other transport.
257
+ - **Should the wizard offer to install missing system deps** (gh,
258
+ jq, ripgrep)? Current answer: no. Out of scope for v0.3.7. A
259
+ future `/doctor --fix` could handle it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinqs/brainrouter-mcp-server",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "BrainRouter MCP server — the cognitive memory engine. Exposes recall, capture, focus scenes, persona, contradictions, skills, and graph queries as MCP tools for any MCP-speaking agent.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,14 +38,14 @@
38
38
  "setup:admin": "node scripts/setup-admin.js"
39
39
  },
40
40
  "dependencies": {
41
+ "@kinqs/brainrouter-types": "^0.3.8",
41
42
  "@modelcontextprotocol/sdk": "^1.11.0",
42
43
  "dotenv": "^16.4.5",
43
44
  "express": "^5.2.1",
44
45
  "glob": "^11.0.0",
45
46
  "gray-matter": "^4.0.3",
46
47
  "sqlite-vec": "^0.1.9",
47
- "zod": "^3.22.4",
48
- "@kinqs/brainrouter-types": "^0.3.6"
48
+ "zod": "^3.22.4"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=22.0.0"