@kinqs/brainrouter-cli 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) 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/changelog/0.2.0.md +15 -0
  8. package/changelog/0.3.0.md +20 -0
  9. package/changelog/0.3.1.md +22 -0
  10. package/changelog/0.3.2.md +15 -0
  11. package/changelog/0.3.3.md +19 -0
  12. package/changelog/0.3.4.md +20 -0
  13. package/changelog/0.3.5.md +9 -0
  14. package/changelog/0.3.6.md +9 -0
  15. package/changelog/0.3.7.md +20 -0
  16. package/changelog/0.3.8.md +30 -0
  17. package/changelog/README.md +41 -0
  18. package/dist/agent/agent.d.ts +34 -1
  19. package/dist/agent/agent.js +372 -79
  20. package/dist/agent/toolCallRecovery.d.ts +57 -0
  21. package/dist/agent/toolCallRecovery.js +130 -0
  22. package/dist/agent/toolSafety.d.ts +17 -0
  23. package/dist/agent/toolSafety.js +102 -0
  24. package/dist/cli/banner.d.ts +20 -0
  25. package/dist/cli/banner.js +47 -14
  26. package/dist/cli/cliPrompt.d.ts +40 -3
  27. package/dist/cli/cliPrompt.js +117 -25
  28. package/dist/cli/commands/_context.d.ts +3 -1
  29. package/dist/cli/commands/_helpers.d.ts +1 -1
  30. package/dist/cli/commands/config.d.ts +46 -0
  31. package/dist/cli/commands/config.js +1042 -0
  32. package/dist/cli/commands/init.d.ts +20 -0
  33. package/dist/cli/commands/init.js +64 -0
  34. package/dist/cli/commands/login.d.ts +13 -0
  35. package/dist/cli/commands/login.js +179 -0
  36. package/dist/cli/commands/mcp.d.ts +13 -11
  37. package/dist/cli/commands/mcp.js +261 -74
  38. package/dist/cli/commands/mcpInstall.d.ts +20 -0
  39. package/dist/cli/commands/mcpInstall.js +87 -0
  40. package/dist/cli/commands/orchestration.js +51 -0
  41. package/dist/cli/commands/releaseNotes.d.ts +24 -0
  42. package/dist/cli/commands/releaseNotes.js +109 -0
  43. package/dist/cli/commands/schedule.d.ts +18 -0
  44. package/dist/cli/commands/schedule.js +189 -0
  45. package/dist/cli/commands/ui.js +119 -60
  46. package/dist/cli/commands/workflow.d.ts +2 -0
  47. package/dist/cli/commands/workflow.js +54 -8
  48. package/dist/cli/ink/ChatApp.d.ts +206 -0
  49. package/dist/cli/ink/ChatApp.js +493 -0
  50. package/dist/cli/ink/Frame.d.ts +26 -0
  51. package/dist/cli/ink/Frame.js +5 -0
  52. package/dist/cli/ink/Picker.d.ts +71 -0
  53. package/dist/cli/ink/Picker.js +168 -0
  54. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  55. package/dist/cli/ink/SlashPalette.js +136 -0
  56. package/dist/cli/ink/TextField.d.ts +34 -0
  57. package/dist/cli/ink/TextField.js +47 -0
  58. package/dist/cli/ink/WizardApp.d.ts +7 -0
  59. package/dist/cli/ink/WizardApp.js +422 -0
  60. package/dist/cli/ink/ambientChat.d.ts +34 -0
  61. package/dist/cli/ink/ambientChat.js +7 -0
  62. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  63. package/dist/cli/ink/consoleCapture.js +33 -0
  64. package/dist/cli/ink/markdownRender.d.ts +41 -0
  65. package/dist/cli/ink/markdownRender.js +278 -0
  66. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  67. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  68. package/dist/cli/ink/runChat.d.ts +34 -0
  69. package/dist/cli/ink/runChat.js +682 -0
  70. package/dist/cli/ink/runPicker.d.ts +31 -0
  71. package/dist/cli/ink/runPicker.js +139 -0
  72. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  73. package/dist/cli/ink/runSlashPalette.js +33 -0
  74. package/dist/cli/ink/runWizard.d.ts +22 -0
  75. package/dist/cli/ink/runWizard.js +133 -0
  76. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  77. package/dist/cli/ink/stdinHandoff.js +78 -0
  78. package/dist/cli/ink/toolFormat.d.ts +75 -0
  79. package/dist/cli/ink/toolFormat.js +206 -0
  80. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  81. package/dist/cli/ink/useTerminalSize.js +26 -0
  82. package/dist/cli/repl.d.ts +25 -3
  83. package/dist/cli/repl.js +52 -714
  84. package/dist/cli/slashSuggest.d.ts +32 -0
  85. package/dist/cli/slashSuggest.js +146 -0
  86. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  87. package/dist/cli/wizard/modelsApi.js +166 -0
  88. package/dist/cli/wizard/picker.d.ts +202 -0
  89. package/dist/cli/wizard/picker.js +547 -0
  90. package/dist/cli/wizard/providers.d.ts +86 -0
  91. package/dist/cli/wizard/providers.js +190 -0
  92. package/dist/cli/wizard/runner.d.ts +13 -0
  93. package/dist/cli/wizard/runner.js +488 -0
  94. package/dist/cli/wizard/types.d.ts +122 -0
  95. package/dist/cli/wizard/types.js +109 -0
  96. package/dist/config/config.d.ts +13 -1
  97. package/dist/config/config.js +45 -3
  98. package/dist/index.js +157 -206
  99. package/dist/memory/briefing.d.ts +1 -1
  100. package/dist/memory/briefing.js +4 -4
  101. package/dist/memory/consolidation.d.ts +1 -1
  102. package/dist/orchestration/agentRegistry.d.ts +36 -0
  103. package/dist/orchestration/agentRegistry.js +64 -0
  104. package/dist/orchestration/orchestrator.d.ts +7 -0
  105. package/dist/orchestration/orchestrator.js +2 -0
  106. package/dist/orchestration/tools.d.ts +105 -3
  107. package/dist/orchestration/tools.js +167 -8
  108. package/dist/prompt/skillCatalog.d.ts +11 -0
  109. package/dist/prompt/skillCatalog.js +134 -0
  110. package/dist/prompt/skillRunner.d.ts +2 -2
  111. package/dist/prompt/skillRunner.js +2 -31
  112. package/dist/prompt/systemPrompt.js +7 -2
  113. package/dist/runtime/anthropicAdapter.d.ts +100 -0
  114. package/dist/runtime/anthropicAdapter.js +293 -0
  115. package/dist/runtime/cronParser.d.ts +23 -0
  116. package/dist/runtime/cronParser.js +122 -0
  117. package/dist/runtime/mcpClient.js +14 -11
  118. package/dist/runtime/mcpPool.d.ts +170 -0
  119. package/dist/runtime/mcpPool.js +442 -0
  120. package/dist/runtime/mcpUtils.d.ts +17 -1
  121. package/dist/runtime/mcpUtils.js +23 -0
  122. package/dist/runtime/scheduleTicker.d.ts +33 -0
  123. package/dist/runtime/scheduleTicker.js +99 -0
  124. package/dist/runtime/vendorSnippets.d.ts +45 -0
  125. package/dist/runtime/vendorSnippets.js +153 -0
  126. package/dist/state/scheduleStore.d.ts +37 -0
  127. package/dist/state/scheduleStore.js +64 -0
  128. package/package.json +14 -5
  129. package/.env.example +0 -116
