@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.
- package/README.md +29 -52
- package/agents/architect.json +18 -0
- package/agents/explorer.json +18 -0
- package/agents/reviewer.json +18 -0
- package/agents/verifier.json +18 -0
- package/agents/worker.json +18 -0
- package/bin/cli.cjs +71 -0
- package/dist/agent/agent.d.ts +224 -3
- package/dist/agent/agent.js +561 -55
- package/dist/cli/banner.d.ts +80 -0
- package/dist/cli/banner.js +232 -0
- package/dist/cli/cliPrompt.d.ts +106 -0
- package/dist/cli/cliPrompt.js +314 -0
- package/dist/cli/commands/_context.d.ts +3 -1
- package/dist/cli/commands/_helpers.d.ts +1 -1
- package/dist/cli/commands/_helpers.js +6 -6
- package/dist/cli/commands/config.d.ts +46 -0
- package/dist/cli/commands/config.js +1042 -0
- package/dist/cli/commands/guard.js +75 -10
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.js +64 -0
- package/dist/cli/commands/login.d.ts +13 -0
- package/dist/cli/commands/login.js +179 -0
- package/dist/cli/commands/mcp.d.ts +19 -0
- package/dist/cli/commands/mcp.js +286 -0
- package/dist/cli/commands/memory.js +2 -2
- package/dist/cli/commands/obs.js +22 -22
- package/dist/cli/commands/orchestration.js +18 -0
- package/dist/cli/commands/session.js +13 -5
- package/dist/cli/commands/ui.js +202 -91
- package/dist/cli/commands/workflow.d.ts +20 -0
- package/dist/cli/commands/workflow.js +368 -51
- package/dist/cli/ink/ChatApp.d.ts +206 -0
- package/dist/cli/ink/ChatApp.js +493 -0
- package/dist/cli/ink/Frame.d.ts +26 -0
- package/dist/cli/ink/Frame.js +5 -0
- package/dist/cli/ink/Picker.d.ts +65 -0
- package/dist/cli/ink/Picker.js +133 -0
- package/dist/cli/ink/SlashPalette.d.ts +51 -0
- package/dist/cli/ink/SlashPalette.js +136 -0
- package/dist/cli/ink/TextField.d.ts +34 -0
- package/dist/cli/ink/TextField.js +47 -0
- package/dist/cli/ink/WizardApp.d.ts +7 -0
- package/dist/cli/ink/WizardApp.js +422 -0
- package/dist/cli/ink/ambientChat.d.ts +34 -0
- package/dist/cli/ink/ambientChat.js +7 -0
- package/dist/cli/ink/consoleCapture.d.ts +11 -0
- package/dist/cli/ink/consoleCapture.js +33 -0
- package/dist/cli/ink/markdownRender.d.ts +41 -0
- package/dist/cli/ink/markdownRender.js +278 -0
- package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
- package/dist/cli/ink/renderWithResizeClear.js +33 -0
- package/dist/cli/ink/runChat.d.ts +34 -0
- package/dist/cli/ink/runChat.js +571 -0
- package/dist/cli/ink/runPicker.d.ts +31 -0
- package/dist/cli/ink/runPicker.js +139 -0
- package/dist/cli/ink/runSlashPalette.d.ts +23 -0
- package/dist/cli/ink/runSlashPalette.js +33 -0
- package/dist/cli/ink/runWizard.d.ts +22 -0
- package/dist/cli/ink/runWizard.js +133 -0
- package/dist/cli/ink/stdinHandoff.d.ts +51 -0
- package/dist/cli/ink/stdinHandoff.js +78 -0
- package/dist/cli/ink/toolFormat.d.ts +73 -0
- package/dist/cli/ink/toolFormat.js +180 -0
- package/dist/cli/ink/useTerminalSize.d.ts +35 -0
- package/dist/cli/ink/useTerminalSize.js +26 -0
- package/dist/cli/repl.d.ts +25 -3
- package/dist/cli/repl.js +64 -646
- package/dist/cli/slashSuggest.d.ts +32 -0
- package/dist/cli/slashSuggest.js +146 -0
- package/dist/cli/spinner.d.ts +34 -0
- package/dist/cli/spinner.js +36 -0
- package/dist/cli/statusline.d.ts +67 -0
- package/dist/cli/statusline.js +204 -0
- package/dist/cli/theme.d.ts +79 -0
- package/dist/cli/theme.js +106 -0
- package/dist/cli/whereView.d.ts +81 -0
- package/dist/cli/whereView.js +245 -0
- package/dist/cli/wizard/modelsApi.d.ts +72 -0
- package/dist/cli/wizard/modelsApi.js +166 -0
- package/dist/cli/wizard/picker.d.ts +202 -0
- package/dist/cli/wizard/picker.js +547 -0
- package/dist/cli/wizard/providers.d.ts +86 -0
- package/dist/cli/wizard/providers.js +190 -0
- package/dist/cli/wizard/runner.d.ts +13 -0
- package/dist/cli/wizard/runner.js +488 -0
- package/dist/cli/wizard/types.d.ts +122 -0
- package/dist/cli/wizard/types.js +109 -0
- package/dist/config/config.d.ts +52 -0
- package/dist/config/config.js +89 -75
- package/dist/index.js +215 -206
- package/dist/memory/briefing.d.ts +11 -1
- package/dist/memory/briefing.js +69 -1
- package/dist/memory/consolidation.d.ts +1 -1
- package/dist/orchestration/agentRegistry.d.ts +36 -0
- package/dist/orchestration/agentRegistry.js +64 -0
- package/dist/orchestration/orchestrator.d.ts +7 -0
- package/dist/orchestration/orchestrator.js +2 -0
- package/dist/orchestration/tools.d.ts +10 -1
- package/dist/orchestration/tools.js +48 -4
- package/dist/prompt/breadthHint.d.ts +5 -0
- package/dist/prompt/breadthHint.js +44 -0
- package/dist/prompt/skillCatalog.d.ts +11 -0
- package/dist/prompt/skillCatalog.js +134 -0
- package/dist/prompt/skillRunner.d.ts +2 -2
- package/dist/prompt/skillRunner.js +2 -31
- package/dist/prompt/systemPrompt.d.ts +34 -0
- package/dist/prompt/systemPrompt.js +128 -108
- package/dist/runtime/dangerousCommand.d.ts +53 -0
- package/dist/runtime/dangerousCommand.js +105 -0
- package/dist/runtime/mcpClient.d.ts +38 -1
- package/dist/runtime/mcpClient.js +104 -13
- package/dist/runtime/mcpPool.d.ts +162 -0
- package/dist/runtime/mcpPool.js +423 -0
- package/dist/runtime/mcpUtils.d.ts +3 -1
- package/dist/state/goalStore.d.ts +98 -17
- package/dist/state/goalStore.js +132 -42
- package/dist/state/preferencesStore.d.ts +67 -3
- package/dist/state/preferencesStore.js +84 -1
- package/dist/state/workflowArtifacts.d.ts +63 -2
- package/dist/state/workflowArtifacts.js +120 -8
- package/dist/tests/_helpers.d.ts +31 -0
- package/dist/tests/_helpers.js +91 -0
- package/package.json +12 -5
- package/.env.example +0 -109
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Picker } from './Picker.js';
|
|
3
|
+
import { TextField } from './TextField.js';
|
|
4
|
+
import { NoTTYError } from '../cliPrompt.js';
|
|
5
|
+
import { resetStdinForReadline, snapshotStdinListeners } from './stdinHandoff.js';
|
|
6
|
+
import { getAmbientChat } from './ambientChat.js';
|
|
7
|
+
import { renderWithResizeClear } from './renderWithResizeClear.js';
|
|
8
|
+
/**
|
|
9
|
+
* One-shot Ink mount helpers. Used by `/config`, `/login`, and any
|
|
10
|
+
* slash command that needs a single picker / text prompt without
|
|
11
|
+
* managing Ink lifecycle by hand.
|
|
12
|
+
*
|
|
13
|
+
* Two paths:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Overlay path** — when the Ink chat REPL is running, the
|
|
16
|
+
* ambient ChatController is set (see ambientChat.ts +
|
|
17
|
+
* runChat.tsx). We render <Picker> inside the chat's overlay
|
|
18
|
+
* slot, NOT as a second Ink mount — that would race the chat
|
|
19
|
+
* for stdin + terminal state and break the picker's interaction.
|
|
20
|
+
*
|
|
21
|
+
* 2. **Standalone path** — for the legacy readline REPL, mount a
|
|
22
|
+
* fresh Ink instance. Unmount via `instance.unmount()` from
|
|
23
|
+
* outside the React tree (no `useApp().exit()`) so the wrapper
|
|
24
|
+
* doesn't risk exiting the wrong Ink instance if something goes
|
|
25
|
+
* sideways. The Picker/TextField components are exit-agnostic;
|
|
26
|
+
* they call onResolve and trust the caller to handle unmount.
|
|
27
|
+
*
|
|
28
|
+
* Stdin handoff for the standalone path: snapshot + detach existing
|
|
29
|
+
* listeners before mount, restore them and reset stdin state after
|
|
30
|
+
* Ink unmounts (matches the pattern in runWizard.tsx / runSlashPalette).
|
|
31
|
+
*/
|
|
32
|
+
// --- Picker -----------------------------------------------------------
|
|
33
|
+
export async function runPicker(opts) {
|
|
34
|
+
// OVERLAY PATH — running inside the Ink chat REPL.
|
|
35
|
+
const ambient = getAmbientChat();
|
|
36
|
+
if (ambient) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let resolved = false;
|
|
39
|
+
const overlayNode = (_jsx(Picker, { ...opts, onResolve: (r) => {
|
|
40
|
+
if (resolved)
|
|
41
|
+
return;
|
|
42
|
+
resolved = true;
|
|
43
|
+
ambient.clearOverlay();
|
|
44
|
+
resolve(r);
|
|
45
|
+
} }));
|
|
46
|
+
ambient.showOverlay(overlayNode).catch(() => {
|
|
47
|
+
if (!resolved) {
|
|
48
|
+
resolved = true;
|
|
49
|
+
resolve({ kind: 'cancelled' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// STANDALONE PATH — readline REPL fallback. Mounts a fresh Ink.
|
|
55
|
+
if (!process.stdin.isTTY) {
|
|
56
|
+
throw new NoTTYError('runPicker requires an interactive TTY.');
|
|
57
|
+
}
|
|
58
|
+
const snap = snapshotStdinListeners(['keypress', 'data']);
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
let captured;
|
|
61
|
+
// We need `instance` inside the onResolve closure but it doesn't
|
|
62
|
+
// exist yet when we build the JSX. Use a forward-declared variable
|
|
63
|
+
// that the closure captures by reference and we assign before the
|
|
64
|
+
// user can possibly interact with the picker. instance.unmount()
|
|
65
|
+
// is cleaner than useApp().exit() — works even if the Picker
|
|
66
|
+
// somehow ends up rendered in the wrong Ink tree.
|
|
67
|
+
let instance;
|
|
68
|
+
const node = (_jsx(Picker, { ...opts, onResolve: (r) => {
|
|
69
|
+
if (!captured)
|
|
70
|
+
captured = r;
|
|
71
|
+
if (instance)
|
|
72
|
+
instance.unmount();
|
|
73
|
+
} }));
|
|
74
|
+
const mounted = renderWithResizeClear(node, { exitOnCtrlC: true });
|
|
75
|
+
instance = mounted.instance;
|
|
76
|
+
instance.waitUntilExit().then(() => {
|
|
77
|
+
mounted.cleanupResizeClear();
|
|
78
|
+
snap.restore();
|
|
79
|
+
resetStdinForReadline();
|
|
80
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
81
|
+
}).catch(() => {
|
|
82
|
+
mounted.cleanupResizeClear();
|
|
83
|
+
snap.restore();
|
|
84
|
+
resetStdinForReadline();
|
|
85
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// --- TextField --------------------------------------------------------
|
|
90
|
+
export async function runTextField(opts) {
|
|
91
|
+
// OVERLAY PATH
|
|
92
|
+
const ambient = getAmbientChat();
|
|
93
|
+
if (ambient) {
|
|
94
|
+
return new Promise((resolve) => {
|
|
95
|
+
let resolved = false;
|
|
96
|
+
const overlayNode = (_jsx(TextField, { ...opts, onResolve: (r) => {
|
|
97
|
+
if (resolved)
|
|
98
|
+
return;
|
|
99
|
+
resolved = true;
|
|
100
|
+
ambient.clearOverlay();
|
|
101
|
+
resolve(r);
|
|
102
|
+
} }));
|
|
103
|
+
ambient.showOverlay(overlayNode).catch(() => {
|
|
104
|
+
if (!resolved) {
|
|
105
|
+
resolved = true;
|
|
106
|
+
resolve({ kind: 'cancelled' });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// STANDALONE PATH
|
|
112
|
+
if (!process.stdin.isTTY) {
|
|
113
|
+
throw new NoTTYError('runTextField requires an interactive TTY.');
|
|
114
|
+
}
|
|
115
|
+
const snap = snapshotStdinListeners(['keypress', 'data']);
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
let captured;
|
|
118
|
+
let instance;
|
|
119
|
+
const node = (_jsx(TextField, { ...opts, onResolve: (r) => {
|
|
120
|
+
if (!captured)
|
|
121
|
+
captured = r;
|
|
122
|
+
if (instance)
|
|
123
|
+
instance.unmount();
|
|
124
|
+
} }));
|
|
125
|
+
const mounted = renderWithResizeClear(node, { exitOnCtrlC: true });
|
|
126
|
+
instance = mounted.instance;
|
|
127
|
+
instance.waitUntilExit().then(() => {
|
|
128
|
+
mounted.cleanupResizeClear();
|
|
129
|
+
snap.restore();
|
|
130
|
+
resetStdinForReadline();
|
|
131
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
132
|
+
}).catch(() => {
|
|
133
|
+
mounted.cleanupResizeClear();
|
|
134
|
+
snap.restore();
|
|
135
|
+
resetStdinForReadline();
|
|
136
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SlashCommandDef, type SlashPaletteResult } from './SlashPalette.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mount the slash palette Ink app and await the user's selection.
|
|
4
|
+
*
|
|
5
|
+
* Returns:
|
|
6
|
+
* { kind: 'submit', text } — full command line to feed to the REPL
|
|
7
|
+
* { kind: 'cancelled' } — user pressed Esc / backspaced past `/`
|
|
8
|
+
*
|
|
9
|
+
* Stdin handoff (matters):
|
|
10
|
+
* - Before mount: snapshot + remove ALL `keypress` / `data`
|
|
11
|
+
* listeners on process.stdin. Otherwise readline (which is paused
|
|
12
|
+
* but still listening) AND Ink both consume bytes — arrow keys
|
|
13
|
+
* get split between them and the palette appears frozen.
|
|
14
|
+
* - After Ink unmount: restore the snapshotted listeners + run
|
|
15
|
+
* `resetStdinForReadline` so the surrounding REPL doesn't exit
|
|
16
|
+
* due to Ink's `stdin.unref()` cleanup.
|
|
17
|
+
*/
|
|
18
|
+
export interface RunSlashPaletteOptions {
|
|
19
|
+
initialQuery: string;
|
|
20
|
+
commands: SlashCommandDef[];
|
|
21
|
+
accentColor?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function runSlashPalette(opts: RunSlashPaletteOptions): Promise<SlashPaletteResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SlashPalette } from './SlashPalette.js';
|
|
3
|
+
import { resetStdinForReadline, snapshotStdinListeners } from './stdinHandoff.js';
|
|
4
|
+
import { renderWithResizeClear } from './renderWithResizeClear.js';
|
|
5
|
+
export async function runSlashPalette(opts) {
|
|
6
|
+
if (!process.stdin.isTTY) {
|
|
7
|
+
return { kind: 'cancelled' };
|
|
8
|
+
}
|
|
9
|
+
// Snapshot + detach existing stdin listeners so Ink owns stdin
|
|
10
|
+
// alone for its mount lifetime.
|
|
11
|
+
const snap = snapshotStdinListeners(['keypress', 'data']);
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let captured;
|
|
14
|
+
const { instance, cleanupResizeClear } = renderWithResizeClear(_jsx(SlashPalette, { initialQuery: opts.initialQuery, commands: opts.commands, accentColor: opts.accentColor, onResolve: (r) => {
|
|
15
|
+
// Capture; the actual `resolve()` runs after Ink's unmount
|
|
16
|
+
// finishes via `waitUntilExit().then`.
|
|
17
|
+
if (captured)
|
|
18
|
+
return;
|
|
19
|
+
captured = r;
|
|
20
|
+
} }), { exitOnCtrlC: false });
|
|
21
|
+
instance.waitUntilExit().then(() => {
|
|
22
|
+
cleanupResizeClear();
|
|
23
|
+
snap.restore();
|
|
24
|
+
resetStdinForReadline();
|
|
25
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
26
|
+
}).catch(() => {
|
|
27
|
+
cleanupResizeClear();
|
|
28
|
+
snap.restore();
|
|
29
|
+
resetStdinForReadline();
|
|
30
|
+
resolve(captured ?? { kind: 'cancelled' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WizardState } from '../wizard/types.js';
|
|
2
|
+
import { type Config } from '../../config/config.js';
|
|
3
|
+
export declare function isOnboarded(): boolean;
|
|
4
|
+
export declare function markOnboarded(): void;
|
|
5
|
+
export interface WizardRunOptions {
|
|
6
|
+
workspaceRoot: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WizardRunResult {
|
|
9
|
+
state: WizardState;
|
|
10
|
+
config?: Config;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Mount the Ink wizard and wait for it to finish. Returns the final
|
|
14
|
+
* `WizardState` (which includes `committed` / `aborted` flags) and,
|
|
15
|
+
* when committed, the freshly-saved Config.
|
|
16
|
+
*
|
|
17
|
+
* Why Ink instead of the previous raw-stdout runner? Ink owns the
|
|
18
|
+
* render loop and diffs the cell grid between frames, so we don't
|
|
19
|
+
* track cursor positions ourselves. Every redraw bug the previous
|
|
20
|
+
* approach had (creep, stacking, off-by-one) is eliminated by design.
|
|
21
|
+
*/
|
|
22
|
+
export declare function runWizard(opts: WizardRunOptions): Promise<WizardRunResult>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { WizardApp } from './WizardApp.js';
|
|
6
|
+
import { writePreferences } from '../../state/preferencesStore.js';
|
|
7
|
+
import { loadOrInitConfig, saveConfig } from '../../config/config.js';
|
|
8
|
+
import { initAgentMd } from '../../prompt/initAgentMd.js';
|
|
9
|
+
import { NoTTYError } from '../cliPrompt.js';
|
|
10
|
+
import { resetStdinForReadline } from './stdinHandoff.js';
|
|
11
|
+
import { renderWithResizeClear } from './renderWithResizeClear.js';
|
|
12
|
+
const ONBOARDED_MARKER = path.join(os.homedir(), '.config', 'brainrouter', '.onboarded');
|
|
13
|
+
export function isOnboarded() {
|
|
14
|
+
try {
|
|
15
|
+
return fs.existsSync(ONBOARDED_MARKER);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function markOnboarded() {
|
|
22
|
+
try {
|
|
23
|
+
fs.mkdirSync(path.dirname(ONBOARDED_MARKER), { recursive: true });
|
|
24
|
+
fs.writeFileSync(ONBOARDED_MARKER, '', 'utf8');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* non-fatal */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mount the Ink wizard and wait for it to finish. Returns the final
|
|
32
|
+
* `WizardState` (which includes `committed` / `aborted` flags) and,
|
|
33
|
+
* when committed, the freshly-saved Config.
|
|
34
|
+
*
|
|
35
|
+
* Why Ink instead of the previous raw-stdout runner? Ink owns the
|
|
36
|
+
* render loop and diffs the cell grid between frames, so we don't
|
|
37
|
+
* track cursor positions ourselves. Every redraw bug the previous
|
|
38
|
+
* approach had (creep, stacking, off-by-one) is eliminated by design.
|
|
39
|
+
*/
|
|
40
|
+
export async function runWizard(opts) {
|
|
41
|
+
if (!process.stdin.isTTY) {
|
|
42
|
+
throw new NoTTYError('BrainRouter has no config and stdin is not a TTY — run `brainrouter` in an interactive terminal at least once to complete the setup wizard.');
|
|
43
|
+
}
|
|
44
|
+
const finalState = await new Promise((resolve) => {
|
|
45
|
+
let captured;
|
|
46
|
+
const { instance, cleanupResizeClear } = renderWithResizeClear(_jsx(WizardApp, { workspaceRoot: opts.workspaceRoot, onFinish: (s) => {
|
|
47
|
+
// Capture but DON'T resolve yet — we want to wait for Ink's
|
|
48
|
+
// own unmount to complete (next tick) before handing stdin
|
|
49
|
+
// back to readline. Resolving prematurely would let our
|
|
50
|
+
// caller try to read stdin while Ink is still tearing down,
|
|
51
|
+
// which breaks the next readline.createInterface.
|
|
52
|
+
captured = s;
|
|
53
|
+
} }), {
|
|
54
|
+
// Don't put Ink's output into the alt-screen buffer — we want
|
|
55
|
+
// the final frame (Done summary) to stay in scrollback after
|
|
56
|
+
// unmount. Ink's default is `exitOnCtrlC: true` which is fine
|
|
57
|
+
// for our Ctrl+C abort path.
|
|
58
|
+
exitOnCtrlC: true,
|
|
59
|
+
});
|
|
60
|
+
instance.waitUntilExit().then(() => {
|
|
61
|
+
cleanupResizeClear();
|
|
62
|
+
// Hand stdin back to the caller in a state where readline (or
|
|
63
|
+
// anything else) can take it. Ink leaves stdin unref'd, in raw
|
|
64
|
+
// mode false, with its 'readable' listener removed; without
|
|
65
|
+
// this reset the post-wizard REPL would print its banner and
|
|
66
|
+
// then immediately exit because nothing kept the event loop
|
|
67
|
+
// alive. See cli/ink/stdinHandoff.ts for the full rationale.
|
|
68
|
+
resetStdinForReadline();
|
|
69
|
+
resolve(captured ?? { aborted: true, committed: false, currentStep: 'welcome', draft: {}, warnings: [] });
|
|
70
|
+
}).catch(() => {
|
|
71
|
+
cleanupResizeClear();
|
|
72
|
+
resetStdinForReadline();
|
|
73
|
+
resolve(captured ?? { aborted: true, committed: false, currentStep: 'welcome', draft: {}, warnings: [] });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
let savedConfig;
|
|
77
|
+
if (finalState.committed) {
|
|
78
|
+
savedConfig = commitWizardDraft(finalState.draft, opts.workspaceRoot);
|
|
79
|
+
markOnboarded();
|
|
80
|
+
}
|
|
81
|
+
return { state: finalState, config: savedConfig };
|
|
82
|
+
}
|
|
83
|
+
function commitWizardDraft(draft, workspaceRoot) {
|
|
84
|
+
const config = loadOrInitConfig();
|
|
85
|
+
if (draft.provider) {
|
|
86
|
+
config.llm = {
|
|
87
|
+
provider: 'openai',
|
|
88
|
+
apiKey: draft.apiKey ?? '',
|
|
89
|
+
model: draft.model ?? draft.provider.defaultModel,
|
|
90
|
+
endpoint: draft.customEndpoint ?? draft.provider.endpoint,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (draft.mcp && draft.mcp.kind !== 'skip') {
|
|
94
|
+
const profileName = draft.mcp.kind === 'remote-http' ? 'remote' : draft.mcp.kind === 'local-http' ? 'local-http' : 'local-stdio';
|
|
95
|
+
const serverConfig = mcpPickToServerConfig(draft.mcp);
|
|
96
|
+
if (serverConfig) {
|
|
97
|
+
config.servers[profileName] = serverConfig;
|
|
98
|
+
config.activeServer = profileName;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (draft.mcp?.kind === 'skip') {
|
|
102
|
+
// Skip means skip — clear any previously-active profile so the CLI doesn't
|
|
103
|
+
// silently re-spawn an MCP child from a stale config. The user can re-add
|
|
104
|
+
// a profile via `/login` later.
|
|
105
|
+
config.activeServer = '';
|
|
106
|
+
}
|
|
107
|
+
saveConfig(config);
|
|
108
|
+
if (draft.theme) {
|
|
109
|
+
try {
|
|
110
|
+
writePreferences(workspaceRoot, { theme: draft.theme });
|
|
111
|
+
}
|
|
112
|
+
catch { /* non-fatal */ }
|
|
113
|
+
}
|
|
114
|
+
if (draft.writeAgentMd) {
|
|
115
|
+
try {
|
|
116
|
+
initAgentMd(workspaceRoot);
|
|
117
|
+
}
|
|
118
|
+
catch { /* non-fatal */ }
|
|
119
|
+
}
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
function mcpPickToServerConfig(pick) {
|
|
123
|
+
if (pick.kind === 'local-stdio') {
|
|
124
|
+
return { type: 'stdio', command: 'brainrouter-mcp', args: [], identity: 'brainrouter' };
|
|
125
|
+
}
|
|
126
|
+
if (pick.kind === 'local-http') {
|
|
127
|
+
return { type: 'http', url: 'http://localhost:3747/mcp', apiKey: pick.apiKey, identity: 'brainrouter' };
|
|
128
|
+
}
|
|
129
|
+
if (pick.kind === 'remote-http') {
|
|
130
|
+
return { type: 'http', url: pick.url, apiKey: pick.apiKey, identity: 'brainrouter' };
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restore stdin to a state where readline (or any other consumer) can
|
|
3
|
+
* own it, AFTER an Ink app has unmounted.
|
|
4
|
+
*
|
|
5
|
+
* Why this is necessary:
|
|
6
|
+
* Ink's `App.js` calls `stdin.unref()` during its `disableRawMode`
|
|
7
|
+
* cleanup (node_modules/ink/build/components/App.js:137). `unref()`
|
|
8
|
+
* removes the stdin handle from the event-loop refcount. Once Ink
|
|
9
|
+
* hands stdin back, NOTHING in the event loop keeps Node alive —
|
|
10
|
+
* readline's 'readable' listener does NOT auto-ref the stream.
|
|
11
|
+
* Node sees zero refs, fires `beforeExit`, and exits cleanly with
|
|
12
|
+
* no `close` event.
|
|
13
|
+
*
|
|
14
|
+
* Symptom: the post-wizard REPL printed its banner ("Type /help…")
|
|
15
|
+
* and then the process exited to bash. No `Goodbye!` from
|
|
16
|
+
* readline's close handler — readline never got a chance to run.
|
|
17
|
+
*
|
|
18
|
+
* Fix order (matters):
|
|
19
|
+
* 1. `process.stdin.ref()` — counter Ink's unref so the event loop
|
|
20
|
+
* doesn't drain.
|
|
21
|
+
* 2. `process.stdin.resume()` — re-enable flowing/readable events.
|
|
22
|
+
* Ink may have paused via `stdin.read()` draining.
|
|
23
|
+
* 3. `setRawMode(true)` on TTY — readline expects raw mode for
|
|
24
|
+
* keypress events. Ink restored it to false on unmount.
|
|
25
|
+
* 4. `readline.emitKeypressEvents(process.stdin)` — re-arm the
|
|
26
|
+
* keypress decoder. Ink's `clearInputState` removed the
|
|
27
|
+
* 'readable' listener (App.js:126); we want our handler back.
|
|
28
|
+
*
|
|
29
|
+
* Call this on the next `setImmediate` after `instance.unmount()` /
|
|
30
|
+
* `waitUntilExit()` resolves — Ink writes its final unmount barrier
|
|
31
|
+
* on the next tick (Ink.js:549-554).
|
|
32
|
+
*/
|
|
33
|
+
export declare function resetStdinForReadline(): void;
|
|
34
|
+
/**
|
|
35
|
+
* Snapshot every listener attached to a stdin event so we can remove
|
|
36
|
+
* them while Ink owns stdin and re-attach them on Ink unmount.
|
|
37
|
+
*
|
|
38
|
+
* Without this, readline's keypress + 'data' listeners stay subscribed
|
|
39
|
+
* while Ink is mounted — both consumers fight for the same bytes,
|
|
40
|
+
* arrow keys go missing, the picker doesn't see Enter, etc. (See
|
|
41
|
+
* `cli-prompt.test.ts` notes on why we use `rl.pause()` AND remove
|
|
42
|
+
* stdin listeners during ask_user_choice.)
|
|
43
|
+
*
|
|
44
|
+
* Usage:
|
|
45
|
+
* const snap = snapshotStdinListeners(['keypress', 'data']);
|
|
46
|
+
* try { await runInk(...); } finally { snap.restore(); resetStdinForReadline(); }
|
|
47
|
+
*/
|
|
48
|
+
export interface StdinListenerSnapshot {
|
|
49
|
+
restore(): void;
|
|
50
|
+
}
|
|
51
|
+
export declare function snapshotStdinListeners(events?: readonly string[]): StdinListenerSnapshot;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
/**
|
|
3
|
+
* Restore stdin to a state where readline (or any other consumer) can
|
|
4
|
+
* own it, AFTER an Ink app has unmounted.
|
|
5
|
+
*
|
|
6
|
+
* Why this is necessary:
|
|
7
|
+
* Ink's `App.js` calls `stdin.unref()` during its `disableRawMode`
|
|
8
|
+
* cleanup (node_modules/ink/build/components/App.js:137). `unref()`
|
|
9
|
+
* removes the stdin handle from the event-loop refcount. Once Ink
|
|
10
|
+
* hands stdin back, NOTHING in the event loop keeps Node alive —
|
|
11
|
+
* readline's 'readable' listener does NOT auto-ref the stream.
|
|
12
|
+
* Node sees zero refs, fires `beforeExit`, and exits cleanly with
|
|
13
|
+
* no `close` event.
|
|
14
|
+
*
|
|
15
|
+
* Symptom: the post-wizard REPL printed its banner ("Type /help…")
|
|
16
|
+
* and then the process exited to bash. No `Goodbye!` from
|
|
17
|
+
* readline's close handler — readline never got a chance to run.
|
|
18
|
+
*
|
|
19
|
+
* Fix order (matters):
|
|
20
|
+
* 1. `process.stdin.ref()` — counter Ink's unref so the event loop
|
|
21
|
+
* doesn't drain.
|
|
22
|
+
* 2. `process.stdin.resume()` — re-enable flowing/readable events.
|
|
23
|
+
* Ink may have paused via `stdin.read()` draining.
|
|
24
|
+
* 3. `setRawMode(true)` on TTY — readline expects raw mode for
|
|
25
|
+
* keypress events. Ink restored it to false on unmount.
|
|
26
|
+
* 4. `readline.emitKeypressEvents(process.stdin)` — re-arm the
|
|
27
|
+
* keypress decoder. Ink's `clearInputState` removed the
|
|
28
|
+
* 'readable' listener (App.js:126); we want our handler back.
|
|
29
|
+
*
|
|
30
|
+
* Call this on the next `setImmediate` after `instance.unmount()` /
|
|
31
|
+
* `waitUntilExit()` resolves — Ink writes its final unmount barrier
|
|
32
|
+
* on the next tick (Ink.js:549-554).
|
|
33
|
+
*/
|
|
34
|
+
export function resetStdinForReadline() {
|
|
35
|
+
const stdin = process.stdin;
|
|
36
|
+
try {
|
|
37
|
+
stdin.ref?.();
|
|
38
|
+
}
|
|
39
|
+
catch { /* node version w/o ref */ }
|
|
40
|
+
try {
|
|
41
|
+
stdin.resume();
|
|
42
|
+
}
|
|
43
|
+
catch { /* already resumed */ }
|
|
44
|
+
if (stdin.isTTY) {
|
|
45
|
+
try {
|
|
46
|
+
stdin.setRawMode?.(true);
|
|
47
|
+
}
|
|
48
|
+
catch { /* not a real TTY */ }
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
readline.emitKeypressEvents(stdin);
|
|
52
|
+
}
|
|
53
|
+
catch { /* already wired */ }
|
|
54
|
+
}
|
|
55
|
+
export function snapshotStdinListeners(events = ['keypress', 'data']) {
|
|
56
|
+
const stdin = process.stdin;
|
|
57
|
+
const captured = [];
|
|
58
|
+
for (const event of events) {
|
|
59
|
+
const listeners = (stdin.listeners?.(event) ?? []);
|
|
60
|
+
for (const listener of listeners) {
|
|
61
|
+
captured.push({ event, listener });
|
|
62
|
+
try {
|
|
63
|
+
stdin.removeListener(event, listener);
|
|
64
|
+
}
|
|
65
|
+
catch { /* ignore */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
restore() {
|
|
70
|
+
for (const { event, listener } of captured) {
|
|
71
|
+
try {
|
|
72
|
+
stdin.on(event, listener);
|
|
73
|
+
}
|
|
74
|
+
catch { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 declare function formatToolCall(name: string, args: Record<string, any> | undefined): string;
|
|
37
|
+
/**
|
|
38
|
+
* Strip the `mcp__<server>__` or `mcp_<server>_` namespace prefix from MCP tool
|
|
39
|
+
* names. Server ids may contain underscores (e.g. `my_server`), so the
|
|
40
|
+
* double-underscore form uses a lazy match. Both prefix conventions are in use
|
|
41
|
+
* across the multi-MCP codepaths until naming is unified.
|
|
42
|
+
* `mcp__brainrouter__memory_search` → `memory_search`
|
|
43
|
+
* `mcp__my_server__memory_search` → `memory_search`
|
|
44
|
+
* `mcp_brainrouter_memory_search` → `memory_search`
|
|
45
|
+
*/
|
|
46
|
+
export declare function stripMcpPrefix(name: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Convert snake_case to PascalCase for readable display names.
|
|
49
|
+
* `memory_search` → `MemorySearch`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function snakeToPascal(name: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Quote + truncate a free-form string argument to fit on one line.
|
|
54
|
+
* Returns `""` for missing input — the empty-quote pair signals "empty
|
|
55
|
+
* arg" rather than "no arg" (a no-arg call would not call this at all).
|
|
56
|
+
*/
|
|
57
|
+
export declare function quoteShort(s: unknown, max: number): string;
|
|
58
|
+
/** Collapse whitespace + truncate a string to one line bounded by `max`. */
|
|
59
|
+
export declare function truncateOneLine(s: unknown, max: number): string;
|
|
60
|
+
/**
|
|
61
|
+
* Classify a tool-result preview line as part of a unified diff so the
|
|
62
|
+
* renderer can color it (red for removals, green for additions, gray
|
|
63
|
+
* for context). Detects file headers (`+++`, `---`), hunk headers
|
|
64
|
+
* (`@@`), and the per-line +/- gutter sign. Returns undefined when the
|
|
65
|
+
* line doesn't look like diff content so callers can leave it alone.
|
|
66
|
+
*/
|
|
67
|
+
export declare function classifyDiffLine(line: string): 'header' | 'hunk' | 'add' | 'del' | 'context' | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* True when a multi-line preview looks like a unified diff (at least one
|
|
70
|
+
* `@@` hunk header OR multiple +/- gutter lines). Used by the tool-result
|
|
71
|
+
* renderer to decide whether to apply diff coloring to the whole block.
|
|
72
|
+
*/
|
|
73
|
+
export declare function looksLikeDiff(preview: string): boolean;
|