codehere 0.3.0-rc.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/domain/services/codebase-detector.js +1 -1
- package/dist/index.js +42 -5
- package/dist/infrastructure/hooks/session-analyzer.js +14 -5
- package/dist/infrastructure/orchestration/conductor-adapter.js +16 -5
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -3
- package/dist/infrastructure/protocols/mcp-server-impl.js +33 -3
- package/dist/infrastructure/security/file-collection.js +3 -0
- package/dist/presentation/cli/commands/report-command.js +56 -11
- package/dist/presentation/cli/commands/status-command.js +32 -10
- package/dist/presentation/cli/repl.js +651 -198
- package/dist/ui.js +0 -3
- package/dist/utils/typo-guard.js +73 -0
- package/dist/utils/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,161 +1,686 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interactive REPL
|
|
2
|
+
* Interactive REPL · raw-mode live command palette.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Faithful port of v0.2.2's session.ts (commit f4828c2 · "REPL fixes,
|
|
5
|
+
* Palette Enter completes selected command, stdin multi-char chunk
|
|
6
|
+
* processing"). Same banner with state badges, same `> ` prompt with
|
|
7
|
+
* brand color, same filter-as-you-type palette under the prompt, same
|
|
8
|
+
* escape-buffering keystroke handler, same Tab/Enter complete, same
|
|
9
|
+
* arrow-key palette navigation when palette is visible / history when not.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* to it in the terminal, not be forced to a web UI. Both surfaces, both
|
|
16
|
-
* first-class.
|
|
11
|
+
* Backends adapted to v0.3 (chat/edit/review/planner removed in D7):
|
|
12
|
+
* - Task slash commands (ask, explain, fix, review, plan, test, refactor,
|
|
13
|
+
* orchestrate, run) → spawn via Conductor
|
|
14
|
+
* - Subcommand slash (trust, report, hook, status, health, serve, setup,
|
|
15
|
+
* config) → invoke `node dist/index.js <cmd>` inline
|
|
16
|
+
* - /undo → "queued for v0.5 (F3 memory layer)"
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* - Read a line → route via Conductor → spawn specialist → stream events
|
|
20
|
-
* to stdout → await completion → next prompt
|
|
21
|
-
* - `/agents` lists installed specialists and the routing rules
|
|
22
|
-
* - `/status` shows recent audit activity
|
|
23
|
-
* - `/quit` (or Ctrl+C / Ctrl+D) exits cleanly
|
|
18
|
+
* Citing f4828c2 for lineage. Don't redesign.
|
|
24
19
|
*/
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
20
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
21
|
+
import { join, basename } from 'path';
|
|
22
|
+
import { spawn as childSpawn } from 'child_process';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { dirname } from 'path';
|
|
25
|
+
import { colors, success, error, warning, newline, createCodehereLogo } from '../../ui.js';
|
|
27
26
|
import { getVersion } from '../../utils/version.js';
|
|
28
27
|
import { SessionManager, availableAgents } from '../../infrastructure/orchestration/session-manager.js';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
28
|
+
import { collectCodeFiles } from '../../infrastructure/security/file-collection.js';
|
|
29
|
+
import chalk from 'chalk';
|
|
30
|
+
const brandBlue = chalk.rgb(74, 144, 226);
|
|
31
|
+
const stdin = process.stdin;
|
|
32
|
+
const stdout = process.stdout;
|
|
33
|
+
const SLASH_COMMANDS = [
|
|
34
|
+
// Tasks · delegate via Conductor
|
|
35
|
+
{
|
|
36
|
+
name: 'ask',
|
|
37
|
+
args: '<question>',
|
|
38
|
+
description: 'Ask anything about your codebase',
|
|
39
|
+
kind: 'task',
|
|
40
|
+
promptTemplate: (a) => a,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'explain',
|
|
44
|
+
args: '<file>',
|
|
45
|
+
description: 'Explain a file',
|
|
46
|
+
kind: 'task',
|
|
47
|
+
promptTemplate: (a) => `Explain this file in plain English: ${a}`,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'fix',
|
|
51
|
+
args: '<file> <hint>',
|
|
52
|
+
description: 'Apply a targeted fix',
|
|
53
|
+
kind: 'task',
|
|
54
|
+
promptTemplate: (a) => {
|
|
55
|
+
const parts = a.split(/\s+/);
|
|
56
|
+
const file = parts[0];
|
|
57
|
+
const hint = parts.slice(1).join(' ');
|
|
58
|
+
return `fix ${file}: ${hint}`;
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'review',
|
|
63
|
+
args: '<file>',
|
|
64
|
+
description: 'Code review with findings',
|
|
65
|
+
kind: 'task',
|
|
66
|
+
promptTemplate: (a) => `Review ${a} for bugs, security issues, and risks. Concrete findings only.`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'plan',
|
|
70
|
+
args: '<goal>',
|
|
71
|
+
description: 'Break a goal into steps',
|
|
72
|
+
kind: 'task',
|
|
73
|
+
promptTemplate: (a) => `Plan: ${a}. Break into concrete steps before any execution.`,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'test',
|
|
77
|
+
args: '<file>',
|
|
78
|
+
description: 'Generate tests for a file',
|
|
79
|
+
kind: 'task',
|
|
80
|
+
promptTemplate: (a) => `add tests for ${a}. Cover edge cases and likely failure modes.`,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'refactor',
|
|
84
|
+
args: '<pattern> <instr>',
|
|
85
|
+
description: 'Refactor across files',
|
|
86
|
+
kind: 'task',
|
|
87
|
+
promptTemplate: (a) => `refactor: ${a}`,
|
|
88
|
+
},
|
|
89
|
+
// Subcommands · run an existing CLI subcommand inline
|
|
90
|
+
{
|
|
91
|
+
name: 'trust',
|
|
92
|
+
args: '[path]',
|
|
93
|
+
description: 'Scan for vulnerabilities',
|
|
94
|
+
kind: 'sub',
|
|
95
|
+
noKey: true,
|
|
96
|
+
subcommand: 'trust',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'report',
|
|
100
|
+
args: '[path]',
|
|
101
|
+
description: 'Security audit report',
|
|
102
|
+
kind: 'sub',
|
|
103
|
+
noKey: true,
|
|
104
|
+
subcommand: 'report',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'hook',
|
|
108
|
+
args: '[install|status]',
|
|
109
|
+
description: 'Safety hook management',
|
|
110
|
+
kind: 'sub',
|
|
111
|
+
noKey: true,
|
|
112
|
+
subcommand: 'hook',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'status',
|
|
116
|
+
args: '',
|
|
117
|
+
description: 'Dashboard — hook activity',
|
|
118
|
+
kind: 'sub',
|
|
119
|
+
noKey: true,
|
|
120
|
+
subcommand: 'status',
|
|
121
|
+
},
|
|
122
|
+
{ name: 'health', args: '', description: 'System health check', kind: 'sub', noKey: true, subcommand: 'health' },
|
|
123
|
+
{
|
|
124
|
+
name: 'setup',
|
|
125
|
+
args: '',
|
|
126
|
+
description: 'Configure first-run preferences',
|
|
127
|
+
kind: 'sub',
|
|
128
|
+
noKey: true,
|
|
129
|
+
subcommand: 'setup',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'config',
|
|
133
|
+
args: '[key] [value]',
|
|
134
|
+
description: 'Manage preferences',
|
|
135
|
+
kind: 'sub',
|
|
136
|
+
noKey: true,
|
|
137
|
+
subcommand: 'config',
|
|
138
|
+
},
|
|
139
|
+
{ name: 'agents', args: '', description: 'List installed specialists', kind: 'session', noKey: true },
|
|
140
|
+
{ name: 'undo', args: '<file>', description: 'Undo file changes (queued for v0.5)', kind: 'session', noKey: true },
|
|
141
|
+
{ name: 'history', args: '', description: 'Show command history', kind: 'session', noKey: true },
|
|
142
|
+
// Helpers
|
|
143
|
+
{ name: 'help', args: '', description: 'Command reference', kind: 'session', noKey: true },
|
|
144
|
+
{ name: 'clear', args: '', description: 'Clear the screen', kind: 'session', noKey: true },
|
|
145
|
+
{ name: 'exit', args: '', description: 'Leave the session', kind: 'session', noKey: true },
|
|
146
|
+
];
|
|
147
|
+
const PALETTE_CMDS = SLASH_COMMANDS.map((c) => ({
|
|
148
|
+
sig: `/${c.name}${c.args ? ' ' + c.args : ''}`,
|
|
149
|
+
name: c.name,
|
|
150
|
+
desc: c.description,
|
|
151
|
+
noKey: c.noKey,
|
|
152
|
+
}));
|
|
153
|
+
function maxPalette() {
|
|
154
|
+
const rows = process.stdout.rows || 24;
|
|
155
|
+
return Math.min(rows - 8, 20);
|
|
156
|
+
}
|
|
157
|
+
// ── State detection (badges in welcome) ──────────────────────────────────
|
|
158
|
+
function detectState() {
|
|
159
|
+
const cwd = process.cwd();
|
|
160
|
+
const name = basename(cwd);
|
|
161
|
+
const settingsPath = join(process.env.HOME || '', '.claude', 'settings.json');
|
|
162
|
+
let hasHooks = false;
|
|
163
|
+
try {
|
|
164
|
+
hasHooks = existsSync(settingsPath) && readFileSync(settingsPath, 'utf-8').includes('codehere');
|
|
54
165
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
166
|
+
catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
const hasAnyAgent = availableAgents().some((a) => a.installed && a.id !== 'conductor');
|
|
170
|
+
const isGitRepo = existsSync(join(cwd, '.git'));
|
|
171
|
+
let fileCount = 0;
|
|
172
|
+
let scannableCount = 0;
|
|
173
|
+
try {
|
|
174
|
+
const count = (dir, d) => {
|
|
175
|
+
if (d > 3)
|
|
176
|
+
return 0;
|
|
177
|
+
let n = 0;
|
|
178
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
179
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist')
|
|
180
|
+
continue;
|
|
181
|
+
if (e.isDirectory())
|
|
182
|
+
n += count(join(dir, e.name), d + 1);
|
|
183
|
+
else
|
|
184
|
+
n++;
|
|
185
|
+
}
|
|
186
|
+
return n;
|
|
187
|
+
};
|
|
188
|
+
fileCount = count(cwd, 0);
|
|
189
|
+
// rc.21: surface the scannable count — code files that trust/scan
|
|
190
|
+
// would actually look at. Honest count beats a single inflated number.
|
|
191
|
+
// UX_OBSERVATIONS P3. Static ESM import (rc.20 used `require()` which is
|
|
192
|
+
// undefined in this package's ESM runtime — Codex caught the silent fail).
|
|
193
|
+
try {
|
|
194
|
+
scannableCount = collectCodeFiles(cwd).length;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* ignore — banner stays silent rather than crashes */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* ignore */
|
|
202
|
+
}
|
|
203
|
+
return { name, hasHooks, hasAgents: hasAnyAgent, isGitRepo, fileCount, scannableCount };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Format the banner's "X files (Y scannable)" line.
|
|
207
|
+
*
|
|
208
|
+
* Pure function (no I/O) so the rc.20 silent-fail regression that Codex
|
|
209
|
+
* caught — banner falling back to total-only when scannable load fails —
|
|
210
|
+
* has a tight unit test.
|
|
211
|
+
*
|
|
212
|
+
* Contract:
|
|
213
|
+
* - scannable > 0 AND scannable < total → "<total> files (<scannable> scannable)"
|
|
214
|
+
* - otherwise → "<total> files"
|
|
215
|
+
*
|
|
216
|
+
* Exported for test/cli/rc15-fixes.test.ts banner integration test.
|
|
217
|
+
*/
|
|
218
|
+
export function formatFileLine(total, scannable) {
|
|
219
|
+
if (scannable > 0 && scannable < total) {
|
|
220
|
+
return `${total} files (${scannable} scannable)`;
|
|
221
|
+
}
|
|
222
|
+
return `${total} files`;
|
|
223
|
+
}
|
|
224
|
+
export function buildWelcomeLines(state, version) {
|
|
225
|
+
const logo = createCodehereLogo();
|
|
226
|
+
const ver = colors.dim(`v${version}`);
|
|
227
|
+
const badges = [
|
|
228
|
+
state.hasHooks ? colors.green('● hooks') : colors.dim('○ hooks'),
|
|
229
|
+
state.hasAgents ? colors.green('● agents') : colors.dim('○ agents'),
|
|
230
|
+
];
|
|
231
|
+
const lines = [
|
|
232
|
+
'',
|
|
233
|
+
` ${logo} ${ver}`,
|
|
234
|
+
'',
|
|
235
|
+
' ' + badges.join(' '),
|
|
236
|
+
'',
|
|
237
|
+
colors.dim(` ${state.name}`) +
|
|
238
|
+
colors.dim(` · ${state.isGitRepo ? 'git' : 'dir'} · ${formatFileLine(state.fileCount, state.scannableCount)}`),
|
|
239
|
+
'',
|
|
240
|
+
];
|
|
241
|
+
if (!state.hasHooks) {
|
|
242
|
+
lines.push(' ' + colors.yellow('→') + ' ' + colors.cyan('/hook install') + colors.dim(' to activate safety scanning'));
|
|
243
|
+
}
|
|
244
|
+
else if (!state.hasAgents) {
|
|
245
|
+
lines.push(' ' +
|
|
246
|
+
colors.yellow('→') +
|
|
247
|
+
' Install ' +
|
|
248
|
+
colors.cyan('claude-code') +
|
|
249
|
+
colors.dim(' / ') +
|
|
250
|
+
colors.cyan('codex') +
|
|
251
|
+
colors.dim(' / aider / opencode to delegate tasks'));
|
|
252
|
+
}
|
|
253
|
+
lines.push(colors.dim(' Type / for commands, or ask anything'));
|
|
254
|
+
lines.push('');
|
|
255
|
+
return lines;
|
|
256
|
+
}
|
|
257
|
+
function printWelcome() {
|
|
258
|
+
const state = detectState();
|
|
259
|
+
for (const line of buildWelcomeLines(state, getVersion())) {
|
|
260
|
+
console.log(line);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// ── Main session ─────────────────────────────────────────────────────────
|
|
264
|
+
export async function runInteractiveRepl() {
|
|
265
|
+
printWelcome();
|
|
266
|
+
if (stdin.isTTY)
|
|
267
|
+
stdin.setRawMode(true);
|
|
268
|
+
stdin.resume();
|
|
269
|
+
stdin.setEncoding('utf8');
|
|
270
|
+
const state = { history: [] };
|
|
271
|
+
const historyBuf = [];
|
|
272
|
+
let historyIdx = -1;
|
|
273
|
+
let line = '';
|
|
274
|
+
let selected = 0;
|
|
275
|
+
let pLines = 0;
|
|
276
|
+
let processing = false;
|
|
277
|
+
let escBuf = '';
|
|
278
|
+
let escTimer = null;
|
|
279
|
+
const cols = () => stdout.columns || 80;
|
|
280
|
+
function truncate(s, maxWidth) {
|
|
281
|
+
// eslint-disable-next-line no-control-regex -- ANSI escape stripping requires the literal ESC byte
|
|
282
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
283
|
+
if (visible.length <= maxWidth)
|
|
284
|
+
return s;
|
|
285
|
+
let vis = 0;
|
|
286
|
+
let cut = 0;
|
|
287
|
+
for (let i = 0; i < s.length; i++) {
|
|
288
|
+
if (s[i] === '\x1b') {
|
|
289
|
+
const end = s.indexOf('m', i);
|
|
290
|
+
if (end !== -1) {
|
|
291
|
+
i = end;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
vis++;
|
|
296
|
+
if (vis >= maxWidth - 1) {
|
|
297
|
+
cut = i + 1;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return cut > 0 ? s.slice(0, cut) + '\x1b[0m' : s;
|
|
302
|
+
}
|
|
303
|
+
function render() {
|
|
304
|
+
stdout.write('\r\x1b[J');
|
|
305
|
+
stdout.write(brandBlue('> ') + line);
|
|
306
|
+
if (line.startsWith('/') && !line.includes(' ')) {
|
|
307
|
+
const filter = line.slice(1).toLowerCase();
|
|
308
|
+
const allMatching = PALETTE_CMDS.filter((c) => c.name.startsWith(filter));
|
|
309
|
+
const limit = maxPalette();
|
|
310
|
+
const filtered = allMatching.slice(0, limit);
|
|
311
|
+
const overflow = allMatching.length - filtered.length;
|
|
312
|
+
if (filtered.length > 0) {
|
|
313
|
+
const maxLen = Math.max(...filtered.map((c) => c.sig.length));
|
|
314
|
+
const w = cols();
|
|
315
|
+
let drawnLines = 0;
|
|
316
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
317
|
+
const c = filtered[i];
|
|
318
|
+
const pad = ' '.repeat(Math.max(1, maxLen - c.sig.length + 2));
|
|
319
|
+
const badge = c.noKey ? colors.dim(' (free)') : '';
|
|
320
|
+
const prefix = i === selected ? brandBlue('▸') : ' ';
|
|
321
|
+
const text = i === selected ? brandBlue(c.sig) : c.sig;
|
|
322
|
+
const row = ` ${prefix} ${text}${pad}${colors.dim(c.desc)}${badge}`;
|
|
323
|
+
stdout.write('\n' + truncate(row, w));
|
|
324
|
+
drawnLines++;
|
|
325
|
+
}
|
|
326
|
+
if (overflow > 0) {
|
|
327
|
+
stdout.write('\n' + colors.dim(` ... +${overflow} more (type to filter)`));
|
|
328
|
+
drawnLines++;
|
|
329
|
+
}
|
|
330
|
+
stdout.write(`\x1b[${drawnLines}A`);
|
|
331
|
+
stdout.write(`\x1b[${line.length + 3}G`);
|
|
332
|
+
pLines = drawnLines;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
stdout.write('\n' + colors.dim(' No matching commands'));
|
|
336
|
+
stdout.write('\x1b[1A');
|
|
337
|
+
stdout.write(`\x1b[${line.length + 3}G`);
|
|
338
|
+
pLines = 1;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
pLines = 0;
|
|
82
343
|
}
|
|
83
|
-
|
|
84
|
-
|
|
344
|
+
}
|
|
345
|
+
function handle(key) {
|
|
346
|
+
if (processing)
|
|
347
|
+
return;
|
|
348
|
+
// Ctrl+C
|
|
349
|
+
if (key === '\x03') {
|
|
350
|
+
if (line.length > 0) {
|
|
351
|
+
line = '';
|
|
352
|
+
selected = 0;
|
|
353
|
+
stdout.write('\r\x1b[J\n');
|
|
354
|
+
render();
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
stdout.write('\r\x1b[J\n');
|
|
358
|
+
process.exit(0);
|
|
359
|
+
}
|
|
85
360
|
return;
|
|
86
361
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
362
|
+
// Ctrl+D
|
|
363
|
+
if (key === '\x04') {
|
|
364
|
+
stdout.write('\r\x1b[J\n' + colors.dim(' See you.') + '\n');
|
|
365
|
+
process.exit(0);
|
|
91
366
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
367
|
+
// Ctrl+L
|
|
368
|
+
if (key === '\x0c') {
|
|
369
|
+
line = '';
|
|
370
|
+
selected = 0;
|
|
371
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
372
|
+
printWelcome();
|
|
373
|
+
render();
|
|
374
|
+
return;
|
|
96
375
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
376
|
+
// Arrow up
|
|
377
|
+
if (key === '\x1b[A') {
|
|
378
|
+
if (pLines > 0) {
|
|
379
|
+
selected = Math.max(0, selected - 1);
|
|
380
|
+
}
|
|
381
|
+
else if (historyBuf.length > 0 && historyIdx < historyBuf.length - 1) {
|
|
382
|
+
historyIdx++;
|
|
383
|
+
line = historyBuf[historyIdx];
|
|
384
|
+
}
|
|
385
|
+
render();
|
|
386
|
+
return;
|
|
101
387
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
388
|
+
// Arrow down
|
|
389
|
+
if (key === '\x1b[B') {
|
|
390
|
+
if (pLines > 0) {
|
|
391
|
+
selected = Math.min(pLines - 1, selected + 1);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
if (historyIdx > 0) {
|
|
395
|
+
historyIdx--;
|
|
396
|
+
line = historyBuf[historyIdx];
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
historyIdx = -1;
|
|
400
|
+
line = '';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
render();
|
|
404
|
+
return;
|
|
107
405
|
}
|
|
108
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
406
|
+
// Tab
|
|
407
|
+
if (key === '\t' && pLines > 0) {
|
|
408
|
+
const filter = line.slice(1).toLowerCase();
|
|
409
|
+
const filtered = PALETTE_CMDS.filter((c) => c.name.startsWith(filter)).slice(0, maxPalette());
|
|
410
|
+
if (filtered[selected]) {
|
|
411
|
+
line = `/${filtered[selected].name} `;
|
|
412
|
+
selected = 0;
|
|
413
|
+
}
|
|
414
|
+
render();
|
|
415
|
+
return;
|
|
113
416
|
}
|
|
114
|
-
|
|
115
|
-
|
|
417
|
+
// Enter
|
|
418
|
+
if (key === '\r' || key === '\n') {
|
|
419
|
+
if (pLines > 0 && line.startsWith('/') && !line.includes(' ')) {
|
|
420
|
+
const filter = line.slice(1).toLowerCase();
|
|
421
|
+
const filtered = PALETTE_CMDS.filter((c) => c.name.startsWith(filter)).slice(0, maxPalette());
|
|
422
|
+
if (filtered[selected]) {
|
|
423
|
+
line = `/${filtered[selected].name}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
stdout.write('\r\x1b[J\n');
|
|
427
|
+
const trimmed = line.trim();
|
|
428
|
+
if (trimmed) {
|
|
429
|
+
historyBuf.unshift(trimmed);
|
|
430
|
+
historyIdx = -1;
|
|
431
|
+
}
|
|
432
|
+
line = '';
|
|
433
|
+
selected = 0;
|
|
434
|
+
pLines = 0;
|
|
435
|
+
if (!trimmed) {
|
|
436
|
+
render();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
processing = true;
|
|
440
|
+
executeInput(trimmed, state)
|
|
441
|
+
.catch((err) => {
|
|
442
|
+
console.log(error(' ' + (err instanceof Error ? err.message : String(err))));
|
|
443
|
+
})
|
|
444
|
+
.finally(() => {
|
|
445
|
+
processing = false;
|
|
446
|
+
stdin.resume();
|
|
447
|
+
stdin.ref();
|
|
448
|
+
if (stdin.isTTY && !stdin.isRaw)
|
|
449
|
+
stdin.setRawMode(true);
|
|
450
|
+
console.log();
|
|
451
|
+
render();
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
116
454
|
}
|
|
117
|
-
|
|
118
|
-
|
|
455
|
+
// Backspace
|
|
456
|
+
if (key === '\x7f' || key === '\b') {
|
|
457
|
+
if (line.length > 0) {
|
|
458
|
+
line = line.slice(0, -1);
|
|
459
|
+
selected = 0;
|
|
460
|
+
render();
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
119
463
|
}
|
|
120
|
-
|
|
121
|
-
|
|
464
|
+
// Ignore stray escape sequences
|
|
465
|
+
if (key.startsWith('\x1b'))
|
|
466
|
+
return;
|
|
467
|
+
// Regular character
|
|
468
|
+
line += key;
|
|
469
|
+
selected = 0;
|
|
470
|
+
render();
|
|
122
471
|
}
|
|
472
|
+
render();
|
|
473
|
+
stdin.on('data', (raw) => {
|
|
474
|
+
if (processing)
|
|
475
|
+
return;
|
|
476
|
+
if (raw.startsWith('\x1b') && raw.length >= 2) {
|
|
477
|
+
handle(raw);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (let i = 0; i < raw.length; i++) {
|
|
481
|
+
if (processing)
|
|
482
|
+
return;
|
|
483
|
+
const ch = raw[i];
|
|
484
|
+
if (escBuf.length > 0) {
|
|
485
|
+
escBuf += ch;
|
|
486
|
+
if (escTimer)
|
|
487
|
+
clearTimeout(escTimer);
|
|
488
|
+
if (escBuf.length >= 3) {
|
|
489
|
+
const seq = escBuf;
|
|
490
|
+
escBuf = '';
|
|
491
|
+
handle(seq);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
escTimer = setTimeout(() => {
|
|
495
|
+
const seq = escBuf;
|
|
496
|
+
escBuf = '';
|
|
497
|
+
handle(seq);
|
|
498
|
+
}, 50);
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (ch === '\x1b') {
|
|
503
|
+
escBuf = ch;
|
|
504
|
+
escTimer = setTimeout(() => {
|
|
505
|
+
const seq = escBuf;
|
|
506
|
+
escBuf = '';
|
|
507
|
+
handle(seq);
|
|
508
|
+
}, 50);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
handle(ch);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
123
514
|
}
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
* Use case: `codehere "review src/auth.ts"` or scripts piping a task in.
|
|
127
|
-
*/
|
|
128
|
-
export async function runOneShot(prompt, cwd = process.cwd()) {
|
|
515
|
+
/** One-shot · `codehere "review src/auth.ts"` runs that task and exits. */
|
|
516
|
+
export async function runOneShot(prompt) {
|
|
129
517
|
const installed = availableAgents().filter((a) => a.installed);
|
|
130
518
|
if (installed.length === 0) {
|
|
131
|
-
console.log(
|
|
519
|
+
console.log(warning('No specialist installed. Run `codehere setup` and try again.'));
|
|
132
520
|
process.exit(1);
|
|
133
521
|
}
|
|
134
|
-
const manager = SessionManager.getInstance();
|
|
135
522
|
try {
|
|
136
|
-
await
|
|
523
|
+
await spawnViaConductor(prompt);
|
|
137
524
|
}
|
|
138
525
|
catch (err) {
|
|
139
|
-
console.
|
|
526
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
140
527
|
process.exit(1);
|
|
141
528
|
}
|
|
142
529
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
530
|
+
// ── Dispatch ─────────────────────────────────────────────────────────────
|
|
531
|
+
async function executeInput(input, state) {
|
|
532
|
+
const lower = input.toLowerCase();
|
|
533
|
+
if (lower.startsWith('/')) {
|
|
534
|
+
const parts = input.substring(1).trim().split(/\s+/);
|
|
535
|
+
const cmd = parts[0].toLowerCase();
|
|
536
|
+
const args = parts.slice(1).join(' ');
|
|
537
|
+
await handleSlash(cmd, args, state);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Legacy non-slash syntax (v0.2 backwards compat)
|
|
541
|
+
if (lower.startsWith('explain '))
|
|
542
|
+
return runTask(`Explain this file in plain English: ${input.substring(8).trim()}`, state);
|
|
543
|
+
if (lower.startsWith('e '))
|
|
544
|
+
return runTask(`Explain this file in plain English: ${input.substring(2).trim()}`, state);
|
|
545
|
+
if (lower.startsWith('fix ')) {
|
|
546
|
+
const rest = input.substring(4).trim();
|
|
547
|
+
const parts = rest.split(/\s+/);
|
|
548
|
+
return runTask(`fix ${parts[0]}: ${parts.slice(1).join(' ')}`, state);
|
|
549
|
+
}
|
|
550
|
+
if (lower.startsWith('review '))
|
|
551
|
+
return runTask(`Review ${input.substring(7).trim()} for bugs, security, and risks.`, state);
|
|
552
|
+
if (lower.startsWith('plan '))
|
|
553
|
+
return runTask(`Plan: ${input.substring(5).trim()}. Break into steps.`, state);
|
|
554
|
+
// Default: ask via Conductor
|
|
555
|
+
await runTask(input, state);
|
|
556
|
+
}
|
|
557
|
+
async function handleSlash(cmd, args, state) {
|
|
558
|
+
if (cmd === 'help' || cmd === '?') {
|
|
559
|
+
showSlashCommandMenu();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (cmd === 'clear') {
|
|
563
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
564
|
+
printWelcome();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (cmd === 'exit' || cmd === 'quit' || cmd === 'q') {
|
|
568
|
+
console.log(colors.dim('Goodbye! 👋'));
|
|
569
|
+
process.exit(0);
|
|
570
|
+
}
|
|
571
|
+
if (cmd === 'agents') {
|
|
572
|
+
const all = availableAgents();
|
|
573
|
+
newline();
|
|
574
|
+
console.log(colors.bold(' Installed specialists:'));
|
|
575
|
+
for (const a of all) {
|
|
576
|
+
const tag = a.installed ? colors.green(' ✓ ') : colors.dim(' · ');
|
|
577
|
+
const status = a.installed ? '' : colors.dim(' (not installed)');
|
|
578
|
+
console.log(tag + colors.cyan(a.id.padEnd(14)) + colors.dim(a.label) + status);
|
|
579
|
+
}
|
|
580
|
+
newline();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (cmd === 'history' || cmd === 'h') {
|
|
584
|
+
if (state.history.length === 0) {
|
|
585
|
+
console.log(colors.dim('No command history yet.'));
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
console.log(colors.bold('Recent Commands:'));
|
|
589
|
+
state.history.slice(-10).forEach((h, i) => {
|
|
590
|
+
const preview = h.command.length > 60 ? h.command.slice(0, 57) + '…' : h.command;
|
|
591
|
+
console.log(colors.dim(` ${i + 1}. ${preview}`));
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (cmd === 'undo') {
|
|
597
|
+
console.log(colors.dim(' /undo is queued for v0.5 (F3 memory layer).'));
|
|
598
|
+
console.log(colors.dim(' For now: ') + colors.cyan('git diff') + colors.dim(' / ') + colors.cyan('git checkout -- <file>'));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const def = SLASH_COMMANDS.find((c) => c.name === cmd);
|
|
602
|
+
if (!def) {
|
|
603
|
+
console.log(warning(`Unknown command: /${cmd}`));
|
|
604
|
+
console.log(colors.dim('Type /help to see available commands'));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (def.kind === 'task' && def.promptTemplate) {
|
|
608
|
+
if (!args && def.args) {
|
|
609
|
+
console.log(warning(`Usage: /${def.name} ${def.args}`));
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
await runTask(def.promptTemplate(args), state);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (def.kind === 'sub' && def.subcommand) {
|
|
616
|
+
await runSubcommand(def.subcommand, args ? args.split(/\s+/) : []);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function showSlashCommandMenu() {
|
|
621
|
+
newline();
|
|
622
|
+
console.log(colors.bold(' Slash commands'));
|
|
623
|
+
newline();
|
|
624
|
+
const maxNameLen = Math.max(...SLASH_COMMANDS.map((c) => c.name.length + c.args.length)) + 1;
|
|
625
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
626
|
+
const signature = `/${cmd.name}${cmd.args ? ' ' + cmd.args : ''}`;
|
|
627
|
+
const pad = ' '.repeat(Math.max(0, maxNameLen + 2 - signature.length + 8));
|
|
628
|
+
const badge = cmd.noKey ? colors.dim(' (free)') : '';
|
|
629
|
+
console.log(` ${colors.cyan(signature)}${pad}${colors.dim(cmd.description)}${badge}`);
|
|
630
|
+
}
|
|
631
|
+
newline();
|
|
632
|
+
console.log(colors.dim(' Type any command with / prefix, or just ask a question directly.'));
|
|
633
|
+
newline();
|
|
634
|
+
}
|
|
635
|
+
// ── Run-task / Run-subcommand helpers ────────────────────────────────────
|
|
636
|
+
async function runTask(prompt, state) {
|
|
637
|
+
await spawnViaConductor(prompt, state);
|
|
638
|
+
}
|
|
639
|
+
async function runSubcommand(name, extraArgs) {
|
|
640
|
+
const here = fileURLToPath(import.meta.url);
|
|
641
|
+
const cliPath = join(dirname(here), '..', '..', 'index.js');
|
|
642
|
+
await new Promise((resolve) => {
|
|
643
|
+
const child = childSpawn('node', [cliPath, name, ...extraArgs], { stdio: 'inherit' });
|
|
644
|
+
child.on('close', () => resolve());
|
|
645
|
+
child.on('error', (err) => {
|
|
646
|
+
console.log(error(' ' + err.message));
|
|
647
|
+
resolve();
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async function spawnViaConductor(prompt, state) {
|
|
652
|
+
const installed = availableAgents().filter((a) => a.installed);
|
|
653
|
+
if (installed.length === 0) {
|
|
654
|
+
console.log(warning('No specialist installed — install one and try again.'));
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const manager = SessionManager.getInstance();
|
|
658
|
+
const session = manager.spawn(prompt, 'conductor', { cwd: process.cwd() });
|
|
659
|
+
console.log(colors.dim(' ┌─ ') +
|
|
660
|
+
colors.bold(session.sessionId.slice(0, 8)) +
|
|
661
|
+
colors.dim(' · ' + new Date(session.startedAt).toLocaleTimeString()));
|
|
662
|
+
let captured = '';
|
|
147
663
|
await new Promise((resolve) => {
|
|
148
664
|
const unsubscribe = session.subscribe((event) => {
|
|
149
|
-
renderEvent(event);
|
|
665
|
+
const text = renderEvent(event);
|
|
666
|
+
if (text)
|
|
667
|
+
captured += text + '\n';
|
|
150
668
|
if (event.type === 'codehere:complete' || event.type === 'codehere:error' || event.type === 'exit') {
|
|
151
669
|
unsubscribe();
|
|
152
670
|
resolve();
|
|
153
671
|
}
|
|
154
672
|
});
|
|
155
673
|
});
|
|
156
|
-
const
|
|
157
|
-
const verdict =
|
|
674
|
+
const status = session.status;
|
|
675
|
+
const verdict = status === 'completed'
|
|
676
|
+
? success(' └─ done')
|
|
677
|
+
: status === 'killed'
|
|
678
|
+
? warning(' └─ killed')
|
|
679
|
+
: error(' └─ ' + status);
|
|
158
680
|
console.log(verdict);
|
|
681
|
+
if (state) {
|
|
682
|
+
state.history.push({ command: prompt, response: captured, timestamp: new Date().toISOString() });
|
|
683
|
+
}
|
|
159
684
|
}
|
|
160
685
|
function renderEvent(event) {
|
|
161
686
|
switch (event.type) {
|
|
@@ -165,11 +690,7 @@ function renderEvent(event) {
|
|
|
165
690
|
colors.bold('routing → ') +
|
|
166
691
|
colors.cyan(data.delegatedTo ?? '?') +
|
|
167
692
|
colors.dim(' · ' + (data.reason ?? '')));
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
case 'codehere:steps': {
|
|
171
|
-
// Step snapshots replay frequently — skip the noisy ones; the UI handles them
|
|
172
|
-
break;
|
|
693
|
+
return null;
|
|
173
694
|
}
|
|
174
695
|
case 'stdout':
|
|
175
696
|
case 'stderr': {
|
|
@@ -177,89 +698,21 @@ function renderEvent(event) {
|
|
|
177
698
|
const line = data?.line;
|
|
178
699
|
if (line) {
|
|
179
700
|
const text = String(line).trimEnd();
|
|
180
|
-
if (text)
|
|
701
|
+
if (text) {
|
|
181
702
|
console.log(colors.dim(' │ ') + text);
|
|
703
|
+
return text;
|
|
704
|
+
}
|
|
182
705
|
}
|
|
183
|
-
|
|
706
|
+
return null;
|
|
184
707
|
}
|
|
185
708
|
case 'error': {
|
|
186
709
|
const data = event.data;
|
|
187
710
|
if (data?.message)
|
|
188
711
|
console.log(colors.red(' │ ✗ ') + data.message);
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
case 'codehere:complete':
|
|
192
|
-
case 'exit': {
|
|
193
|
-
// handled by runTask's verdict line
|
|
194
|
-
break;
|
|
712
|
+
return null;
|
|
195
713
|
}
|
|
196
714
|
default:
|
|
197
|
-
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function printAgentsAndRoutingRules() {
|
|
202
|
-
newline();
|
|
203
|
-
console.log(colors.bold(' Installed specialists:'));
|
|
204
|
-
const all = availableAgents();
|
|
205
|
-
for (const a of all) {
|
|
206
|
-
const tag = a.installed ? colors.green(' ✓ ') : colors.dim(' · ');
|
|
207
|
-
const status = a.installed ? '' : colors.dim(' (not installed)');
|
|
208
|
-
console.log(tag + colors.cyan(a.id.padEnd(14)) + colors.dim(a.label) + status);
|
|
209
|
-
}
|
|
210
|
-
newline();
|
|
211
|
-
console.log(colors.bold(' Routing (rule-based; LLM classifier in v0.4):'));
|
|
212
|
-
console.log(colors.dim(' review · audit · explain → claude-code'));
|
|
213
|
-
console.log(colors.dim(' refactor · restructure → codex'));
|
|
214
|
-
console.log(colors.dim(' fix · patch · bugfix → aider'));
|
|
215
|
-
console.log(colors.dim(' scaffold · generate · bootstrap → opencode'));
|
|
216
|
-
console.log(colors.dim(' add tests · write tests → codex'));
|
|
217
|
-
console.log(colors.dim(' (anything else) → claude-code'));
|
|
218
|
-
newline();
|
|
219
|
-
}
|
|
220
|
-
async function printStatusSummary() {
|
|
221
|
-
newline();
|
|
222
|
-
try {
|
|
223
|
-
const { existsSync, readFileSync } = await import('fs');
|
|
224
|
-
const { join } = await import('path');
|
|
225
|
-
const { homedir } = await import('os');
|
|
226
|
-
const auditPath = process.env.CODEHERE_AUDIT_FILE || join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
|
|
227
|
-
if (!existsSync(auditPath)) {
|
|
228
|
-
console.log(colors.dim(' No audit log yet. Run a task or `codehere hook install`.'));
|
|
229
|
-
newline();
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const lines = readFileSync(auditPath, 'utf-8').split('\n').filter(Boolean).slice(-10);
|
|
233
|
-
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
234
|
-
let actions = 0;
|
|
235
|
-
let blocked = 0;
|
|
236
|
-
let trustSum = 0;
|
|
237
|
-
let trustN = 0;
|
|
238
|
-
for (const line of lines) {
|
|
239
|
-
try {
|
|
240
|
-
const entry = JSON.parse(line);
|
|
241
|
-
const ts = entry.timestamp ? Date.parse(entry.timestamp) : 0;
|
|
242
|
-
if (ts < cutoff)
|
|
243
|
-
continue;
|
|
244
|
-
actions++;
|
|
245
|
-
if (entry.blocked)
|
|
246
|
-
blocked++;
|
|
247
|
-
if (typeof entry.riskScore === 'number') {
|
|
248
|
-
trustSum += 100 - entry.riskScore;
|
|
249
|
-
trustN++;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
/* skip malformed line */
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const avgTrust = trustN > 0 ? Math.round(trustSum / trustN) : null;
|
|
257
|
-
console.log(colors.bold(' Last 24h:'));
|
|
258
|
-
console.log(` ${actions} actions · ${blocked} blocked${avgTrust !== null ? ` · avg trust ${avgTrust}` : ''}`);
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
console.log(colors.dim(' Could not read audit log: ' + (err instanceof Error ? err.message : String(err))));
|
|
715
|
+
return null;
|
|
262
716
|
}
|
|
263
|
-
newline();
|
|
264
717
|
}
|
|
265
718
|
//# sourceMappingURL=repl.js.map
|