@opencoven/coven-code 0.0.3 → 0.0.6

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 (115) hide show
  1. package/README.md +29 -130
  2. package/bin/coven-code +26 -0
  3. package/install.js +117 -0
  4. package/package.json +26 -23
  5. package/bin/coven-code-sdk.mjs +0 -12
  6. package/bin/coven-code.mjs +0 -19
  7. package/docs/CLI.md +0 -256
  8. package/docs/CONFIGURATION.md +0 -107
  9. package/docs/DEMO.md +0 -453
  10. package/docs/DEVELOPMENT.md +0 -104
  11. package/docs/DOGFOOD-PROTOCOL.md +0 -263
  12. package/docs/MCP-SKILLS-PLUGINS.md +0 -127
  13. package/docs/README.md +0 -39
  14. package/docs/RELEASE.md +0 -33
  15. package/docs/SDK.md +0 -107
  16. package/docs/superpowers/plans/2026-05-25-coven-code-panel-tui.md +0 -904
  17. package/docs/superpowers/plans/2026-05-25-coven-code-rebrand.md +0 -670
  18. package/docs/superpowers/specs/2026-05-25-coven-code-panel-tui-design.md +0 -235
  19. package/docs/superpowers/specs/2026-05-26-slash-first-tui-review.md +0 -63
  20. package/src/agent/fixture.mjs +0 -95
  21. package/src/agent/lane.mjs +0 -136
  22. package/src/cli/dispatch.mjs +0 -66
  23. package/src/cli/execute.mjs +0 -590
  24. package/src/cli/help.mjs +0 -58
  25. package/src/cli/interactive-core.mjs +0 -28
  26. package/src/cli/interactive-io.mjs +0 -101
  27. package/src/cli/interactive-slash.mjs +0 -184
  28. package/src/cli/notifications.mjs +0 -13
  29. package/src/cli/parse.mjs +0 -83
  30. package/src/cli/reasoning.mjs +0 -45
  31. package/src/cli/refs.mjs +0 -162
  32. package/src/cli/repl.mjs +0 -60
  33. package/src/cli/slash-commands.mjs +0 -375
  34. package/src/cli/stream-json.mjs +0 -116
  35. package/src/cli/tui-actions.mjs +0 -72
  36. package/src/cli/tui-blessed.mjs +0 -198
  37. package/src/cli/tui-keys.mjs +0 -80
  38. package/src/cli/tui-lane.mjs +0 -73
  39. package/src/cli/tui-render.mjs +0 -169
  40. package/src/cli/tui-submit.mjs +0 -82
  41. package/src/cli/tui.mjs +0 -174
  42. package/src/commands/agents.mjs +0 -53
  43. package/src/commands/config.mjs +0 -27
  44. package/src/commands/ide.mjs +0 -17
  45. package/src/commands/login.mjs +0 -84
  46. package/src/commands/mcp.mjs +0 -176
  47. package/src/commands/permissions-eval.mjs +0 -122
  48. package/src/commands/permissions-rules.mjs +0 -53
  49. package/src/commands/permissions-text.mjs +0 -112
  50. package/src/commands/permissions.mjs +0 -62
  51. package/src/commands/plugins.mjs +0 -86
  52. package/src/commands/review.mjs +0 -74
  53. package/src/commands/skill.mjs +0 -23
  54. package/src/commands/threads.mjs +0 -165
  55. package/src/commands/tools.mjs +0 -77
  56. package/src/commands/update.mjs +0 -31
  57. package/src/commands/usage.mjs +0 -34
  58. package/src/constants.mjs +0 -52
  59. package/src/main.mjs +0 -87
  60. package/src/mcp/discover.mjs +0 -154
  61. package/src/mcp/local.mjs +0 -55
  62. package/src/mcp/parsers.mjs +0 -46
  63. package/src/mcp/permissions.mjs +0 -52
  64. package/src/mcp/probe.mjs +0 -85
  65. package/src/mcp/registry.mjs +0 -96
  66. package/src/mcp/remote-oauth.mjs +0 -55
  67. package/src/mcp/remote-session.mjs +0 -54
  68. package/src/mcp/remote-sse.mjs +0 -82
  69. package/src/mcp/remote.mjs +0 -74
  70. package/src/plugins/api.mjs +0 -187
  71. package/src/plugins/configuration.mjs +0 -124
  72. package/src/plugins/discover.mjs +0 -84
  73. package/src/plugins/helpers.mjs +0 -187
  74. package/src/plugins/subsystems.mjs +0 -198
  75. package/src/plugins/validators.mjs +0 -142
  76. package/src/sdk-execute.mjs +0 -82
  77. package/src/sdk-install.mjs +0 -187
  78. package/src/sdk-settings.mjs +0 -88
  79. package/src/sdk.mjs +0 -163
  80. package/src/settings/load.mjs +0 -134
  81. package/src/settings/paths.mjs +0 -101
  82. package/src/skills/builtin/building-skills/SKILL.md +0 -20
  83. package/src/skills/discover.mjs +0 -95
  84. package/src/threads/store.mjs +0 -176
  85. package/src/tools/builtin/bash.mjs +0 -110
  86. package/src/tools/builtin/create-file.mjs +0 -66
  87. package/src/tools/builtin/edit-file.mjs +0 -76
  88. package/src/tools/builtin/finder.mjs +0 -73
  89. package/src/tools/builtin/glob.mjs +0 -74
  90. package/src/tools/builtin/grep.mjs +0 -82
  91. package/src/tools/builtin/index.mjs +0 -83
  92. package/src/tools/builtin/librarian.mjs +0 -97
  93. package/src/tools/builtin/look-at.mjs +0 -92
  94. package/src/tools/builtin/mcp.mjs +0 -51
  95. package/src/tools/builtin/mermaid.mjs +0 -59
  96. package/src/tools/builtin/oracle.mjs +0 -56
  97. package/src/tools/builtin/painter.mjs +0 -81
  98. package/src/tools/builtin/plugin-tool.mjs +0 -53
  99. package/src/tools/builtin/read-mcp-resource.mjs +0 -63
  100. package/src/tools/builtin/read-web-page.mjs +0 -72
  101. package/src/tools/builtin/read.mjs +0 -59
  102. package/src/tools/builtin/runtime-content.mjs +0 -31
  103. package/src/tools/builtin/runtime-decisions.mjs +0 -115
  104. package/src/tools/builtin/runtime.mjs +0 -85
  105. package/src/tools/builtin/task.mjs +0 -63
  106. package/src/tools/builtin/toolbox-tool.mjs +0 -57
  107. package/src/tools/builtin/undo-edit.mjs +0 -97
  108. package/src/tools/builtin/web-search.mjs +0 -128
  109. package/src/tools/toolbox.mjs +0 -273
  110. package/src/util/fs.mjs +0 -13
  111. package/src/util/glob.mjs +0 -46
  112. package/src/util/html.mjs +0 -21
  113. package/src/util/media.mjs +0 -13
  114. package/src/util/shell.mjs +0 -24
  115. package/src/util/table.mjs +0 -11
