@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,180 @@
1
+ /**
2
+ * Tool call display formatters — claude-code-style semantic rendering.
3
+ *
4
+ * The raw `tool_name({"path": "...", ...})` JSON dump that fell out of
5
+ * `agent.runTurn`'s onToolStart callback is hostile to quick scanning
6
+ * during a long turn. claude-code's transcript format is
7
+ *
8
+ * ⏺ Read(src/auth/login.ts)
9
+ * ⏺ Bash(npm test)
10
+ * ⏺ Grep("authenticate")
11
+ *
12
+ * — one-line, identity-revealing, no JSON. These helpers do the same
13
+ * mapping for our built-in LOCAL_TOOLS (cli/../agent/agent.ts) + MCP
14
+ * tool names (which carry an `mcp__<server>__` namespace prefix that
15
+ * the user doesn't care about).
16
+ *
17
+ * Reference for the convention: claude-code transcripts (see
18
+ * openSrc/claude-code/CHANGELOG.md mentions throughout; the format is
19
+ * not formally documented but used in every claude-code session).
20
+ */
21
+ /**
22
+ * Format a tool call as a one-line `Function(args)` summary.
23
+ *
24
+ * Examples:
25
+ * formatToolCall('read_file', { path: 'src/foo.ts' })
26
+ * → "Read(src/foo.ts)"
27
+ * formatToolCall('run_command', { command: 'npm test' })
28
+ * → "Bash(npm test)"
29
+ * formatToolCall('grep_search', { query: 'authenticate', path: '.' })
30
+ * → 'Grep("authenticate")'
31
+ * formatToolCall('mcp__brainrouter__memory_search', { q: 'auth' })
32
+ * → 'MemorySearch("auth")'
33
+ * formatToolCall('spawn_agent', { role: 'researcher', prompt: '...' })
34
+ * → 'Spawn(researcher, "...")'
35
+ */
36
+ export function formatToolCall(name, args) {
37
+ const safeArgs = args ?? {};
38
+ const clean = stripMcpPrefix(name);
39
+ switch (clean) {
40
+ case 'read_file': {
41
+ const path = safeArgs.path ?? '.';
42
+ const startLine = safeArgs.startLine;
43
+ const endLine = safeArgs.endLine;
44
+ if (startLine && endLine)
45
+ return `Read(${path}:${startLine}-${endLine})`;
46
+ if (startLine)
47
+ return `Read(${path}:${startLine})`;
48
+ return `Read(${path})`;
49
+ }
50
+ case 'write_file':
51
+ return `Write(${safeArgs.path ?? ''})`;
52
+ case 'edit_file':
53
+ return `Edit(${safeArgs.path ?? ''})`;
54
+ case 'list_dir':
55
+ return `LS(${safeArgs.path ?? '.'})`;
56
+ case 'grep_search':
57
+ return `Grep(${quoteShort(safeArgs.query, 50)})`;
58
+ case 'glob_files':
59
+ return `Glob(${quoteShort(safeArgs.pattern, 50)})`;
60
+ case 'run_command':
61
+ return `Bash(${truncateOneLine(safeArgs.command ?? '', 70)})`;
62
+ case 'fetch_url':
63
+ return `Fetch(${truncateOneLine(safeArgs.url ?? '', 70)})`;
64
+ case 'web_search':
65
+ return `WebSearch(${quoteShort(safeArgs.query, 50)})`;
66
+ case 'spawn_agent': {
67
+ const role = String(safeArgs.role ?? 'agent');
68
+ const label = safeArgs.label ? ` [${safeArgs.label}]` : '';
69
+ const task = truncateOneLine(safeArgs.prompt ?? '', 50);
70
+ return `Spawn(${role}${label}, "${task}")`;
71
+ }
72
+ case 'spawn_agents': {
73
+ const agents = Array.isArray(safeArgs.agents) ? safeArgs.agents : [];
74
+ const roles = agents.map((a) => String(a?.role ?? 'agent')).join(', ');
75
+ return `SpawnAll(${agents.length}: ${roles})`;
76
+ }
77
+ case 'update_plan':
78
+ return `UpdatePlan()`;
79
+ case 'ask_user_choice':
80
+ return `AskUser(${quoteShort(safeArgs.question, 50)})`;
81
+ }
82
+ // Generic fallback: PascalCase the name and surface the first string-shaped
83
+ // argument as the identifying value. Better than JSON-dumping everything.
84
+ const pretty = snakeToPascal(clean);
85
+ const firstString = Object.values(safeArgs).find((v) => typeof v === 'string' && v.length > 0);
86
+ if (firstString !== undefined) {
87
+ return `${pretty}(${truncateOneLine(firstString, 60)})`;
88
+ }
89
+ // No args, or no string args — just show the name.
90
+ return `${pretty}()`;
91
+ }
92
+ /**
93
+ * Strip the `mcp__<server>__` or `mcp_<server>_` namespace prefix from MCP tool
94
+ * names. Server ids may contain underscores (e.g. `my_server`), so the
95
+ * double-underscore form uses a lazy match. Both prefix conventions are in use
96
+ * across the multi-MCP codepaths until naming is unified.
97
+ * `mcp__brainrouter__memory_search` → `memory_search`
98
+ * `mcp__my_server__memory_search` → `memory_search`
99
+ * `mcp_brainrouter_memory_search` → `memory_search`
100
+ */
101
+ export function stripMcpPrefix(name) {
102
+ const dbl = name.match(/^mcp__.+?__(.+)$/);
103
+ if (dbl)
104
+ return dbl[1];
105
+ const sgl = name.match(/^mcp_[^_]+_(.+)$/);
106
+ if (sgl)
107
+ return sgl[1];
108
+ return name;
109
+ }
110
+ /**
111
+ * Convert snake_case to PascalCase for readable display names.
112
+ * `memory_search` → `MemorySearch`.
113
+ */
114
+ export function snakeToPascal(name) {
115
+ return name
116
+ .split('_')
117
+ .filter((p) => p.length > 0)
118
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
119
+ .join('');
120
+ }
121
+ /**
122
+ * Quote + truncate a free-form string argument to fit on one line.
123
+ * Returns `""` for missing input — the empty-quote pair signals "empty
124
+ * arg" rather than "no arg" (a no-arg call would not call this at all).
125
+ */
126
+ export function quoteShort(s, max) {
127
+ if (typeof s !== 'string' || s.length === 0)
128
+ return '""';
129
+ const oneLine = s.replace(/\s+/g, ' ').trim();
130
+ const truncated = oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
131
+ return `"${truncated}"`;
132
+ }
133
+ /** Collapse whitespace + truncate a string to one line bounded by `max`. */
134
+ export function truncateOneLine(s, max) {
135
+ if (typeof s !== 'string')
136
+ return '';
137
+ const oneLine = s.replace(/\s+/g, ' ').trim();
138
+ return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine;
139
+ }
140
+ /**
141
+ * Classify a tool-result preview line as part of a unified diff so the
142
+ * renderer can color it (red for removals, green for additions, gray
143
+ * for context). Detects file headers (`+++`, `---`), hunk headers
144
+ * (`@@`), and the per-line +/- gutter sign. Returns undefined when the
145
+ * line doesn't look like diff content so callers can leave it alone.
146
+ */
147
+ export function classifyDiffLine(line) {
148
+ if (line.startsWith('+++ ') || line.startsWith('--- '))
149
+ return 'header';
150
+ if (line.startsWith('@@'))
151
+ return 'hunk';
152
+ if (line.startsWith('+'))
153
+ return 'add';
154
+ if (line.startsWith('-'))
155
+ return 'del';
156
+ return undefined;
157
+ }
158
+ /**
159
+ * True when a multi-line preview looks like a unified diff (at least one
160
+ * `@@` hunk header OR multiple +/- gutter lines). Used by the tool-result
161
+ * renderer to decide whether to apply diff coloring to the whole block.
162
+ */
163
+ export function looksLikeDiff(preview) {
164
+ if (!preview)
165
+ return false;
166
+ const lines = preview.split('\n');
167
+ let hunk = false;
168
+ let gutter = 0;
169
+ for (const line of lines) {
170
+ if (line.startsWith('@@')) {
171
+ hunk = true;
172
+ break;
173
+ }
174
+ if (line.startsWith('+') || line.startsWith('-'))
175
+ gutter++;
176
+ if (gutter >= 2)
177
+ break;
178
+ }
179
+ return hunk || gutter >= 2;
180
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Live terminal dimensions hook — re-renders the component whenever
3
+ * the user resizes the terminal window.
4
+ *
5
+ * Why this exists:
6
+ *
7
+ * Ink's `useStdout()` returns the stdout stream, and `stdout.columns`
8
+ * IS a live getter that always returns the current width. Ink also
9
+ * subscribes to `stdout.on('resize')` internally and triggers a
10
+ * re-render. In theory, reading `stdout?.columns` inside a render
11
+ * function automatically picks up the new width on the next resize.
12
+ *
13
+ * In practice, several dynamic parts of the chat REPL — the composer
14
+ * divider, the slash palette description column, the footer hints —
15
+ * were computed from a single inline `cols` const at the top of
16
+ * render. When the user dragged the terminal narrower or wider:
17
+ *
18
+ * - The divider stayed at its old length (overflowing or short).
19
+ * - The slash palette's description budget didn't update.
20
+ * - The footer right-side hint didn't collapse on narrow widths.
21
+ *
22
+ * Cause: Ink does re-render on resize, but children that received
23
+ * `cols` as a stable prop weren't being re-invoked with the new value
24
+ * in all of our cases. Explicitly subscribing to `resize` and using a
25
+ * React state update guarantees a re-render with the new dimensions
26
+ * regardless of Ink's internal heuristics.
27
+ *
28
+ * Returns `{ columns, rows }` — both auto-update on every resize event.
29
+ * Defaults to 80 × 24 when stdout is unavailable (non-TTY tests).
30
+ */
31
+ export interface TerminalSize {
32
+ columns: number;
33
+ rows: number;
34
+ }
35
+ export declare function useTerminalSize(): TerminalSize;
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useStdout } from 'ink';
3
+ const DEFAULT_COLUMNS = 80;
4
+ const DEFAULT_ROWS = 24;
5
+ export function useTerminalSize() {
6
+ const { stdout } = useStdout();
7
+ const [size, setSize] = useState(() => ({
8
+ columns: stdout?.columns ?? DEFAULT_COLUMNS,
9
+ rows: stdout?.rows ?? DEFAULT_ROWS,
10
+ }));
11
+ useEffect(() => {
12
+ if (!stdout)
13
+ return;
14
+ const onResize = () => {
15
+ setSize({
16
+ columns: stdout.columns ?? DEFAULT_COLUMNS,
17
+ rows: stdout.rows ?? DEFAULT_ROWS,
18
+ });
19
+ };
20
+ stdout.on('resize', onResize);
21
+ return () => {
22
+ stdout.off('resize', onResize);
23
+ };
24
+ }, [stdout]);
25
+ return size;
26
+ }
@@ -1,9 +1,31 @@
1
+ import readline from 'node:readline';
1
2
  import type { Agent } from '../agent/agent.js';
