@kinqs/brainrouter-cli 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/bin/cli.cjs +71 -0
  8. package/dist/agent/agent.d.ts +224 -3
  9. package/dist/agent/agent.js +561 -55
  10. package/dist/cli/banner.d.ts +80 -0
  11. package/dist/cli/banner.js +232 -0
  12. package/dist/cli/cliPrompt.d.ts +106 -0
  13. package/dist/cli/cliPrompt.js +314 -0
  14. package/dist/cli/commands/_context.d.ts +3 -1
  15. package/dist/cli/commands/_helpers.d.ts +1 -1
  16. package/dist/cli/commands/_helpers.js +6 -6
  17. package/dist/cli/commands/config.d.ts +46 -0
  18. package/dist/cli/commands/config.js +1042 -0
  19. package/dist/cli/commands/guard.js +75 -10
  20. package/dist/cli/commands/init.d.ts +20 -0
  21. package/dist/cli/commands/init.js +64 -0
  22. package/dist/cli/commands/login.d.ts +13 -0
  23. package/dist/cli/commands/login.js +179 -0
  24. package/dist/cli/commands/mcp.d.ts +19 -0
  25. package/dist/cli/commands/mcp.js +286 -0
  26. package/dist/cli/commands/memory.js +2 -2
  27. package/dist/cli/commands/obs.js +22 -22
  28. package/dist/cli/commands/orchestration.js +18 -0
  29. package/dist/cli/commands/session.js +13 -5
  30. package/dist/cli/commands/ui.js +202 -91
  31. package/dist/cli/commands/workflow.d.ts +20 -0
  32. package/dist/cli/commands/workflow.js +368 -51
  33. package/dist/cli/ink/ChatApp.d.ts +206 -0
  34. package/dist/cli/ink/ChatApp.js +493 -0
  35. package/dist/cli/ink/Frame.d.ts +26 -0
  36. package/dist/cli/ink/Frame.js +5 -0
  37. package/dist/cli/ink/Picker.d.ts +65 -0
  38. package/dist/cli/ink/Picker.js +133 -0
  39. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  40. package/dist/cli/ink/SlashPalette.js +136 -0
  41. package/dist/cli/ink/TextField.d.ts +34 -0
  42. package/dist/cli/ink/TextField.js +47 -0
  43. package/dist/cli/ink/WizardApp.d.ts +7 -0
  44. package/dist/cli/ink/WizardApp.js +422 -0
  45. package/dist/cli/ink/ambientChat.d.ts +34 -0
  46. package/dist/cli/ink/ambientChat.js +7 -0
  47. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  48. package/dist/cli/ink/consoleCapture.js +33 -0
  49. package/dist/cli/ink/markdownRender.d.ts +41 -0
  50. package/dist/cli/ink/markdownRender.js +278 -0
  51. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  52. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  53. package/dist/cli/ink/runChat.d.ts +34 -0
  54. package/dist/cli/ink/runChat.js +571 -0
  55. package/dist/cli/ink/runPicker.d.ts +31 -0
  56. package/dist/cli/ink/runPicker.js +139 -0
  57. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  58. package/dist/cli/ink/runSlashPalette.js +33 -0
  59. package/dist/cli/ink/runWizard.d.ts +22 -0
  60. package/dist/cli/ink/runWizard.js +133 -0
  61. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  62. package/dist/cli/ink/stdinHandoff.js +78 -0
  63. package/dist/cli/ink/toolFormat.d.ts +73 -0
  64. package/dist/cli/ink/toolFormat.js +180 -0
  65. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  66. package/dist/cli/ink/useTerminalSize.js +26 -0
  67. package/dist/cli/repl.d.ts +25 -3
  68. package/dist/cli/repl.js +64 -646
  69. package/dist/cli/slashSuggest.d.ts +32 -0
  70. package/dist/cli/slashSuggest.js +146 -0
  71. package/dist/cli/spinner.d.ts +34 -0
  72. package/dist/cli/spinner.js +36 -0
  73. package/dist/cli/statusline.d.ts +67 -0
  74. package/dist/cli/statusline.js +204 -0
  75. package/dist/cli/theme.d.ts +79 -0
  76. package/dist/cli/theme.js +106 -0
  77. package/dist/cli/whereView.d.ts +81 -0
  78. package/dist/cli/whereView.js +245 -0
  79. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  80. package/dist/cli/wizard/modelsApi.js +166 -0
  81. package/dist/cli/wizard/picker.d.ts +202 -0
  82. package/dist/cli/wizard/picker.js +547 -0
  83. package/dist/cli/wizard/providers.d.ts +86 -0
  84. package/dist/cli/wizard/providers.js +190 -0
  85. package/dist/cli/wizard/runner.d.ts +13 -0
  86. package/dist/cli/wizard/runner.js +488 -0
  87. package/dist/cli/wizard/types.d.ts +122 -0
  88. package/dist/cli/wizard/types.js +109 -0
  89. package/dist/config/config.d.ts +52 -0
  90. package/dist/config/config.js +89 -75
  91. package/dist/index.js +215 -206
  92. package/dist/memory/briefing.d.ts +11 -1
  93. package/dist/memory/briefing.js +69 -1
  94. package/dist/memory/consolidation.d.ts +1 -1
  95. package/dist/orchestration/agentRegistry.d.ts +36 -0
  96. package/dist/orchestration/agentRegistry.js +64 -0
  97. package/dist/orchestration/orchestrator.d.ts +7 -0
  98. package/dist/orchestration/orchestrator.js +2 -0
  99. package/dist/orchestration/tools.d.ts +10 -1
  100. package/dist/orchestration/tools.js +48 -4
  101. package/dist/prompt/breadthHint.d.ts +5 -0
  102. package/dist/prompt/breadthHint.js +44 -0
  103. package/dist/prompt/skillCatalog.d.ts +11 -0
  104. package/dist/prompt/skillCatalog.js +134 -0
  105. package/dist/prompt/skillRunner.d.ts +2 -2
  106. package/dist/prompt/skillRunner.js +2 -31
  107. package/dist/prompt/systemPrompt.d.ts +34 -0
  108. package/dist/prompt/systemPrompt.js +128 -108
  109. package/dist/runtime/dangerousCommand.d.ts +53 -0
  110. package/dist/runtime/dangerousCommand.js +105 -0
  111. package/dist/runtime/mcpClient.d.ts +38 -1
  112. package/dist/runtime/mcpClient.js +104 -13
  113. package/dist/runtime/mcpPool.d.ts +162 -0
  114. package/dist/runtime/mcpPool.js +423 -0
  115. package/dist/runtime/mcpUtils.d.ts +3 -1
  116. package/dist/state/goalStore.d.ts +98 -17
  117. package/dist/state/goalStore.js +132 -42
  118. package/dist/state/preferencesStore.d.ts +67 -3
  119. package/dist/state/preferencesStore.js +84 -1
  120. package/dist/state/workflowArtifacts.d.ts +63 -2
  121. package/dist/state/workflowArtifacts.js +120 -8
  122. package/dist/tests/_helpers.d.ts +31 -0
  123. package/dist/tests/_helpers.js +91 -0
  124. package/package.json +12 -5
  125. package/.env.example +0 -109
@@ -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/mcpClient.js';
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/mcpClient.js';
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 ora from 'ora';
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 spinner = ora(chalk.gray(`${toolName}…`)).start();
22
+ const s = spinner(chalk.gray(`${toolName}…`)).start();
23
23
  const res = await callMcpTool(mcpClient, toolName, args);
24
- spinner.stop();
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 spinner = ora(chalk.gray(`${toolName}…`)).start();
47
+ const s = spinner(chalk.gray(`${toolName}…`)).start();
48
48
  const res = await callMcpTool(mcpClient, toolName, args);
49
- spinner.stop();
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 = ora(chalk.gray(`Loading skill: ${skillName}...`)).start();
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;