@@ -1,904 +0,0 @@
1
- # Coven Code Panel TUI Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Make bare `coven-code` open a keyboard-first panel TUI while preserving execute mode, stream-json mode, command subcommands, and the classic readline REPL escape hatch.
6
-
7
- **Architecture:** Extract the existing interactive command behavior from `src/cli/repl.mjs` into `src/cli/interactive-core.mjs`, then have both the classic REPL and new TUI call that shared core. Add `src/cli/tui.mjs` as a full-screen terminal UI controller with a transcript, status rail, tabs, composer, command palette, and deterministic test hooks.
8
-
9
- **Tech Stack:** Node.js ESM, Node built-in test runner, existing `runExecute` command engine, ANSI terminal control via Node stdlib for the first TUI pass.
10
-
11
- ---
12
-
13
- ## File Structure
14
-
15
- - Modify: `src/cli/repl.mjs`
16
- - Keep classic readline mode.
17
- - Delegate slash commands, prompt turns, history, editor, and thread helpers to shared interactive-core helpers.
18
- - Create: `src/cli/interactive-core.mjs`
19
- - Own reusable interactive state and command handling.
20
- - Export `createInteractiveSession`, `handleInteractiveInput`, `printSlashHelp`, `readEditorPrompt`, `interactiveContinuationThread`, and `runInteractiveTurn`.
21
- - Create: `src/cli/tui.mjs`
22
- - Own TUI rendering, keyboard handling, command palette state, status rail, tab state, and fallback-safe startup.
23
- - Export `runTuiInteractive`, `createTuiModel`, `renderTuiFrame`, and `handleTuiKey`.
24
- - Modify: `src/main.mjs`
25
- - Route bare TTY sessions to `runTuiInteractive`.
26
- - Route `COVEN_CODE_REPL=1 coven-code` to `runInteractive`.
27
- - Preserve execute and command dispatch behavior.
28
- - Modify: `test/cli.test.mjs`
29
- - Add tests for interactive routing, classic REPL escape hatch, core slash command behavior, TUI model behavior, and command compatibility.
30
- - Modify: `README.md`
31
- - Document the TUI default and classic REPL escape hatch.
32
- - Modify: `docs/CLI.md`
33
- - Document interactive TUI behavior and unchanged noninteractive behavior.
34
- - Modify: `docs/DEVELOPMENT.md`
35
- - Document TUI test entry points and manual smoke command.
36
-
37
- ## Task 1: Extract Shared Interactive Core
38
-
39
- **Files:**
40
- - Create: `src/cli/interactive-core.mjs`
41
- - Modify: `src/cli/repl.mjs`
42
- - Test: `test/cli.test.mjs`
43
-
44
- - [ ] **Step 1: Write failing tests for shared interactive state**
45
-
46
- Append these tests to `test/cli.test.mjs`:
47
-
48
- ```js
49
- test('interactive core handles mode, reasoning, queue, and new thread slash commands', async () => {
50
- const { createInteractiveSession, handleInteractiveInput } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'interactive-core.mjs')));
51
- const parsed = { mode: 'smart', reasoningEffort: undefined };
52
- const session = createInteractiveSession(parsed);
53
-
54
- const mode = await handleInteractiveInput(session, '/mode deep');
55
- assert.equal(mode.kind, 'command');
56
- assert.deepEqual(mode.lines, ['mode: deep', 'reasoning effort: medium']);
57
- assert.equal(parsed.mode, 'deep');
58
- assert.equal(parsed.reasoningEffort, 'medium');
59
-
60
- const reasoning = await handleInteractiveInput(session, '/reasoning next');
61
- assert.equal(reasoning.kind, 'command');
62
- assert.deepEqual(reasoning.lines, ['reasoning effort: high']);
63
- assert.equal(parsed.reasoningEffort, 'high');
64
-
65
- const queued = await handleInteractiveInput(session, '/queue follow up');
66
- assert.equal(queued.kind, 'command');
67
- assert.deepEqual(queued.lines, ['queued: follow up']);
68
- assert.deepEqual(session.queuedMessages, ['follow up']);
69
-
70
- session.thread = { id: 'T-test', messages: [{ role: 'user', content: 'hi' }] };
71
- const fresh = await handleInteractiveInput(session, '/new');
72
- assert.equal(fresh.kind, 'command');
73
- assert.deepEqual(fresh.lines, ['new thread']);
74
- assert.equal(session.thread, undefined);
75
- });
76
- ```
77
-
78
- - [ ] **Step 2: Run test to verify it fails**
79
-
80
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive core handles mode"`
81
-
82
- Expected: FAIL with a module-not-found error for `src/cli/interactive-core.mjs`.
83
-
84
- - [ ] **Step 3: Create `src/cli/interactive-core.mjs`**
85
-
86
- Move the reusable helpers from `src/cli/repl.mjs` into the new module and include this public API:
87
-
88
- ```js
89
- import { readFileSync } from 'node:fs';
90
- import { spawnSync } from 'node:child_process';
91
- import { appendFile, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
92
- import { tmpdir } from 'node:os';
93
- import path from 'node:path';
94
- import { AGENT_MODES, CLI_NAME, CONFIG_SUBDIR, REPL_HISTORY_LIMIT } from '../constants.mjs';
95
- import { configDir } from '../settings/paths.mjs';
96
- import { shellQuote, splitShellWords } from '../util/shell.mjs';
97
- import { latestActiveThread, requireThread } from '../threads/store.mjs';
98
- import { runCommand } from './dispatch.mjs';
99
- import { runExecute } from './execute.mjs';
100
- import {
101
- coerceReasoningEffortForMode,
102
- nextReasoningEffortForMode,
103
- reasoningEffortForMode,
104
- } from './reasoning.mjs';
105
-
106
- export function createInteractiveSession(parsed, options = {}) {
107
- return {
108
- parsed,
109
- thread: options.thread,
110
- queuedMessages: [],
111
- commandRunner: options.commandRunner ?? runCommand,
112
- executeRunner: options.executeRunner ?? runExecute,
113
- editorReader: options.editorReader ?? readEditorPrompt,
114
- };
115
- }
116
-
117
- export async function handleInteractiveInput(session, text) {
118
- if (!text) return { kind: 'empty', lines: [] };
119
- if (text === '/exit' || text === '/quit') return { kind: 'exit', lines: [] };
120
- if (text === '/help') return { kind: 'help', lines: slashHelpLines() };
121
- if (!text.startsWith('/')) {
122
- session.thread = await runInteractiveTurn(session.parsed, text, session.thread, session.executeRunner);
123
- while (session.queuedMessages.length > 0) {
124
- session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
125
- }
126
- return { kind: 'turn', lines: [] };
127
- }
128
-
129
- const tokens = splitShellWords(text.slice(1));
130
- const [cmd, ...rest] = tokens;
131
- if (!cmd) return { kind: 'empty', lines: [] };
132
-
133
- if (cmd === 'mode') {
134
- const nextMode = rest[0];
135
- if (!nextMode) return { kind: 'command', lines: [`mode: ${session.parsed.mode}`] };
136
- if (!AGENT_MODES.includes(nextMode)) return { kind: 'error', lines: [`${CLI_NAME}: Unknown mode: ${nextMode}`] };
137
- session.parsed.mode = nextMode;
138
- session.parsed.reasoningEffort = coerceReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort);
139
- return { kind: 'command', lines: [`mode: ${session.parsed.mode}`, `reasoning effort: ${session.parsed.reasoningEffort}`] };
140
- }
141
-
142
- if (cmd === 'reasoning') {
143
- try {
144
- const nextEffort = rest[0] === 'next'
145
- ? nextReasoningEffortForMode(session.parsed.mode, session.parsed.reasoningEffort)
146
- : rest[0];
147
- session.parsed.reasoningEffort = reasoningEffortForMode(session.parsed.mode, nextEffort ?? session.parsed.reasoningEffort);
148
- return { kind: 'command', lines: [`reasoning effort: ${session.parsed.reasoningEffort}`] };
149
- } catch (error) {
150
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
151
- }
152
- }
153
-
154
- if (cmd === 'queue') {
155
- const queued = rest.join(' ').trim();
156
- if (!queued) return { kind: 'error', lines: [`${CLI_NAME}: /queue requires a prompt`] };
157
- session.queuedMessages.push(queued);
158
- return { kind: 'command', lines: [`queued: ${queued}`] };
159
- }
160
-
161
- if (cmd === 'new') {
162
- session.thread = undefined;
163
- return { kind: 'command', lines: ['new thread'] };
164
- }
165
-
166
- if (cmd === 'continue') {
167
- try {
168
- session.thread = interactiveContinuationThread(rest[0]);
169
- return { kind: 'command', lines: [`continued: ${session.thread.id}`] };
170
- } catch (error) {
171
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
172
- }
173
- }
174
-
175
- if (cmd === `${CLI_NAME}:` && rest.join(' ') === 'help') return { kind: 'help', lines: slashHelpLines() };
176
-
177
- if (cmd === 'editor') {
178
- const edited = await session.editorReader();
179
- if (edited) {
180
- session.thread = await runInteractiveTurn(session.parsed, edited, session.thread, session.executeRunner);
181
- while (session.queuedMessages.length > 0) {
182
- session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
183
- }
184
- }
185
- return { kind: 'command', lines: [] };
186
- }
187
-
188
- if (cmd === 'edit') {
189
- try {
190
- if (!session.thread) throw new Error('No current thread to edit');
191
- const editTarget = editablePreviousPrompt(session.thread);
192
- const edited = await session.editorReader(editTarget.prompt);
193
- if (edited) {
194
- session.thread.messages = session.thread.messages.slice(0, editTarget.index);
195
- if (session.thread.messages.length === 0) {
196
- session.thread.title = edited.split(/\r?\n/).find(Boolean)?.slice(0, 120) || '(empty prompt)';
197
- }
198
- session.thread = await runInteractiveTurn(session.parsed, edited, session.thread, session.executeRunner);
199
- while (session.queuedMessages.length > 0) {
200
- session.thread = await runInteractiveTurn(session.parsed, session.queuedMessages.shift(), session.thread, session.executeRunner);
201
- }
202
- }
203
- return { kind: 'command', lines: [] };
204
- } catch (error) {
205
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
206
- }
207
- }
208
-
209
- if (cmd === 'thread:' && rest.join(' ') === 'archive and quit') {
210
- try {
211
- if (!session.thread) throw new Error('No current thread to archive');
212
- await session.commandRunner('threads', ['archive', session.thread.id], session.parsed, '');
213
- return { kind: 'exit', lines: [] };
214
- } catch (error) {
215
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
216
- }
217
- }
218
-
219
- if (cmd === 'thread:' && rest[0] === 'set' && rest[1] === 'visibility') {
220
- try {
221
- if (!session.thread) throw new Error('No current thread to update');
222
- await session.commandRunner('threads', ['visibility', session.thread.id, ...rest.slice(2)], session.parsed, '');
223
- return { kind: 'command', lines: [] };
224
- } catch (error) {
225
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
226
- }
227
- }
228
-
229
- if (cmd === 'feedback:' && rest.join(' ') === 'send report with diagnostics') {
230
- try {
231
- if (!session.thread) throw new Error('No current thread to report');
232
- await session.commandRunner('threads', ['report', session.thread.id], session.parsed, '');
233
- return { kind: 'command', lines: [] };
234
- } catch (error) {
235
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
236
- }
237
- }
238
-
239
- try {
240
- if (cmd === 'skill:') await session.commandRunner('skill', rest, session.parsed, '');
241
- else if (cmd === 'plugins:') await session.commandRunner('plugins', rest, session.parsed, '');
242
- else await session.commandRunner(cmd, rest, session.parsed, '');
243
- return { kind: 'command', lines: [] };
244
- } catch (error) {
245
- if (String(error?.message ?? '').startsWith('Unknown command:')) {
246
- try {
247
- await session.commandRunner('plugins', ['run', cmd], session.parsed, '');
248
- return { kind: 'command', lines: [] };
249
- } catch (pluginError) {
250
- if (!String(pluginError?.message ?? '').startsWith('Unknown plugin command:')) throw pluginError;
251
- }
252
- }
253
- return { kind: 'error', lines: [`${CLI_NAME}: ${error?.message ?? error}`] };
254
- }
255
- }
256
-
257
- export function slashHelpLines() {
258
- return [
259
- 'Slash commands:',
260
- ' /exit, /quit leave the REPL',
261
- ' /help show this message',
262
- ` /${CLI_NAME}: help`,
263
- ' show command-palette help',
264
- ' /mode [name] show or set mode: smart, deep, rush, large',
265
- ' /reasoning [level|next]',
266
- ' show, set, or cycle reasoning effort',
267
- ' /new start a fresh thread',
268
- ' /continue [thread-id] continue the latest active thread or a specific thread',
269
- ' /queue <prompt> send a follow-up prompt after the next turn',
270
- ' /editor compose the next prompt in $EDITOR',
271
- ' /edit edit the previous prompt in $EDITOR',
272
- ' /ide connect connect or inspect local IDE integration',
273
- ' /skill: list list installed skills',
274
- ' /plugins: reload reload project and user plugins',
275
- ' /thread: archive and quit',
276
- ' archive the current thread and leave the REPL',
277
- ' /thread: set visibility <level>',
278
- ' set current thread visibility',
279
- ' /feedback: send report with diagnostics',
280
- ' create a diagnostic report for the current thread',
281
- ` /<subcommand> [args] run any top-level ${CLI_NAME} subcommand (e.g. /tools list)`,
282
- 'End a line with `\\` to continue the prompt onto the next line.',
283
- 'Anything else is sent as a one-turn prompt.',
284
- '',
285
- 'Keybindings:',
286
- ' Ctrl+P open the command palette',
287
- ' Ctrl+E open the current prompt in $EDITOR',
288
- ' Ctrl+M switch agent modes',
289
- ' Ctrl+R cycle reasoning effort',
290
- ' Tab cycle TUI tabs',
291
- ' Esc close overlays',
292
- ' @ mention files',
293
- ];
294
- }
295
-
296
- export function printSlashHelp() {
297
- for (const line of slashHelpLines()) console.log(line);
298
- }
299
-
300
- export async function readEditorPrompt(initialText = '') {
301
- const editor = process.env.EDITOR || process.env.VISUAL;
302
- if (!editor) {
303
- console.error(`${CLI_NAME}: /editor requires $EDITOR or $VISUAL`);
304
- return '';
305
- }
306
- const file = path.join(tmpdir(), `${CONFIG_SUBDIR}-prompt-${process.pid}-${Date.now()}.md`);
307
- try {
308
- await writeFile(file, initialText);
309
- const result = spawnSync(`${editor} ${shellQuote(file)}`, {
310
- stdio: 'inherit',
311
- shell: true,
312
- });
313
- if (result.error) {
314
- console.error(`${CLI_NAME}: Unable to run editor: ${result.error.message}`);
315
- return '';
316
- }
317
- if ((result.status ?? 0) !== 0) {
318
- console.error(`${CLI_NAME}: Editor exited with status ${result.status}`);
319
- return '';
320
- }
321
- return (await readFile(file, 'utf8')).trim();
322
- } finally {
323
- await unlink(file).catch(() => {});
324
- }
325
- }
326
-
327
- export function editablePreviousPrompt(thread) {
328
- const index = (thread.messages ?? []).findLastIndex((message) => (
329
- message.role === 'user' && typeof message.content === 'string'
330
- ));
331
- if (index === -1) throw new Error('No previous user prompt to edit');
332
- return { index, prompt: thread.messages[index].content };
333
- }
334
-
335
- export function interactiveContinuationThread(threadId) {
336
- if (threadId) return requireThread(threadId);
337
- const thread = latestActiveThread();
338
- if (!thread) throw new Error('No active thread to continue');
339
- return thread;
340
- }
341
-
342
- export async function runInteractiveTurn(parsed, text, thread, executeRunner = runExecute) {
343
- try {
344
- return await executeRunner(
345
- { ...parsed, execute: true, prompt: text, streamJson: false, streamJsonThinking: false, streamJsonInput: false },
346
- '',
347
- { thread },
348
- );
349
- } catch (error) {
350
- console.error(`${CLI_NAME}: ${error?.message ?? error}`);
351
- return thread;
352
- }
353
- }
354
-
355
- export function replHistoryFile() {
356
- return process.env.COVEN_CODE_REPL_HISTORY_FILE
357
- || path.join(configDir(), CONFIG_SUBDIR, 'repl_history');
358
- }
359
-
360
- export function loadReplHistory() {
361
- if (process.env.COVEN_CODE_REPL_HISTORY === '0') return [];
362
- try {
363
- return readFileSync(replHistoryFile(), 'utf8')
364
- .split(/\r?\n/)
365
- .filter(Boolean)
366
- .slice(-REPL_HISTORY_LIMIT)
367
- .reverse();
368
- } catch {
369
- return [];
370
- }
371
- }
372
-
373
- export async function appendReplHistory(line) {
374
- if (process.env.COVEN_CODE_REPL_HISTORY === '0') return;
375
- try {
376
- const file = replHistoryFile();
377
- await mkdir(path.dirname(file), { recursive: true });
378
- await appendFile(file, `${line}\n`);
379
- } catch {
380
- // history must never break the REPL
381
- }
382
- }
383
- ```
384
-
385
- - [ ] **Step 4: Update `src/cli/repl.mjs` to delegate**
386
-
387
- Replace embedded command handling with `createInteractiveSession` and `handleInteractiveInput`. Keep readline buffering and prompts in `repl.mjs`.
388
-
389
- - [ ] **Step 5: Run test to verify it passes**
390
-
391
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive core handles mode"`
392
-
393
- Expected: PASS.
394
-
395
- - [ ] **Step 6: Commit**
396
-
397
- ```bash
398
- git add src/cli/repl.mjs src/cli/interactive-core.mjs test/cli.test.mjs
399
- git commit -m "refactor: extract coven-code interactive core"
400
- ```
401
-
402
- ## Task 2: Add Interactive Routing
403
-
404
- **Files:**
405
- - Modify: `src/main.mjs`
406
- - Create: `src/cli/tui.mjs`
407
- - Test: `test/cli.test.mjs`
408
-
409
- - [ ] **Step 1: Write failing routing tests**
410
-
411
- Append tests that import `selectInteractiveRunner` from `src/main.mjs`:
412
-
413
- ```js
414
- test('interactive routing chooses tui by default for tty sessions and repl when requested', async () => {
415
- const { selectInteractiveRunner } = await import(pathToFileURL(path.join(repoRoot, 'src', 'main.mjs')));
416
-
417
- assert.equal(selectInteractiveRunner({
418
- stdinIsTTY: true,
419
- stdoutIsTTY: true,
420
- env: {},
421
- }), 'tui');
422
-
423
- assert.equal(selectInteractiveRunner({
424
- stdinIsTTY: true,
425
- stdoutIsTTY: true,
426
- env: { COVEN_CODE_REPL: '1' },
427
- }), 'repl');
428
-
429
- assert.equal(selectInteractiveRunner({
430
- stdinIsTTY: false,
431
- stdoutIsTTY: true,
432
- env: {},
433
- }), 'repl');
434
- });
435
- ```
436
-
437
- - [ ] **Step 2: Run test to verify it fails**
438
-
439
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive routing chooses tui"`
440
-
441
- Expected: FAIL because `selectInteractiveRunner` is not exported.
442
-
443
- - [ ] **Step 3: Add minimal `src/cli/tui.mjs`**
444
-
445
- Create a first stub that delegates to the classic REPL until the real TUI task:
446
-
447
- ```js
448
- import { runInteractive } from './repl.mjs';
449
-
450
- export async function runTuiInteractive(parsed, initialInput = '') {
451
- return runInteractive(parsed, initialInput);
452
- }
453
- ```
454
-
455
- - [ ] **Step 4: Export `selectInteractiveRunner` and wire routing in `src/main.mjs`**
456
-
457
- Add:
458
-
459
- ```js
460
- import { runTuiInteractive } from './cli/tui.mjs';
461
- ```
462
-
463
- Add:
464
-
465
- ```js
466
- export function selectInteractiveRunner({ stdinIsTTY, stdoutIsTTY, env }) {
467
- if (env.COVEN_CODE_REPL === '1') return 'repl';
468
- if (stdinIsTTY && stdoutIsTTY) return 'tui';
469
- return 'repl';
470
- }
471
- ```
472
-
473
- Replace the interactive branch with:
474
-
475
- ```js
476
- if (process.stdout.isTTY && (process.stdin.isTTY || stdin.length > 0)) {
477
- const runner = selectInteractiveRunner({
478
- stdinIsTTY: Boolean(process.stdin.isTTY),
479
- stdoutIsTTY: Boolean(process.stdout.isTTY),
480
- env: process.env,
481
- });
482
- if (runner === 'tui') await runTuiInteractive(parsed, stdin);
483
- else await runInteractive(parsed, stdin);
484
- return;
485
- }
486
- ```
487
-
488
- - [ ] **Step 5: Run test to verify it passes**
489
-
490
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "interactive routing chooses tui"`
491
-
492
- Expected: PASS.
493
-
494
- - [ ] **Step 6: Verify execute mode still works**
495
-
496
- Run: `node ./bin/coven-code.mjs -x "what is 2+2?"`
497
-
498
- Expected: `4`.
499
-
500
- - [ ] **Step 7: Commit**
501
-
502
- ```bash
503
- git add src/main.mjs src/cli/tui.mjs test/cli.test.mjs
504
- git commit -m "feat: route coven-code tty sessions to tui"
505
- ```
506
-
507
- ## Task 3: Build the TUI Model and Renderer
508
-
509
- **Files:**
510
- - Modify: `src/cli/tui.mjs`
511
- - Test: `test/cli.test.mjs`
512
-
513
- - [ ] **Step 1: Write failing model/render tests**
514
-
515
- Append:
516
-
517
- ```js
518
- test('tui model renders panel layout with transcript tabs status rail and composer', async () => {
519
- const { createTuiModel, renderTuiFrame } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'tui.mjs')));
520
-
521
- const model = createTuiModel({
522
- version: '0.0.0-test',
523
- mode: 'smart',
524
- reasoningEffort: 'medium',
525
- threadId: 'T-test',
526
- toolCount: 18,
527
- });
528
- model.transcript.push({ role: 'user', text: 'hello' });
529
- model.composer = 'what is 2+2?';
530
-
531
- const frame = renderTuiFrame(model, { columns: 82, rows: 24, color: false });
532
-
533
- assert.match(frame, /Coven Code 0\.0\.0-test/);
534
- assert.match(frame, /chat\s+tools\s+threads\s+config\s+help/);
535
- assert.match(frame, /you/);
536
- assert.match(frame, /hello/);
537
- assert.match(frame, /thread: T-test/);
538
- assert.match(frame, /mode: smart/);
539
- assert.match(frame, /> what is 2\+2\?/);
540
- });
541
- ```
542
-
543
- - [ ] **Step 2: Run test to verify it fails**
544
-
545
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui model renders"`
546
-
547
- Expected: FAIL because `createTuiModel` and `renderTuiFrame` are not exported.
548
-
549
- - [ ] **Step 3: Implement model and renderer**
550
-
551
- Implement:
552
-
553
- ```js
554
- import { CLI_NAME, VERSION } from '../constants.mjs';
555
- import { createInteractiveSession, handleInteractiveInput, slashHelpLines } from './interactive-core.mjs';
556
-
557
- const TABS = ['chat', 'tools', 'threads', 'config', 'help'];
558
-
559
- export function createTuiModel(options = {}) {
560
- return {
561
- version: options.version ?? VERSION,
562
- mode: options.mode ?? 'smart',
563
- reasoningEffort: options.reasoningEffort ?? 'medium',
564
- threadId: options.threadId ?? 'new thread',
565
- toolCount: options.toolCount ?? 0,
566
- queueCount: options.queueCount ?? 0,
567
- activeTab: options.activeTab ?? 'chat',
568
- paletteOpen: false,
569
- composer: '',
570
- transcript: [],
571
- status: 'idle',
572
- };
573
- }
574
-
575
- export function renderTuiFrame(model, size = {}) {
576
- const columns = Math.max(50, size.columns ?? process.stdout.columns ?? 80);
577
- const rows = Math.max(16, size.rows ?? process.stdout.rows ?? 24);
578
- const width = columns;
579
- const bodyRows = rows - 7;
580
- const divider = '-'.repeat(width);
581
- const tabs = TABS.map((tab) => tab === model.activeTab ? `[${tab}]` : tab).join(' ');
582
- const transcript = renderTranscript(model, Math.max(3, bodyRows - 4), width - 24);
583
- const status = [
584
- `thread: ${model.threadId}`,
585
- `mode: ${model.mode}`,
586
- `reasoning: ${model.reasoningEffort}`,
587
- `queued: ${model.queueCount}`,
588
- `tools: ${model.toolCount}`,
589
- `status: ${model.status}`,
590
- ];
591
- const body = mergeColumns(transcript, status, width);
592
- const lines = [
593
- `Coven Code ${model.version}`.slice(0, width),
594
- tabs.slice(0, width),
595
- divider,
596
- ...body,
597
- divider,
598
- `> ${model.composer}`.slice(0, width),
599
- ];
600
- return lines.slice(0, rows).join('\n');
601
- }
602
-
603
- function renderTranscript(model, limit, width) {
604
- if (model.activeTab === 'help') return slashHelpLines().slice(0, limit).map((line) => line.slice(0, width));
605
- if (model.activeTab !== 'chat') return [`${model.activeTab} panel`, 'Use slash commands or Ctrl-P palette actions.'];
606
- const entries = model.transcript.slice(-limit).flatMap((entry) => [
607
- `${entry.role}:`.slice(0, width),
608
- ...String(entry.text).split(/\r?\n/).map((line) => line.slice(0, width)),
609
- ]);
610
- return entries.length > 0 ? entries.slice(-limit) : ['Ready. Type a prompt or /help.'];
611
- }
612
-
613
- function mergeColumns(leftLines, rightLines, width) {
614
- const rightWidth = Math.min(22, Math.floor(width * 0.32));
615
- const leftWidth = width - rightWidth - 3;
616
- const count = Math.max(leftLines.length, rightLines.length);
617
- const rows = [];
618
- for (let index = 0; index < count; index += 1) {
619
- const left = (leftLines[index] ?? '').padEnd(leftWidth).slice(0, leftWidth);
620
- const right = (rightLines[index] ?? '').slice(0, rightWidth);
621
- rows.push(`${left} | ${right}`);
622
- }
623
- return rows;
624
- }
625
- ```
626
-
627
- - [ ] **Step 4: Run test to verify it passes**
628
-
629
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui model renders"`
630
-
631
- Expected: PASS.
632
-
633
- - [ ] **Step 5: Commit**
634
-
635
- ```bash
636
- git add src/cli/tui.mjs test/cli.test.mjs
637
- git commit -m "feat: add coven-code tui frame renderer"
638
- ```
639
-
640
- ## Task 4: Add Keyboard Handling and Command Palette Actions
641
-
642
- **Files:**
643
- - Modify: `src/cli/tui.mjs`
644
- - Test: `test/cli.test.mjs`
645
-
646
- - [ ] **Step 1: Write failing keyboard tests**
647
-
648
- Append:
649
-
650
- ```js
651
- test('tui key handling cycles tabs and command palette actions reuse interactive commands', async () => {
652
- const { createTuiModel, handleTuiKey } = await import(pathToFileURL(path.join(repoRoot, 'src', 'cli', 'tui.mjs')));
653
- const model = createTuiModel({ mode: 'smart', reasoningEffort: 'medium' });
654
- const session = {
655
- parsed: { mode: 'smart', reasoningEffort: 'medium' },
656
- queuedMessages: [],
657
- commandRunner: async () => {},
658
- executeRunner: async () => undefined,
659
- editorReader: async () => '',
660
- };
661
-
662
- await handleTuiKey(model, session, { name: 'tab' });
663
- assert.equal(model.activeTab, 'tools');
664
-
665
- await handleTuiKey(model, session, { ctrl: true, name: 'p' });
666
- assert.equal(model.paletteOpen, true);
667
-
668
- model.paletteIndex = 0;
669
- await handleTuiKey(model, session, { name: 'enter' });
670
- assert.equal(model.paletteOpen, false);
671
- assert.equal(model.transcript.at(-1).text, 'new thread');
672
-
673
- model.composer = '/mode deep';
674
- await handleTuiKey(model, session, { name: 'enter' });
675
- assert.equal(session.parsed.mode, 'deep');
676
- assert.match(model.transcript.at(-1).text, /mode: deep/);
677
- });
678
- ```
679
-
680
- - [ ] **Step 2: Run test to verify it fails**
681
-
682
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui key handling"`
683
-
684
- Expected: FAIL because `handleTuiKey` is not exported.
685
-
686
- - [ ] **Step 3: Implement `handleTuiKey` and palette actions**
687
-
688
- Add:
689
-
690
- ```js
691
- const PALETTE_ACTIONS = [
692
- ['New thread', '/new'],
693
- ['Continue latest thread', '/continue'],
694
- ['Open help', '/help'],
695
- ['List tools', '/tools list'],
696
- ['List skills', '/skill: list'],
697
- ['List plugins', '/plugins: list'],
698
- ];
699
-
700
- export async function handleTuiKey(model, session, key) {
701
- if (key?.name === 'tab') {
702
- const index = TABS.indexOf(model.activeTab);
703
- model.activeTab = TABS[(index + 1) % TABS.length];
704
- return;
705
- }
706
- if (key?.ctrl && key.name === 'p') {
707
- model.paletteOpen = true;
708
- model.paletteIndex = 0;
709
- return;
710
- }
711
- if (model.paletteOpen && key?.name === 'enter') {
712
- const [, command] = PALETTE_ACTIONS[model.paletteIndex ?? 0];
713
- model.paletteOpen = false;
714
- await submitTuiText(model, session, command);
715
- return;
716
- }
717
- if (key?.ctrl && key.name === 'n') {
718
- await submitTuiText(model, session, '/new');
719
- return;
720
- }
721
- if (key?.ctrl && key.name === 'r') {
722
- await submitTuiText(model, session, '/reasoning next');
723
- return;
724
- }
725
- if (key?.ctrl && key.name === 'm') {
726
- const next = session.parsed.mode === 'smart' ? 'deep' : session.parsed.mode === 'deep' ? 'rush' : 'smart';
727
- await submitTuiText(model, session, `/mode ${next}`);
728
- return;
729
- }
730
- if (key?.name === 'enter') {
731
- const text = model.composer.trim();
732
- model.composer = '';
733
- await submitTuiText(model, session, text);
734
- }
735
- }
736
-
737
- async function submitTuiText(model, session, text) {
738
- if (!text) return;
739
- model.transcript.push({ role: 'you', text });
740
- model.status = 'running';
741
- const result = await handleInteractiveInput(session, text);
742
- model.mode = session.parsed.mode;
743
- model.reasoningEffort = session.parsed.reasoningEffort ?? model.reasoningEffort;
744
- model.threadId = session.thread?.id ?? 'new thread';
745
- model.queueCount = session.queuedMessages.length;
746
- if (result.lines.length > 0) model.transcript.push({ role: result.kind === 'error' ? 'error' : 'coven', text: result.lines.join('\n') });
747
- model.status = result.kind === 'exit' ? 'done' : 'idle';
748
- }
749
- ```
750
-
751
- - [ ] **Step 4: Run test to verify it passes**
752
-
753
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui key handling"`
754
-
755
- Expected: PASS.
756
-
757
- - [ ] **Step 5: Commit**
758
-
759
- ```bash
760
- git add src/cli/tui.mjs test/cli.test.mjs
761
- git commit -m "feat: add coven-code tui keyboard actions"
762
- ```
763
-
764
- ## Task 5: Run the Full-Screen TUI Loop
765
-
766
- **Files:**
767
- - Modify: `src/cli/tui.mjs`
768
- - Test: `test/cli.test.mjs`
769
-
770
- - [ ] **Step 1: Write failing smoke test for scripted TUI input**
771
-
772
- Append:
773
-
774
- ```js
775
- test('tui scripted smoke processes input and exits without changing execute mode', async () => {
776
- const result = runCovenCode([], {
777
- input: '/mode deep\n/exit\n',
778
- env: {
779
- COVEN_CODE_TUI_SCRIPTED: '1',
780
- COVEN_CODE_REPL_HISTORY: '0',
781
- },
782
- });
783
-
784
- assert.equal(result.status, 0, result.stderr);
785
- assert.match(result.stdout, /Coven Code/);
786
- assert.match(result.stdout, /mode: deep/);
787
-
788
- const execute = runCovenCode(['-x', 'what is 2+2?']);
789
- assert.equal(execute.status, 0, execute.stderr);
790
- assert.equal(execute.stdout.trim(), '4');
791
- });
792
- ```
793
-
794
- - [ ] **Step 2: Run test to verify it fails**
795
-
796
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui scripted smoke"`
797
-
798
- Expected: FAIL because scripted TUI mode is not implemented.
799
-
800
- - [ ] **Step 3: Implement scripted and interactive TUI loops**
801
-
802
- In `runTuiInteractive`:
803
-
804
- - Create `session = createInteractiveSession(parsed)`.
805
- - Create `model = createTuiModel(...)`.
806
- - If `COVEN_CODE_TUI_SCRIPTED=1`, read newline-separated commands from `initialInput`, call `submitTuiText`, print `renderTuiFrame`, and return.
807
- - For real TTY mode, use `readline.emitKeypressEvents(process.stdin)`, raw mode when available, clear/redraw the frame with ANSI `\x1b[2J\x1b[H`, and dispatch keys to `handleTuiKey`.
808
- - On `Ctrl-C`, restore raw mode and exit.
809
-
810
- - [ ] **Step 4: Run test to verify it passes**
811
-
812
- Run: `npm test -- test/cli.test.mjs --test-name-pattern "tui scripted smoke"`
813
-
814
- Expected: PASS.
815
-
816
- - [ ] **Step 5: Manual smoke in Terminal**
817
-
818
- Run in Terminal:
819
-
820
- ```bash
821
- cd /Users/buns/Documents/GitHub/OpenCoven/coven-code
822
- COVEN_CODE_REPL_HISTORY=0 COVEN_CODE_SKIP_UPDATE_CHECK=1 npm run coven-code
823
- ```
824
-
825
- Expected: full-screen TUI opens, shows header/tabs/transcript/status/composer, `/exit` leaves cleanly.
826
-
827
- - [ ] **Step 6: Commit**
828
-
829
- ```bash
830
- git add src/cli/tui.mjs test/cli.test.mjs
831
- git commit -m "feat: run coven-code panel tui"
832
- ```
833
-
834
- ## Task 6: Update Docs and Final Verification
835
-
836
- **Files:**
837
- - Modify: `README.md`
838
- - Modify: `docs/CLI.md`
839
- - Modify: `docs/DEVELOPMENT.md`
840
-
841
- - [ ] **Step 1: Write docs updates**
842
-
843
- Document:
844
-
845
- - Bare `coven-code` opens the panel TUI.
846
- - `COVEN_CODE_REPL=1 coven-code` opens the classic readline REPL.
847
- - `COVEN_CODE_TUI_SCRIPTED=1` is only for tests/automation.
848
- - `-x`, `--stream-json`, stdin piping, and subcommands are unchanged.
849
-
850
- - [ ] **Step 2: Verify docs diff**
851
-
852
- Run: `git diff -- README.md docs/CLI.md docs/DEVELOPMENT.md`
853
-
854
- Expected: only TUI-related documentation changes.
855
-
856
- - [ ] **Step 3: Run targeted checks**
857
-
858
- Run:
859
-
860
- ```bash
861
- git diff --check
862
- node ./bin/coven-code.mjs --help
863
- node ./bin/coven-code.mjs -x "what is 2+2?"
864
- npm test
865
- ```
866
-
867
- Expected:
868
-
869
- - `git diff --check` exits 0
870
- - help renders
871
- - execute mode prints `4`
872
- - tests pass with 0 failures
873
-
874
- - [ ] **Step 4: Commit**
875
-
876
- ```bash
877
- git add README.md docs/CLI.md docs/DEVELOPMENT.md
878
- git commit -m "docs: document coven-code panel tui"
879
- ```
880
-
881
- ## Final Verification
882
-
883
- Run:
884
-
885
- ```bash
886
- git diff --check
887
- node ./bin/coven-code.mjs --help
888
- node ./bin/coven-code.mjs -x "what is 2+2?"
889
- COVEN_CODE_TUI_SCRIPTED=1 COVEN_CODE_REPL_HISTORY=0 node ./bin/coven-code.mjs <<'EOF'
890
- /mode deep
891
- /exit
892
- EOF
893
- npm test
894
- git status --short --branch
895
- ```
896
-
897
- Expected:
898
-
899
- - whitespace check passes
900
- - help renders
901
- - execute mode prints `4`
902
- - scripted TUI output includes `Coven Code` and `mode: deep`
903
- - tests pass with 0 failures
904
- - worktree is clean except being ahead by local commits