2
- import type { McpClientWrapper } from '../runtime/mcpClient.js';
3
+ import type { McpClientPool as McpClientWrapper } from '../runtime/mcpPool.js';
3
4
  import type { Config } from '../config/config.js';
4
- import type { WorkspaceInfo } from '../config/workspace.js';
5
- export declare function startREPL(agent: Agent, mcpClient: McpClientWrapper, config: Config, workspace?: WorkspaceInfo): void;
5
+ import type { ReplContext } from './commands/_context.js';
6
+ /**
7
+ * All slash commands the REPL recognizes. Used for tab autocomplete and for
8
+ * the readline completer. Keep alphabetically grouped roughly by surface area.
9
+ *
10
+ * The Ink chat REPL (cli/ink/runChat.tsx) consumes this same list for its
11
+ * inline slash palette so both surfaces stay in lockstep as new commands land.
12
+ */
13
+ export declare const SLASH_COMMANDS: readonly ["/help", "/status", "/workspace", "/where", "/tools", "/skills", "/plan", "/transcript", "/doctor", "/config", "/diff", "/commit", "/clear", "/compact", "/exit", "/quit", "/roles", "/agents", "/agent", "/spawn", "/wait", "/spec", "/feature-dev", "/grill-me", "/review", "/implement-plan", "/skill", "/workflow", "/workflows", "/approve", "/memory", "/recall", "/briefing", "/scenes", "/working", "/forget", "/init", "/login", "/sessions", "/resume", "/model", "/mcp", "/goal", "/copy", "/fork", "/rename", "/permissions", "/hooks", "/hookify", "/loop", "/continue", "/auto-review", "/vim", "/statusline", "/quiet", "/handover", "/explain", "/trace", "/failed", "/verify", "/audit", "/export", "/import", "/persona", "/skill-hints", "/diagnostics", "/tokens", "/watch", "/yolo", "/mode", "/review-policy", "/sandbox", "/kill", "/theme", "/title", "/personality", "/effort", "/new", "/side", "/btw", "/raw", "/feedback", "/rollout", "/ps", "/stop", "/logout", "/apps", "/plugins", "/experimental", "/memories", "/debug-config", "/mention", "/keymap", "/ide"];
6
14
  export declare function renderHelp(category?: string): void;
15
+ /**
16
+ * Look up a one-line description for a slash command by walking the
17
+ * help registry. Used to populate the slash-suggest popup. Falls back
18
+ * to a generic placeholder if the command isn't documented (those
19
+ * commands still work — they just won't get a custom description
20
+ * inside the popup until someone adds them to `HELP_CATEGORIES`).
21
+ *
22
+ * Description text in `HELP_CATEGORIES` sometimes carries a parenthesised
23
+ * argument hint (e.g. "/config [key] [value]"); we strip everything
24
+ * after the first space when matching by cmd token so e.g. `/config`
25
+ * matches `cmd: "/config [key] [value]"`.
26
+ */
27
+ export declare function lookupSlashDescription(cmd: string): string;
28
+ export declare function handleSlashCommand(command: string, args: string[], agent: Agent, mcpClient: McpClientWrapper, config: Config, rl: readline.Interface, ctx: ReplContext): Promise<void>;
7
29
  /**
8
30
  * Tab-completion source for `@path/to/file` mentions. Given a partial workspace
9
31
  * path, return the matching files and directories one level deep. Stays inside