@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
@@ -0,0 +1,168 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo, useRef } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { Frame } from './Frame.js';
6
+ const OTHER_ID = '__other__';
7
+ function themeToAccent(mode) {
8
+ if (mode === 'light')
9
+ return '#A24E1F';
10
+ if (mode === 'mono')
11
+ return 'white';
12
+ if (mode === 'dark')
13
+ return '#CC9166';
14
+ return undefined;
15
+ }
16
+ export function Picker(props) {
17
+ const augmentedRows = useMemo(() => {
18
+ if (!props.allowOther)
19
+ return props.rows;
20
+ return [
21
+ ...props.rows,
22
+ {
23
+ id: OTHER_ID,
24
+ label: props.otherLabel ?? 'Other',
25
+ description: props.otherDescription ?? 'Type a free-form answer',
26
+ },
27
+ ];
28
+ }, [props.rows, props.allowOther, props.otherLabel, props.otherDescription]);
29
+ const [cursor, setCursor] = useState(() => Math.max(0, Math.min(props.initialCursor ?? 0, augmentedRows.length - 1)));
30
+ const [selected, setSelected] = useState(() => new Set());
31
+ const [phase, setPhase] = useState(props.prefilledOther !== undefined ? 'other' : 'pick');
32
+ const [otherText, setOtherText] = useState(props.prefilledOther ?? '');
33
+ const [preview, setPreview] = useState(undefined);
34
+ // Picker is intentionally "exit-agnostic" — it just calls
35
+ // `props.onResolve(result)` and trusts the caller (runPicker or the
36
+ // chat overlay slot) to decide whether to unmount Ink. This matters
37
+ // because when Picker renders as an overlay INSIDE the chat Ink, an
38
+ // internal `useApp().exit()` would unmount the WHOLE chat instead of
39
+ // just hiding the picker. runPicker.tsx wraps Picker in a small
40
+ // `ExitWrapper` for the standalone mount that owns the exit.
41
+ const finish = (result) => {
42
+ props.onResolve(result);
43
+ };
44
+ // Recompute preview when cursor moves OR on first mount.
45
+ //
46
+ // CRITICAL: store onCursorChange in a ref instead of including it in
47
+ // the deps array. Callers pass an inline lambda; React would see a
48
+ // new function reference every render, fire this effect, which calls
49
+ // setPreview(), which re-renders, which makes a new lambda, which
50
+ // fires the effect again — infinite loop that swallows every
51
+ // keystroke. The latest-callback ref pattern is the canonical fix.
52
+ const onCursorChangeRef = useRef(props.onCursorChange);
53
+ useEffect(() => { onCursorChangeRef.current = props.onCursorChange; });
54
+ useEffect(() => {
55
+ if (phase !== 'pick') {
56
+ setPreview(undefined);
57
+ return;
58
+ }
59
+ const cb = onCursorChangeRef.current;
60
+ if (!cb) {
61
+ setPreview(undefined);
62
+ return;
63
+ }
64
+ const row = augmentedRows[cursor];
65
+ if (!row || row.id === OTHER_ID) {
66
+ setPreview(undefined);
67
+ return;
68
+ }
69
+ try {
70
+ setPreview(cb(row.id, cursor));
71
+ }
72
+ catch {
73
+ setPreview(undefined);
74
+ }
75
+ // Intentionally omit augmentedRows + onCursorChangeRef from deps:
76
+ // augmentedRows is derived from props and stable across renders
77
+ // for a given mount; the callback is read through the ref.
78
+ // eslint-disable-next-line react-hooks/exhaustive-deps
79
+ }, [cursor, phase]);
80
+ useInput((input, key) => {
81
+ if (key.ctrl && input === 'c') {
82
+ finish({ kind: 'cancelled' });
83
+ return;
84
+ }
85
+ if (phase === 'other') {
86
+ // TextInput owns Enter/Backspace/character handling via onChange.
87
+ // We only handle Esc here to bail back to pick phase.
88
+ if (key.escape) {
89
+ setPhase('pick');
90
+ setOtherText('');
91
+ }
92
+ return;
93
+ }
94
+ if (key.upArrow) {
95
+ setCursor((c) => (c - 1 + augmentedRows.length) % augmentedRows.length);
96
+ return;
97
+ }
98
+ if (key.downArrow) {
99
+ setCursor((c) => (c + 1) % augmentedRows.length);
100
+ return;
101
+ }
102
+ if (key.return) {
103
+ const row = augmentedRows[cursor];
104
+ if (props.multiSelect) {
105
+ if (selected.size === 0)
106
+ return;
107
+ if (selected.has(OTHER_ID)) {
108
+ setPhase('other');
109
+ return;
110
+ }
111
+ const ids = augmentedRows.filter((r) => selected.has(r.id)).map((r) => r.id);
112
+ finish({ kind: 'multi', id: ids[0] ?? '', ids });
113
+ return;
114
+ }
115
+ if (row.id === OTHER_ID) {
116
+ setPhase('other');
117
+ return;
118
+ }
119
+ finish({ kind: 'pick', id: row.id });
120
+ return;
121
+ }
122
+ if (input === ' ' && props.multiSelect) {
123
+ const row = augmentedRows[cursor];
124
+ setSelected((prev) => {
125
+ const next = new Set(prev);
126
+ if (next.has(row.id))
127
+ next.delete(row.id);
128
+ else
129
+ next.add(row.id);
130
+ return next;
131
+ });
132
+ return;
133
+ }
134
+ if (key.escape || input === 'q') {
135
+ finish({ kind: 'cancelled' });
136
+ return;
137
+ }
138
+ });
139
+ const footer = props.footer ?? (phase === 'other'
140
+ ? '↵ accept · esc back · ⌫ erase'
141
+ : props.multiSelect
142
+ ? '↑/↓ navigate · space toggle · ↵ confirm · esc / q cancel'
143
+ : '↑/↓ navigate · ↵ confirm · esc / q cancel');
144
+ const accent = props.accentColor ?? themeToAccent(props.theme?.mode) ?? '#CC9166';
145
+ return (_jsxs(Frame, { title: props.title, subtitle: props.subtitle, badge: props.badge, footer: footer, accentColor: accent, children: [phase === 'pick' ? (_jsx(PickerRows, { rows: augmentedRows, cursor: cursor, accentColor: accent, multiSelect: !!props.multiSelect, selected: selected })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: accent, children: "\u203A Type your answer" }), _jsx(Text, { color: "gray", dimColor: true, children: props.otherDescription ?? 'Press ENTER to accept' }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: otherText, onChange: setOtherText, onSubmit: (value) => {
146
+ const trimmed = value.trim();
147
+ if (!trimmed)
148
+ return;
149
+ if (props.multiSelect) {
150
+ finish({
151
+ kind: 'multi',
152
+ id: augmentedRows.find((r) => selected.has(r.id) && r.id !== OTHER_ID)?.id ?? '',
153
+ ids: augmentedRows.filter((r) => selected.has(r.id) && r.id !== OTHER_ID).map((r) => r.id),
154
+ otherText: trimmed,
155
+ });
156
+ return;
157
+ }
158
+ finish({ kind: 'other', text: trimmed });
159
+ } })] })] })), preview && preview.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, children: preview.map((line, i) => _jsx(Text, { children: line }, i)) })) : null] }));
160
+ }
161
+ function PickerRows({ rows, cursor, accentColor, multiSelect, selected }) {
162
+ return (_jsx(Box, { flexDirection: "column", children: rows.map((row, i) => (_jsx(PickerRowView, { row: row, selected: i === cursor, accentColor: accentColor, multiSelect: multiSelect, checked: selected.has(row.id) }, row.id))) }));
163
+ }
164
+ function PickerRowView({ row, selected, accentColor, multiSelect, checked }) {
165
+ // Selected glyph + bold label + right-aligned value, lifted from
166
+ // openSrc/grok-cli/src/ui/components/SuggestionOverlay.tsx
167
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: accentColor, children: selected ? ' › ' : ' ' }), multiSelect ? _jsx(Text, { color: checked ? accentColor : 'gray', children: checked ? '[x] ' : '[ ] ' }) : null, _jsx(Box, { flexGrow: 1, children: _jsx(Text, { bold: selected, color: selected ? accentColor : undefined, children: row.label }) }), row.value ? _jsx(Text, { color: "gray", children: row.value }) : null] }), row.description ? (_jsx(Box, { paddingLeft: 5, children: _jsx(Text, { color: "gray", dimColor: true, children: row.description }) })) : null] }));
168
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Claude-code-style slash command palette.
3
+ *
4
+ * Opens when the user types `/` at an empty prompt. Renders:
5
+ *
6
+ * ─────────────────────────────────────────────────────────
7
+ * ❯ /loop
8
+ * ─────────────────────────────────────────────────────────
9
+ * › /loop Run a prompt or slash command on a cadence
10
+ * /login In-REPL MCP profile editor
11
+ * /logout Clear API keys from the active profile
12
+ * ↑/↓ select · ↵ confirm · tab autocomplete · esc cancel
13
+ *
14
+ * Filter ranking (lifted from openSrc/grok-cli/src/ui/slash-menu.ts +
15
+ * openSrc/codex/codex-rs/tui/src/bottom_pane/command_popup.rs:143):
16
+ *
17
+ * 0 command body starts with query (best — same-prefix match)
18
+ * 1 command body contains query
19
+ * 2 description contains query
20
+ * 3 no match — filtered out
21
+ *
22
+ * Stable secondary sort by original index so commands with the same
23
+ * score render in their canonical order each keystroke. Max visible
24
+ * rows capped at 8 (claude-code CHANGELOG line 378 — popup should
25
+ * NOT scale with terminal height; it stays compact).
26
+ */
27
+ export interface SlashCommandDef {
28
+ /** "/help", "/config", etc. — the literal token. */
29
+ cmd: string;
30
+ /** One-line description shown after the command. */
31
+ description: string;
32
+ }
33
+ export interface SlashPaletteProps {
34
+ /** Initial input buffer (typically just "/" — the keystroke that opened the palette). */
35
+ initialQuery: string;
36
+ /** All registered slash commands. */
37
+ commands: SlashCommandDef[];
38
+ /** Theme accent for highlights / borders. Defaults to brand orange. */
39
+ accentColor?: string;
40
+ /** Called when the user accepts a line (Enter) — text is the full submitted line. */
41
+ onResolve: (result: SlashPaletteResult) => void;
42
+ }
43
+ export type SlashPaletteResult = {
44
+ kind: 'submit';
45
+ text: string;
46
+ } | {
47
+ kind: 'cancelled';
48
+ };
49
+ export declare function scoreSlashCommand(cmd: SlashCommandDef, query: string): number;
50
+ export declare function filterCommands(commands: SlashCommandDef[], query: string): SlashCommandDef[];
51
+ export declare function SlashPalette({ initialQuery, commands, accentColor, onResolve }: SlashPaletteProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useMemo, useEffect, useRef } from 'react';
3
+ import { Box, Text, useApp, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { useTerminalSize } from './useTerminalSize.js';
6
+ const MAX_VISIBLE = 8;
7
+ export function scoreSlashCommand(cmd, query) {
8
+ if (!query)
9
+ return 0;
10
+ const q = query.toLowerCase();
11
+ const body = cmd.cmd.slice(1).toLowerCase();
12
+ if (body.startsWith(q))
13
+ return 0;
14
+ if (body.includes(q))
15
+ return 1;
16
+ if (cmd.description.toLowerCase().includes(q))
17
+ return 2;
18
+ return 3;
19
+ }
20
+ export function filterCommands(commands, query) {
21
+ if (!query)
22
+ return commands.slice(0, MAX_VISIBLE);
23
+ const scored = commands
24
+ .map((c, i) => ({ c, i, s: scoreSlashCommand(c, query) }))
25
+ .filter((x) => x.s < 3);
26
+ scored.sort((a, b) => (a.s - b.s) || (a.i - b.i));
27
+ return scored.slice(0, MAX_VISIBLE).map((x) => x.c);
28
+ }
29
+ export function SlashPalette({ initialQuery, commands, accentColor = '#CC9166', onResolve }) {
30
+ const [value, setValue] = useState(initialQuery);
31
+ const [cursor, setCursor] = useState(0);
32
+ const { exit } = useApp();
33
+ // Compute the query portion (everything after the leading `/`, up to
34
+ // the first space — so `/spawn researcher` filters by `spawn`).
35
+ const query = useMemo(() => {
36
+ if (!value.startsWith('/'))
37
+ return '';
38
+ const tail = value.slice(1);
39
+ const space = tail.indexOf(' ');
40
+ return space < 0 ? tail : tail.slice(0, space);
41
+ }, [value]);
42
+ const matches = useMemo(() => filterCommands(commands, query), [commands, query]);
43
+ // Clamp cursor when matches shrink.
44
+ useEffect(() => {
45
+ if (cursor >= matches.length)
46
+ setCursor(Math.max(0, matches.length - 1));
47
+ }, [matches.length, cursor]);
48
+ const onResolveRef = useRef(onResolve);
49
+ useEffect(() => { onResolveRef.current = onResolve; });
50
+ useInput((input, key) => {
51
+ if (key.ctrl && input === 'c') {
52
+ onResolveRef.current({ kind: 'cancelled' });
53
+ exit();
54
+ return;
55
+ }
56
+ if (key.escape) {
57
+ onResolveRef.current({ kind: 'cancelled' });
58
+ exit();
59
+ return;
60
+ }
61
+ if (key.upArrow) {
62
+ if (matches.length > 0) {
63
+ setCursor((c) => (c - 1 + matches.length) % matches.length);
64
+ }
65
+ return;
66
+ }
67
+ if (key.downArrow) {
68
+ if (matches.length > 0) {
69
+ setCursor((c) => (c + 1) % matches.length);
70
+ }
71
+ return;
72
+ }
73
+ if (key.tab) {
74
+ // Tab autocompletes to the currently-highlighted command + space.
75
+ // User can continue typing args, then hit Enter to submit.
76
+ if (matches.length > 0) {
77
+ const picked = matches[cursor] ?? matches[0];
78
+ setValue(picked.cmd + ' ');
79
+ setCursor(0);
80
+ }
81
+ return;
82
+ }
83
+ });
84
+ // When user presses Enter — TextInput's onSubmit fires. Resolve with
85
+ // the highlighted match IF the buffer is JUST `/<query>` (no args
86
+ // typed yet); otherwise submit the buffer as-is so /spawn role prompt
87
+ // works without forcing the user to tab-complete first.
88
+ const onSubmit = (text) => {
89
+ const trimmed = text.trim();
90
+ if (!trimmed) {
91
+ onResolveRef.current({ kind: 'cancelled' });
92
+ exit();
93
+ return;
94
+ }
95
+ // If the user typed JUST the slash + query (no args yet), AND the
96
+ // highlighted match is different from what they typed, expand to
97
+ // the match. Otherwise submit verbatim.
98
+ const tail = trimmed.slice(1);
99
+ const hasSpace = tail.includes(' ');
100
+ if (!hasSpace && matches.length > 0) {
101
+ const picked = matches[cursor] ?? matches[0];
102
+ if (picked.cmd !== trimmed) {
103
+ onResolveRef.current({ kind: 'submit', text: picked.cmd });
104
+ exit();
105
+ return;
106
+ }
107
+ }
108
+ onResolveRef.current({ kind: 'submit', text: trimmed });
109
+ exit();
110
+ };
111
+ // If the user types more than just `/`, AND it no longer starts with
112
+ // `/` (e.g. backspaced past the slash), cancel the palette so the
113
+ // user returns to the normal readline prompt.
114
+ useEffect(() => {
115
+ if (value.length > 0 && !value.startsWith('/')) {
116
+ onResolveRef.current({ kind: 'cancelled' });
117
+ exit();
118
+ }
119
+ }, [value, exit]);
120
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Divider, { color: accentColor }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: accentColor, children: "\u276F " }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: onSubmit })] }), _jsx(Divider, { color: accentColor }), matches.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "(no matching commands)" }) })) : (_jsx(_Fragment, { children: matches.map((cmd, i) => (_jsx(SlashRow, { cmd: cmd, selected: i === cursor, accentColor: accentColor }, cmd.cmd))) })), _jsx(Box, { paddingX: 1, marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "\u2191/\u2193 select \u00B7 \u21B5 confirm \u00B7 tab autocomplete \u00B7 esc cancel" }) })] }));
121
+ }
122
+ function SlashRow({ cmd, selected, accentColor }) {
123
+ // Width hint: pad the cmd column so descriptions align across rows.
124
+ // The widest known slash command is around 28 chars (e.g. `/implement-plan`
125
+ // is 15; longest help cmd lines are longer); 24 is a sensible target.
126
+ const cmdCol = 26;
127
+ const padded = cmd.cmd.length >= cmdCol ? cmd.cmd : cmd.cmd + ' '.repeat(cmdCol - cmd.cmd.length);
128
+ return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: accentColor, children: selected ? '› ' : ' ' }), _jsx(Text, { bold: selected, color: selected ? accentColor : undefined, children: padded }), _jsx(Text, { color: "gray", children: cmd.description })] }));
129
+ }
130
+ function Divider({ color }) {
131
+ // Full-width horizontal rule — matches claude-code's chrome. Uses
132
+ // useTerminalSize so the divider auto-reflows on terminal resize
133
+ // instead of being frozen at its initial width.
134
+ const { columns } = useTerminalSize();
135
+ return (_jsx(Box, { children: _jsx(Text, { color: color, dimColor: true, children: '─'.repeat(Math.max(20, columns - 1)) }) }));
136
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Framed free-text input. Used by the wizard's API-key step, the
3
+ * remote-URL prompt, and the /config / /login sub-prompts.
4
+ *
5
+ * The `validate` callback runs on submit. Return undefined to accept;
6
+ * return a string to render it as an inline error and keep the field
7
+ * open for the user to fix.
8
+ */
9
+ export interface TextFieldProps {
10
+ title: string;
11
+ subtitle?: string;
12
+ badge?: string;
13
+ prefilled?: string;
14
+ placeholder?: string;
15
+ mask?: boolean;
16
+ validate?: (value: string) => string | undefined;
17
+ accentColor?: string;
18
+ /** Back-compat: pulls accentColor from theme.mode if accentColor not set. */
19
+ theme?: {
20
+ mode: string;
21
+ };
22
+ /** Ignored — kept for back-compat with old picker shape. */
23
+ eraseOnClose?: boolean;
24
+ /** Ignored — kept for back-compat. */
25
+ footer?: string;
26
+ onResolve: (result: TextFieldResult) => void;
27
+ }
28
+ export type TextFieldResult = {
29
+ kind: 'accept';
30
+ text: string;
31
+ } | {
32
+ kind: 'cancelled';
33
+ };
34
+ export declare function TextField(props: TextFieldProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import TextInput from 'ink-text-input';
5
+ import { Frame } from './Frame.js';
6
+ function themeToAccent(mode) {
7
+ if (mode === 'light')
8
+ return '#A24E1F';
9
+ if (mode === 'mono')
10
+ return 'white';
11
+ if (mode === 'dark')
12
+ return '#CC9166';
13
+ return undefined;
14
+ }
15
+ export function TextField(props) {
16
+ const [value, setValue] = useState(props.prefilled ?? '');
17
+ const [error, setError] = useState(undefined);
18
+ // Exit-agnostic: just resolves. The caller (runTextField for the
19
+ // standalone mount, or the chat overlay slot when invoked from inside
20
+ // the Ink chat REPL) decides what to do. See Picker.tsx comment for
21
+ // why a built-in `useApp().exit()` here would break the overlay path.
22
+ const finish = (result) => {
23
+ props.onResolve(result);
24
+ };
25
+ useInput((input, key) => {
26
+ if (key.escape || (key.ctrl && input === 'c')) {
27
+ finish({ kind: 'cancelled' });
28
+ }
29
+ });
30
+ const onSubmit = (next) => {
31
+ if (props.validate) {
32
+ const verdict = props.validate(next);
33
+ if (verdict !== undefined) {
34
+ setError(verdict);
35
+ return;
36
+ }
37
+ }
38
+ finish({ kind: 'accept', text: next });
39
+ };
40
+ const onChange = (next) => {
41
+ setValue(next);
42
+ if (error)
43
+ setError(undefined);
44
+ };
45
+ const accent = props.accentColor ?? themeToAccent(props.theme?.mode) ?? '#CC9166';
46
+ return (_jsxs(Frame, { title: props.title, subtitle: props.subtitle, badge: props.badge, footer: props.footer ?? '↵ accept · esc cancel · ⌫ erase', accentColor: accent, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: props.placeholder, mask: props.mask ? '·' : undefined })] }), error ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u2717 ", error] }) })) : null] }));
47
+ }
@@ -0,0 +1,7 @@
1
+ import { type WizardState } from '../wizard/types.js';
2
+ export interface WizardAppProps {
3
+ workspaceRoot: string;
4
+ /** Fires once the wizard reaches a terminal state. */
5
+ onFinish: (state: WizardState) => void;
6
+ }
7
+ export declare function WizardApp({ workspaceRoot, onFinish }: WizardAppProps): import("react/jsx-runtime").JSX.Element;