@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
package/dist/cli/cliPrompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
1
2
|
/**
|
|
2
3
|
* Shared bridge between the REPL's readline interface and modules outside
|
|
3
4
|
* repl.ts that need to (a) write above the prompt without scrambling input,
|
|
@@ -19,6 +20,13 @@ export function setActiveReadline(rl) {
|
|
|
19
20
|
export function getActiveReadline() {
|
|
20
21
|
return activeReadline;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* True while `askChoice` is rendering its raw-mode picker. The REPL's own
|
|
25
|
+
* keypress handler (shift+tab access-mode cycle) checks this and yields,
|
|
26
|
+
* so the picker has uncontested control of stdin while it's active.
|
|
27
|
+
*/
|
|
28
|
+
let pickerActive = false;
|
|
29
|
+
export function isPickerActive() { return pickerActive; }
|
|
22
30
|
/**
|
|
23
31
|
* One-shot yes/no question. Returns true only when the user types y/yes
|
|
24
32
|
* (case-insensitive). Returns the supplied default when stdin isn't a TTY
|
|
@@ -41,6 +49,312 @@ export function askYesNo(question, defaultValue = false) {
|
|
|
41
49
|
});
|
|
42
50
|
});
|
|
43
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Surfaced when `askChoice` is called outside an interactive TTY. The tool
|
|
54
|
+
* wrapper turns this into a tool-call error so the LLM falls back to deciding
|
|
55
|
+
* itself — silently picking option 1 for the agent in CI / piped runs would
|
|
56
|
+
* make a load-bearing decision the user never saw.
|
|
57
|
+
*/
|
|
58
|
+
export class NoTTYError extends Error {
|
|
59
|
+
constructor(message) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.name = 'NoTTYError';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Surfaced when the user pressed Esc / q / Ctrl+C inside the picker.
|
|
66
|
+
* The tool wrapper converts this into a tool-call error so the LLM knows
|
|
67
|
+
* the user declined to commit and can re-plan instead of guessing.
|
|
68
|
+
*/
|
|
69
|
+
export class CancelledChoiceError extends Error {
|
|
70
|
+
constructor(message = 'ask_user_choice was cancelled by the user before they picked an option.') {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'CancelledChoiceError';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// --- Pure picker state machine -------------------------------------------
|
|
76
|
+
// Split out as exported pure functions so they're trivial to unit-test
|
|
77
|
+
// without faking a TTY or piping through keypress events. The orchestrator
|
|
78
|
+
// (`askChoice`) only owns the side-effecting bits: wiring stdin keypress
|
|
79
|
+
// events into the reducer and re-rendering the screen.
|
|
80
|
+
/** Synthetic always-on "Other" option appended to every picker. */
|
|
81
|
+
const OTHER_LABEL = 'Other';
|
|
82
|
+
const OTHER_DESCRIPTION = 'Type a free-form answer not listed above';
|
|
83
|
+
export function initPickerState(options, multiSelect, init = {}) {
|
|
84
|
+
const augmented = [...options, { label: OTHER_LABEL, description: OTHER_DESCRIPTION }];
|
|
85
|
+
const otherText = init.prefilledOther ?? '';
|
|
86
|
+
const awaitingOther = otherText.length > 0;
|
|
87
|
+
// When pre-filled "Other" is requested, position the cursor on the
|
|
88
|
+
// Other row so a subsequent Esc → re-render lands the user there
|
|
89
|
+
// (otherwise they'd snap back to row 0 with no explanation).
|
|
90
|
+
const cursor = awaitingOther
|
|
91
|
+
? augmented.length - 1
|
|
92
|
+
: Math.max(0, Math.min(init.initialCursor ?? 0, augmented.length - 1));
|
|
93
|
+
return {
|
|
94
|
+
options: augmented,
|
|
95
|
+
cursor,
|
|
96
|
+
multiSelect,
|
|
97
|
+
selected: new Set(),
|
|
98
|
+
awaitingOther,
|
|
99
|
+
otherText,
|
|
100
|
+
done: false,
|
|
101
|
+
cancelled: false,
|
|
102
|
+
result: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function finalizeWithOther(state, text) {
|
|
106
|
+
if (state.multiSelect) {
|
|
107
|
+
const otherIdx = state.options.length - 1;
|
|
108
|
+
const indices = Array.from(state.selected).sort((a, b) => a - b);
|
|
109
|
+
const labels = indices.map((i) => (i === otherIdx ? text : state.options[i].label));
|
|
110
|
+
return { ...state, done: true, result: labels, otherText: text };
|
|
111
|
+
}
|
|
112
|
+
return { ...state, done: true, result: text, otherText: text };
|
|
113
|
+
}
|
|
114
|
+
export function reducePicker(state, key) {
|
|
115
|
+
if (state.done)
|
|
116
|
+
return state;
|
|
117
|
+
// Ctrl+C always cancels, in any phase. Don't gate on `key.name === 'c'`
|
|
118
|
+
// alone — some terminals send the sequence without a named binding.
|
|
119
|
+
if (key.ctrl && (key.name === 'c' || key.sequence === '')) {
|
|
120
|
+
return { ...state, done: true, cancelled: true };
|
|
121
|
+
}
|
|
122
|
+
// --- Free-text "Other" phase ------------------------------------------
|
|
123
|
+
if (state.awaitingOther) {
|
|
124
|
+
if (key.name === 'return' || key.sequence === '\r' || key.sequence === '\n') {
|
|
125
|
+
const text = state.otherText.trim();
|
|
126
|
+
if (!text)
|
|
127
|
+
return state; // empty ENTER is a no-op so the user can retry
|
|
128
|
+
return finalizeWithOther(state, text);
|
|
129
|
+
}
|
|
130
|
+
if (key.name === 'backspace') {
|
|
131
|
+
return { ...state, otherText: state.otherText.slice(0, -1) };
|
|
132
|
+
}
|
|
133
|
+
if (key.name === 'escape') {
|
|
134
|
+
// Bail back to the picker so a stray ENTER on Other isn't a one-way trip.
|
|
135
|
+
return { ...state, awaitingOther: false, otherText: '' };
|
|
136
|
+
}
|
|
137
|
+
if (key.char && key.char.length === 1) {
|
|
138
|
+
return { ...state, otherText: state.otherText + key.char };
|
|
139
|
+
}
|
|
140
|
+
return state;
|
|
141
|
+
}
|
|
142
|
+
// --- Picker phase -----------------------------------------------------
|
|
143
|
+
switch (key.name) {
|
|
144
|
+
case 'up':
|
|
145
|
+
return { ...state, cursor: (state.cursor - 1 + state.options.length) % state.options.length };
|
|
146
|
+
case 'down':
|
|
147
|
+
return { ...state, cursor: (state.cursor + 1) % state.options.length };
|
|
148
|
+
case 'space': {
|
|
149
|
+
if (!state.multiSelect)
|
|
150
|
+
return state;
|
|
151
|
+
const next = new Set(state.selected);
|
|
152
|
+
if (next.has(state.cursor))
|
|
153
|
+
next.delete(state.cursor);
|
|
154
|
+
else
|
|
155
|
+
next.add(state.cursor);
|
|
156
|
+
return { ...state, selected: next };
|
|
157
|
+
}
|
|
158
|
+
case 'return': {
|
|
159
|
+
const otherIdx = state.options.length - 1;
|
|
160
|
+
if (state.multiSelect) {
|
|
161
|
+
// Confirming with nothing selected is a no-op — the user must SPACE
|
|
162
|
+
// at least one row first. Bailing here keeps "I pressed ENTER too
|
|
163
|
+
// soon" from silently committing to an empty array.
|
|
164
|
+
if (state.selected.size === 0)
|
|
165
|
+
return state;
|
|
166
|
+
if (state.selected.has(otherIdx)) {
|
|
167
|
+
return { ...state, awaitingOther: true };
|
|
168
|
+
}
|
|
169
|
+
const indices = Array.from(state.selected).sort((a, b) => a - b);
|
|
170
|
+
return { ...state, done: true, result: indices.map((i) => state.options[i].label) };
|
|
171
|
+
}
|
|
172
|
+
if (state.cursor === otherIdx) {
|
|
173
|
+
return { ...state, awaitingOther: true };
|
|
174
|
+
}
|
|
175
|
+
return { ...state, done: true, result: state.options[state.cursor].label };
|
|
176
|
+
}
|
|
177
|
+
case 'escape':
|
|
178
|
+
case 'q':
|
|
179
|
+
return { ...state, done: true, cancelled: true };
|
|
180
|
+
}
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
export function renderPicker(state, question, header) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
if (header)
|
|
186
|
+
lines.push(`[${header}]`);
|
|
187
|
+
lines.push(question);
|
|
188
|
+
lines.push('');
|
|
189
|
+
for (let i = 0; i < state.options.length; i++) {
|
|
190
|
+
const opt = state.options[i];
|
|
191
|
+
const cursor = i === state.cursor ? '▶' : ' ';
|
|
192
|
+
const mark = state.multiSelect ? (state.selected.has(i) ? '☑ ' : '☐ ') : '';
|
|
193
|
+
lines.push(` ${cursor} ${mark}${opt.label} — ${opt.description}`);
|
|
194
|
+
}
|
|
195
|
+
lines.push('');
|
|
196
|
+
if (state.awaitingOther) {
|
|
197
|
+
lines.push('[Other] Type your answer and press ENTER · Backspace to edit · Esc to go back');
|
|
198
|
+
lines.push(`> ${state.otherText}_`);
|
|
199
|
+
}
|
|
200
|
+
else if (state.multiSelect) {
|
|
201
|
+
lines.push('↑/↓ navigate · SPACE toggle · ENTER confirm · q to cancel');
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
lines.push('↑/↓ navigate · ENTER confirm · q to cancel');
|
|
205
|
+
}
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
export function askChoice(question, options, opts = {}) {
|
|
209
|
+
// Input-shape validation first — bad shape is a caller bug regardless of
|
|
210
|
+
// TTY availability, and surfacing it as "no TTY" would misdirect the agent
|
|
211
|
+
// toward "decide yourself" when the real fix is "re-emit the call with a
|
|
212
|
+
// valid options array".
|
|
213
|
+
if (!Array.isArray(options) || options.length < 2 || options.length > 4) {
|
|
214
|
+
const count = Array.isArray(options) ? options.length : 'invalid';
|
|
215
|
+
return Promise.reject(new Error(`ask_user_choice requires 2–4 options; received ${count}.`));
|
|
216
|
+
}
|
|
217
|
+
// Reject duplicate labels (case-insensitive). The picker shows labels as
|
|
218
|
+
// the human-readable identifier and returns them as the result, so two
|
|
219
|
+
// options with the same label make the return value ambiguous and downstream
|
|
220
|
+
// branching unreliable. Catch it here, not after the picker is half-drawn.
|
|
221
|
+
// The synthetic "Other" option also collides with a user-supplied "other",
|
|
222
|
+
// so reject that too.
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
for (const o of options) {
|
|
225
|
+
const key = (o?.label ?? '').toLowerCase();
|
|
226
|
+
if (key === OTHER_LABEL.toLowerCase()) {
|
|
227
|
+
return Promise.reject(new Error(`ask_user_choice cannot use "${o.label}" as a label — "${OTHER_LABEL}" is reserved for the always-on free-text fallback.`));
|
|
228
|
+
}
|
|
229
|
+
if (seen.has(key)) {
|
|
230
|
+
return Promise.reject(new Error(`ask_user_choice options must have unique labels; "${o.label}" appears more than once (case-insensitive).`));
|
|
231
|
+
}
|
|
232
|
+
seen.add(key);
|
|
233
|
+
}
|
|
234
|
+
if (!activeReadline || !process.stdin.isTTY) {
|
|
235
|
+
return Promise.reject(new NoTTYError('ask_user_choice requires an interactive TTY (no readline interface is active or stdin is not a TTY). ' +
|
|
236
|
+
'Fall back to deciding yourself based on the available context, and state which option you picked and why in your reply.'));
|
|
237
|
+
}
|
|
238
|
+
return runPicker(question, options, opts);
|
|
239
|
+
}
|
|
240
|
+
function runPicker(question, options, opts) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const rl = activeReadline;
|
|
243
|
+
const stdout = process.stdout;
|
|
244
|
+
let state = initPickerState(options, !!opts.multiSelect, {
|
|
245
|
+
prefilledOther: opts.prefilledOther,
|
|
246
|
+
initialCursor: opts.initialCursor,
|
|
247
|
+
});
|
|
248
|
+
let renderedNewlines = 0;
|
|
249
|
+
let renderedAtLeastOnce = false;
|
|
250
|
+
// Pause the parent rl so its `line` handler doesn't fire on our ENTER
|
|
251
|
+
// press. We restore on cleanup.
|
|
252
|
+
rl.pause();
|
|
253
|
+
// readline.createInterface already calls emitKeypressEvents and sets raw
|
|
254
|
+
// mode for a TTY input; this is belt-and-suspenders for cases where the
|
|
255
|
+
// parent code disabled raw mode somewhere along the way.
|
|
256
|
+
readline.emitKeypressEvents(process.stdin);
|
|
257
|
+
try {
|
|
258
|
+
process.stdin.setRawMode?.(true);
|
|
259
|
+
}
|
|
260
|
+
catch { /* not a real TTY */ }
|
|
261
|
+
process.stdin.resume();
|
|
262
|
+
// Hide cursor while the picker is on screen — keeps the rendering tight.
|
|
263
|
+
stdout.write('\x1b[?25l');
|
|
264
|
+
pickerActive = true;
|
|
265
|
+
const clear = () => {
|
|
266
|
+
if (renderedNewlines > 0) {
|
|
267
|
+
// `\x1b[<n>F` = cursor up n lines AND col 1 (atomic). For an
|
|
268
|
+
// M-line frame containing M-1 newlines, the cursor sits at
|
|
269
|
+
// the END of line M after the write (we don't write a
|
|
270
|
+
// trailing newline). Going up `renderedNewlines` (= M-1)
|
|
271
|
+
// lines lands EXACTLY at the start of line 1 — no off-by-one.
|
|
272
|
+
stdout.write(`\x1b[${renderedNewlines}F\x1b[J`);
|
|
273
|
+
}
|
|
274
|
+
else if (renderedNewlines === 0 && renderedAtLeastOnce) {
|
|
275
|
+
// Single-line frame edge case: nothing to scroll up; just
|
|
276
|
+
// wipe the current line.
|
|
277
|
+
stdout.write('\r\x1b[K');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const render = () => {
|
|
281
|
+
clear();
|
|
282
|
+
const text = renderPicker(state, question, opts.header);
|
|
283
|
+
stdout.write(text);
|
|
284
|
+
// Track newlines (NOT lines). For "a\nb\nc" that's 2 — which
|
|
285
|
+
// is exactly the cursor-up count we need for the next clear().
|
|
286
|
+
renderedNewlines = (text.match(/\n/g) ?? []).length;
|
|
287
|
+
renderedAtLeastOnce = true;
|
|
288
|
+
};
|
|
289
|
+
const cleanup = () => {
|
|
290
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
291
|
+
// Restore cursor visibility. Leave raw mode TRUE — the REPL expects it
|
|
292
|
+
// on (Backspace + arrow keys + readline's editing all rely on raw mode)
|
|
293
|
+
// and a previous version that restored a captured `wasRaw` flipped raw
|
|
294
|
+
// mode back to false in terminals where readline's auto-init never
|
|
295
|
+
// fully engaged, which manifested as Backspace echoing `^?` after the
|
|
296
|
+
// picker exited. Picker is the one component that's GUARANTEED to know
|
|
297
|
+
// raw mode is needed, so it's the right place to assert the invariant.
|
|
298
|
+
stdout.write('\x1b[?25h');
|
|
299
|
+
try {
|
|
300
|
+
process.stdin.setRawMode?.(true);
|
|
301
|
+
}
|
|
302
|
+
catch { /* noop */ }
|
|
303
|
+
pickerActive = false;
|
|
304
|
+
// Don't auto-resume the parent rl — runAgentTurn paused it intentionally
|
|
305
|
+
// and will resume on its own schedule.
|
|
306
|
+
};
|
|
307
|
+
const onKeypress = (str, key) => {
|
|
308
|
+
const named = key?.name;
|
|
309
|
+
const isPrintable = typeof str === 'string'
|
|
310
|
+
&& str.length === 1
|
|
311
|
+
&& !key?.ctrl
|
|
312
|
+
&& named !== 'return'
|
|
313
|
+
&& named !== 'escape'
|
|
314
|
+
&& named !== 'backspace'
|
|
315
|
+
&& named !== 'tab';
|
|
316
|
+
const pk = {
|
|
317
|
+
name: named,
|
|
318
|
+
ctrl: !!key?.ctrl,
|
|
319
|
+
sequence: key?.sequence,
|
|
320
|
+
char: isPrintable ? str : undefined,
|
|
321
|
+
};
|
|
322
|
+
const prevCursor = state.cursor;
|
|
323
|
+
const wasAwaitingOther = state.awaitingOther;
|
|
324
|
+
const nextState = reducePicker(state, pk);
|
|
325
|
+
if (nextState === state)
|
|
326
|
+
return;
|
|
327
|
+
state = nextState;
|
|
328
|
+
// Live-preview hook (0.3.7): fire on a genuine cursor move in the
|
|
329
|
+
// picker phase only. Don't fire while collecting free-text in the
|
|
330
|
+
// "Other" phase — that would spam the callback on every keystroke
|
|
331
|
+
// for no useful signal. Settling back into picker phase from Other
|
|
332
|
+
// (Esc) doesn't fire either (the cursor "stayed" on Other).
|
|
333
|
+
if (opts.onCursorChange
|
|
334
|
+
&& !state.done
|
|
335
|
+
&& !state.awaitingOther
|
|
336
|
+
&& !wasAwaitingOther
|
|
337
|
+
&& state.cursor !== prevCursor) {
|
|
338
|
+
try {
|
|
339
|
+
opts.onCursorChange(state.cursor);
|
|
340
|
+
}
|
|
341
|
+
catch { /* preview callbacks must never crash the picker */ }
|
|
342
|
+
}
|
|
343
|
+
render();
|
|
344
|
+
if (state.done) {
|
|
345
|
+
cleanup();
|
|
346
|
+
if (state.cancelled) {
|
|
347
|
+
reject(new CancelledChoiceError());
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
resolve(state.result);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
process.stdin.on('keypress', onKeypress);
|
|
355
|
+
render();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
44
358
|
/**
|
|
45
359
|
* Print a line of output while the prompt is showing, then redraw the prompt
|
|
46
360
|
* with whatever the user was mid-typing. Used by callbacks that fire while the
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type readline from 'node:readline';
|
|
15
15
|
import type { Agent } from '../../agent/agent.js';
|
|
16
|
-
import type { McpClientWrapper } from '../../runtime/
|
|
16
|
+
import type { McpClientPool as McpClientWrapper } from '../../runtime/mcpPool.js';
|
|
17
17
|
import type { Config } from '../../config/config.js';
|
|
18
18
|
/**
|
|
19
19
|
* Lifecycle / REPL-scoped state that command handlers can read or mutate.
|
|
@@ -24,6 +24,8 @@ import type { Config } from '../../config/config.js';
|
|
|
24
24
|
export interface ReplContext {
|
|
25
25
|
/** Refresh the readline prompt (color reflects access mode + status segments). */
|
|
26
26
|
refreshPromptForMode: () => void;
|
|
27
|
+
/** Replace the startup banner in the active chat scrollback, if the UI supports it. */
|
|
28
|
+
replaceBanner?: (text: string) => void;
|
|
27
29
|
/** True while the REPL is mid-turn; loop ticks should defer when set. */
|
|
28
30
|
isProcessing: () => boolean;
|
|
29
31
|
/** Programmatically run an agent turn (used by /continue and friends). */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* wrapper.
|
|
9
9
|
*/
|
|
10
10
|
import type { Agent } from '../../agent/agent.js';
|
|
11
|
-
import type { McpClientWrapper } from '../../runtime/
|
|
11
|
+
import type { McpClientPool as McpClientWrapper } from '../../runtime/mcpPool.js';
|
|
12
12
|
/**
|
|
13
13
|
* Memory-aware variant of printMcpCall. Calls the tool, extracts the flat
|
|
14
14
|
* record list from whatever shape it returns, and renders compact cards
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* wrapper.
|
|
9
9
|
*/
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import
|
|
11
|
+
import { spinner } from '../spinner.js';
|
|
12
12
|
import { callMcpTool } from '../../runtime/mcpUtils.js';
|
|
13
13
|
import { clampPayload, extractMemories, renderMemoryCards } from '../../memory/formatters.js';
|
|
14
14
|
import { buildSkillPrompt, resolveSkill, SLASH_TO_SKILL } from '../../prompt/skillRunner.js';
|
|
@@ -19,9 +19,9 @@ import { buildSkillPrompt, resolveSkill, SLASH_TO_SKILL } from '../../prompt/ski
|
|
|
19
19
|
* raw output only when no records can be parsed.
|
|
20
20
|
*/
|
|
21
21
|
export async function printMemoryCards(mcpClient, toolName, args, heading) {
|
|
22
|
-
const
|
|
22
|
+
const s = spinner(chalk.gray(`${toolName}…`)).start();
|
|
23
23
|
const res = await callMcpTool(mcpClient, toolName, args);
|
|
24
|
-
|
|
24
|
+
s.stop();
|
|
25
25
|
console.log();
|
|
26
26
|
if (res.isError) {
|
|
27
27
|
console.log(chalk.red(`${heading}: tool error — ${res.text || '(no message)'}`));
|
|
@@ -44,9 +44,9 @@ export async function printMemoryCards(mcpClient, toolName, args, heading) {
|
|
|
44
44
|
* the tool's text output under a heading.
|
|
45
45
|
*/
|
|
46
46
|
export async function printMcpCall(mcpClient, toolName, args, heading) {
|
|
47
|
-
const
|
|
47
|
+
const s = spinner(chalk.gray(`${toolName}…`)).start();
|
|
48
48
|
const res = await callMcpTool(mcpClient, toolName, args);
|
|
49
|
-
|
|
49
|
+
s.stop();
|
|
50
50
|
console.log(chalk.bold(`\n${heading}`));
|
|
51
51
|
if (res.isError) {
|
|
52
52
|
console.log(chalk.red(` Tool error: ${res.text || '(no message)'}`));
|
|
@@ -113,7 +113,7 @@ export async function runSkillCommand(agent, mcpClient, slashCommand, userInput,
|
|
|
113
113
|
await runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn);
|
|
114
114
|
}
|
|
115
115
|
export async function runSkillByName(agent, mcpClient, skillName, userInput, orchestration, runTurn) {
|
|
116
|
-
const loader =
|
|
116
|
+
const loader = spinner(chalk.gray(`Loading skill: ${skillName}...`)).start();
|
|
117
117
|
let prompt;
|
|
118
118
|
try {
|
|
119
119
|
const skill = await resolveSkill(mcpClient, skillName, agent.workspaceRoot, 'full');
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CommandContext } from './_context.js';
|
|
2
|
+
import { type Theme } from '../theme.js';
|
|
3
|
+
/**
|
|
4
|
+
* `/config` slash command — 0.3.7 redesign on the new atomic-frame picker
|
|
5
|
+
* (`../wizard/picker.ts`).
|
|
6
|
+
*
|
|
7
|
+
* Verb-overloaded (lifted from
|
|
8
|
+
* `openSrc/DeepSeek-TUI/crates/tui/src/commands/config.rs:43`):
|
|
9
|
+
*
|
|
10
|
+
* - `/config` — open the settings home panel
|
|
11
|
+
* - `/config <key>` — print the current value for <key>
|
|
12
|
+
* - `/config <key> <val>` — set <key> to <val> and persist
|
|
13
|
+
* - `/config raw|json` — print scrubbed JSON dump
|
|
14
|
+
*
|
|
15
|
+
* Persistence routes through `saveConfig` / `writePreferences` — never
|
|
16
|
+
* touches JSON files directly so future schema changes stay centralized.
|
|
17
|
+
*/
|
|
18
|
+
export declare function tryHandleConfigCommand(ctx: CommandContext): Promise<boolean>;
|
|
19
|
+
export type ParsedConfigArgs = {
|
|
20
|
+
mode: 'home';
|
|
21
|
+
} | {
|
|
22
|
+
mode: 'raw';
|
|
23
|
+
} | {
|
|
24
|
+
mode: 'get';
|
|
25
|
+
key: string;
|
|
26
|
+
} | {
|
|
27
|
+
mode: 'set';
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function parseConfigArgs(args: string[]): ParsedConfigArgs;
|
|
32
|
+
export declare function listKnownConfigKeys(): string[];
|
|
33
|
+
export declare function editLlm(ctx: CommandContext): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Shared prompt for the BrainRouter MCP HTTP API key (the
|
|
36
|
+
* `BRAINROUTER_API_KEY` bearer token). Pre-fills from the env var if
|
|
37
|
+
* set, then from the previously-saved key, then blank. Returns:
|
|
38
|
+
* - the trimmed key string (possibly empty when user chose "no key")
|
|
39
|
+
* - undefined when the user pressed Esc
|
|
40
|
+
*
|
|
41
|
+
* Exported so `/login` and any future MCP-setup surfaces share one
|
|
42
|
+
* prompt copy — same subtitle text, same env-var pre-fill, same
|
|
43
|
+
* "blank OK" semantics.
|
|
44
|
+
*/
|
|
45
|
+
export declare function promptBrainrouterApiKey(theme: Theme, kind: 'local' | 'remote', existing?: string): Promise<string | undefined>;
|
|
46
|
+
export declare function buildScrubbedConfigJson(config: CommandContext['config']): string;
|