@@ -1,4 +1,5 @@
1
1
  import readline from 'node:readline';
2
+ import { getAmbientChat } from './ink/ambientChat.js';
2
3
  /**
3
4
  * Shared bridge between the REPL's readline interface and modules outside
4
5
  * repl.ts that need to (a) write above the prompt without scrambling input,
@@ -33,6 +34,9 @@ export function isPickerActive() { return pickerActive; }
33
34
  * (e.g. piped non-interactive runs).
34
35
  */
35
36
  export function askYesNo(question, defaultValue = false) {
37
+ if (getAmbientChat() && process.stdin.isTTY) {
38
+ return runInkYesNo(question, defaultValue);
39
+ }
36
40
  if (!activeReadline || !process.stdin.isTTY) {
37
41
  return Promise.resolve(defaultValue);
38
42
  }
@@ -80,15 +84,23 @@ export class CancelledChoiceError extends Error {
80
84
  /** Synthetic always-on "Other" option appended to every picker. */
81
85
  const OTHER_LABEL = 'Other';
82
86
  const OTHER_DESCRIPTION = 'Type a free-form answer not listed above';
83
- export function initPickerState(options, multiSelect) {
87
+ export function initPickerState(options, multiSelect, init = {}) {
84
88
  const augmented = [...options, { label: OTHER_LABEL, description: OTHER_DESCRIPTION }];
89
+ const otherText = init.prefilledOther ?? '';
90
+ const awaitingOther = otherText.length > 0;
91
+ // When pre-filled "Other" is requested, position the cursor on the
92
+ // Other row so a subsequent Esc → re-render lands the user there
93
+ // (otherwise they'd snap back to row 0 with no explanation).
94
+ const cursor = awaitingOther
95
+ ? augmented.length - 1
96
+ : Math.max(0, Math.min(init.initialCursor ?? 0, augmented.length - 1));
85
97
  return {
86
98
  options: augmented,
87
- cursor: 0,
99
+ cursor,
88
100
  multiSelect,
89
101
  selected: new Set(),
90
- awaitingOther: false,
91
- otherText: '',
102
+ awaitingOther,
103
+ otherText,
92
104
  done: false,
93
105
  cancelled: false,
94
106
  result: null,
@@ -197,20 +209,6 @@ export function renderPicker(state, question, header) {
197
209
  }
198
210
  return lines.join('\n');
199
211
  }
200
- /**
201
- * Mid-turn multi-choice prompt with arrow-key navigation, a checkbox UI
202
- * for multi-select, and an always-on "Other" option that drops to free-text
203
- * input. Pause/resume the parent REPL the same way `askYesNo` does, so it
204
- * composes cleanly with the existing readline bridge.
205
- *
206
- * Non-TTY behavior is strict: throws `NoTTYError` instead of defaulting to
207
- * option 1. The agent calling this is asking the human for judgment; making
208
- * the call for them in CI / piped / `brainrouter run` would silently commit
209
- * to a path the user never saw.
210
- *
211
- * User cancellation (Esc, q, Ctrl+C) throws `CancelledChoiceError` so the
212
- * tool wrapper can surface "user declined to commit" as a tool-call error.
213
- */
214
212
  export function askChoice(question, options, opts = {}) {
215
213
  // Input-shape validation first — bad shape is a caller bug regardless of
216
214
  // TTY availability, and surfacing it as "no TTY" would misdirect the agent
@@ -241,14 +239,79 @@ export function askChoice(question, options, opts = {}) {
241
239
  return Promise.reject(new NoTTYError('ask_user_choice requires an interactive TTY (no readline interface is active or stdin is not a TTY). ' +
242
240
  'Fall back to deciding yourself based on the available context, and state which option you picked and why in your reply.'));
243
241
  }
242
+ if (getAmbientChat()) {
243
+ return runInkChoice(question, options, opts);
244
+ }
244
245
  return runPicker(question, options, opts);
245
246
  }
247
+ async function runInkYesNo(question, defaultValue) {
248
+ const { runPicker } = await import('./ink/runPicker.js');
249
+ const result = await runPicker({
250
+ title: question,
251
+ badge: 'Confirm',
252
+ rows: [
253
+ { id: 'yes', label: 'Yes', description: 'Allow this action' },
254
+ { id: 'no', label: 'No', description: 'Do not allow this action' },
255
+ ],
256
+ initialCursor: defaultValue ? 0 : 1,
257
+ allowOther: false,
258
+ });
259
+ if (result.kind !== 'pick')
260
+ return defaultValue;
261
+ return result.id === 'yes';
262
+ }
263
+ async function runInkChoice(question, options, opts) {
264
+ const { runPicker } = await import('./ink/runPicker.js');
265
+ const rows = options.map((option, i) => ({
266
+ id: `choice:${i}`,
267
+ label: option.label,
268
+ description: option.description,
269
+ }));
270
+ const result = await runPicker({
271
+ title: question,
272
+ badge: opts.header,
273
+ rows,
274
+ initialCursor: opts.initialCursor,
275
+ allowOther: true,
276
+ otherLabel: OTHER_LABEL,
277
+ otherDescription: OTHER_DESCRIPTION,
278
+ prefilledOther: opts.prefilledOther,
279
+ multiSelect: !!opts.multiSelect,
280
+ onCursorChange: opts.onCursorChange
281
+ ? (_id, index) => {
282
+ opts.onCursorChange?.(index);
283
+ return undefined;
284
+ }
285
+ : undefined,
286
+ });
287
+ if (result.kind === 'cancelled') {
288
+ throw new CancelledChoiceError();
289
+ }
290
+ if (result.kind === 'other') {
291
+ return result.text;
292
+ }
293
+ if (result.kind === 'multi') {
294
+ const answers = result.ids.map((id) => {
295
+ const idx = Number(id.slice('choice:'.length));
296
+ return options[idx]?.label;
297
+ }).filter((label) => !!label);
298
+ if (result.otherText)
299
+ answers.push(result.otherText);
300
+ return answers;
301
+ }
302
+ const idx = Number(result.id.slice('choice:'.length));
303
+ return options[idx]?.label ?? options[0].label;
304
+ }
246
305
  function runPicker(question, options, opts) {
247
306
  return new Promise((resolve, reject) => {
248
307
  const rl = activeReadline;
249
308
  const stdout = process.stdout;
250
- let state = initPickerState(options, !!opts.multiSelect);
251
- let renderedLines = 0;
309
+ let state = initPickerState(options, !!opts.multiSelect, {
310
+ prefilledOther: opts.prefilledOther,
311
+ initialCursor: opts.initialCursor,
312
+ });
313
+ let renderedNewlines = 0;
314
+ let renderedAtLeastOnce = false;
252
315
  // Pause the parent rl so its `line` handler doesn't fire on our ENTER
253
316
  // press. We restore on cleanup.
254
317
  rl.pause();
@@ -265,16 +328,28 @@ function runPicker(question, options, opts) {
265
328
  stdout.write('\x1b[?25l');
266
329
  pickerActive = true;
267
330
  const clear = () => {
268
- if (renderedLines > 0) {
269
- // Move cursor up `renderedLines` then clear to end of screen.
270
- stdout.write(`\x1b[${renderedLines}A\r\x1b[J`);
331
+ if (renderedNewlines > 0) {
332
+ // `\x1b[<n>F` = cursor up n lines AND col 1 (atomic). For an
333
+ // M-line frame containing M-1 newlines, the cursor sits at
334
+ // the END of line M after the write (we don't write a
335
+ // trailing newline). Going up `renderedNewlines` (= M-1)
336
+ // lines lands EXACTLY at the start of line 1 — no off-by-one.
337
+ stdout.write(`\x1b[${renderedNewlines}F\x1b[J`);
338
+ }
339
+ else if (renderedNewlines === 0 && renderedAtLeastOnce) {
340
+ // Single-line frame edge case: nothing to scroll up; just
341
+ // wipe the current line.
342
+ stdout.write('\r\x1b[K');
271
343
  }
272
344
  };
273
345
  const render = () => {
274
346
  clear();
275
347
  const text = renderPicker(state, question, opts.header);
276
- stdout.write(text + '\n');
277
- renderedLines = text.split('\n').length;
348
+ stdout.write(text);
349
+ // Track newlines (NOT lines). For "a\nb\nc" that's 2 — which
350
+ // is exactly the cursor-up count we need for the next clear().
351
+ renderedNewlines = (text.match(/\n/g) ?? []).length;
352
+ renderedAtLeastOnce = true;
278
353
  };
279
354
  const cleanup = () => {
280
355
  process.stdin.removeListener('keypress', onKeypress);
@@ -309,10 +384,27 @@ function runPicker(question, options, opts) {
309
384
  sequence: key?.sequence,
310
385
  char: isPrintable ? str : undefined,
311
386
  };
387
+ const prevCursor = state.cursor;
388
+ const wasAwaitingOther = state.awaitingOther;
312
389
  const nextState = reducePicker(state, pk);
313
390
  if (nextState === state)
314
391
  return;
315
392
  state = nextState;
393
+ // Live-preview hook (0.3.7): fire on a genuine cursor move in the
394
+ // picker phase only. Don't fire while collecting free-text in the
395
+ // "Other" phase — that would spam the callback on every keystroke
396
+ // for no useful signal. Settling back into picker phase from Other
397
+ // (Esc) doesn't fire either (the cursor "stayed" on Other).
398
+ if (opts.onCursorChange
399
+ && !state.done
400
+ && !state.awaitingOther
401
+ && !wasAwaitingOther
402
+ && state.cursor !== prevCursor) {
403
+ try {
404
+ opts.onCursorChange(state.cursor);
405
+ }
406
+ catch { /* preview callbacks must never crash the picker */ }
407
+ }
316
408
  render();
317
409
  if (state.done) {
318
410
  cleanup();
@@ -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
@@ -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;