codehere 0.3.0-rc.11 → 0.3.0-rc.12
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/dist/presentation/cli/repl.js +504 -278
- package/package.json +1 -1
|
@@ -1,75 +1,424 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interactive REPL ·
|
|
2
|
+
* Interactive REPL · raw-mode live command palette.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - /undo (no v0.3 undo layer) → queued for v0.5 (F3 memory)
|
|
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.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
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
|
+
*
|
|
18
|
+
* Citing f4828c2 for lineage. Don't redesign.
|
|
13
19
|
*/
|
|
14
|
-
import
|
|
15
|
-
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';
|
|
26
|
+
import { getVersion } from '../../utils/version.js';
|
|
16
27
|
import { SessionManager, availableAgents } from '../../infrastructure/orchestration/session-manager.js';
|
|
28
|
+
import chalk from 'chalk';
|
|
29
|
+
const brandBlue = chalk.rgb(74, 144, 226);
|
|
30
|
+
const stdin = process.stdin;
|
|
31
|
+
const stdout = process.stdout;
|
|
32
|
+
const SLASH_COMMANDS = [
|
|
33
|
+
// Tasks · delegate via Conductor
|
|
34
|
+
{
|
|
35
|
+
name: 'ask',
|
|
36
|
+
args: '<question>',
|
|
37
|
+
description: 'Ask anything about your codebase',
|
|
38
|
+
kind: 'task',
|
|
39
|
+
promptTemplate: (a) => a,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'explain',
|
|
43
|
+
args: '<file>',
|
|
44
|
+
description: 'Explain a file',
|
|
45
|
+
kind: 'task',
|
|
46
|
+
promptTemplate: (a) => `Explain this file in plain English: ${a}`,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'fix',
|
|
50
|
+
args: '<file> <hint>',
|
|
51
|
+
description: 'Apply a targeted fix',
|
|
52
|
+
kind: 'task',
|
|
53
|
+
promptTemplate: (a) => {
|
|
54
|
+
const parts = a.split(/\s+/);
|
|
55
|
+
const file = parts[0];
|
|
56
|
+
const hint = parts.slice(1).join(' ');
|
|
57
|
+
return `fix ${file}: ${hint}`;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'review',
|
|
62
|
+
args: '<file>',
|
|
63
|
+
description: 'Code review with findings',
|
|
64
|
+
kind: 'task',
|
|
65
|
+
promptTemplate: (a) => `Review ${a} for bugs, security issues, and risks. Concrete findings only.`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'plan',
|
|
69
|
+
args: '<goal>',
|
|
70
|
+
description: 'Break a goal into steps',
|
|
71
|
+
kind: 'task',
|
|
72
|
+
promptTemplate: (a) => `Plan: ${a}. Break into concrete steps before any execution.`,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'test',
|
|
76
|
+
args: '<file>',
|
|
77
|
+
description: 'Generate tests for a file',
|
|
78
|
+
kind: 'task',
|
|
79
|
+
promptTemplate: (a) => `add tests for ${a}. Cover edge cases and likely failure modes.`,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'refactor',
|
|
83
|
+
args: '<pattern> <instr>',
|
|
84
|
+
description: 'Refactor across files',
|
|
85
|
+
kind: 'task',
|
|
86
|
+
promptTemplate: (a) => `refactor: ${a}`,
|
|
87
|
+
},
|
|
88
|
+
// Subcommands · run an existing CLI subcommand inline
|
|
89
|
+
{ name: 'trust', args: '[path]', description: 'Scan for vulnerabilities', kind: 'sub', noKey: true, subcommand: 'trust' },
|
|
90
|
+
{ name: 'report', args: '[path]', description: 'Security audit report', kind: 'sub', noKey: true, subcommand: 'report' },
|
|
91
|
+
{ name: 'hook', args: '[install|status]', description: 'Safety hook management', kind: 'sub', noKey: true, subcommand: 'hook' },
|
|
92
|
+
{ name: 'status', args: '', description: 'Dashboard — hook activity', kind: 'sub', noKey: true, subcommand: 'status' },
|
|
93
|
+
{ name: 'health', args: '', description: 'System health check', kind: 'sub', noKey: true, subcommand: 'health' },
|
|
94
|
+
{ name: 'setup', args: '', description: 'Configure first-run preferences', kind: 'sub', noKey: true, subcommand: 'setup' },
|
|
95
|
+
{ name: 'config', args: '[key] [value]', description: 'Manage preferences', kind: 'sub', noKey: true, subcommand: 'config' },
|
|
96
|
+
{ name: 'agents', args: '', description: 'List installed specialists', kind: 'session', noKey: true },
|
|
97
|
+
{ name: 'undo', args: '<file>', description: 'Undo file changes (queued for v0.5)', kind: 'session', noKey: true },
|
|
98
|
+
{ name: 'history', args: '', description: 'Show command history', kind: 'session', noKey: true },
|
|
99
|
+
// Helpers
|
|
100
|
+
{ name: 'help', args: '', description: 'Command reference', kind: 'session', noKey: true },
|
|
101
|
+
{ name: 'clear', args: '', description: 'Clear the screen', kind: 'session', noKey: true },
|
|
102
|
+
{ name: 'exit', args: '', description: 'Leave the session', kind: 'session', noKey: true },
|
|
103
|
+
];
|
|
104
|
+
const PALETTE_CMDS = SLASH_COMMANDS.map((c) => ({
|
|
105
|
+
sig: `/${c.name}${c.args ? ' ' + c.args : ''}`,
|
|
106
|
+
name: c.name,
|
|
107
|
+
desc: c.description,
|
|
108
|
+
noKey: c.noKey,
|
|
109
|
+
}));
|
|
110
|
+
function maxPalette() {
|
|
111
|
+
const rows = process.stdout.rows || 24;
|
|
112
|
+
return Math.min(rows - 8, 20);
|
|
113
|
+
}
|
|
114
|
+
// ── State detection (badges in welcome) ──────────────────────────────────
|
|
115
|
+
function detectState() {
|
|
116
|
+
const cwd = process.cwd();
|
|
117
|
+
const name = basename(cwd);
|
|
118
|
+
const settingsPath = join(process.env.HOME || '', '.claude', 'settings.json');
|
|
119
|
+
let hasHooks = false;
|
|
120
|
+
try {
|
|
121
|
+
hasHooks = existsSync(settingsPath) && readFileSync(settingsPath, 'utf-8').includes('codehere');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* ignore */
|
|
125
|
+
}
|
|
126
|
+
const hasAnyAgent = availableAgents().some((a) => a.installed && a.id !== 'conductor');
|
|
127
|
+
const isGitRepo = existsSync(join(cwd, '.git'));
|
|
128
|
+
let fileCount = 0;
|
|
129
|
+
try {
|
|
130
|
+
const count = (dir, d) => {
|
|
131
|
+
if (d > 3)
|
|
132
|
+
return 0;
|
|
133
|
+
let n = 0;
|
|
134
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
135
|
+
if (e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist')
|
|
136
|
+
continue;
|
|
137
|
+
if (e.isDirectory())
|
|
138
|
+
n += count(join(dir, e.name), d + 1);
|
|
139
|
+
else
|
|
140
|
+
n++;
|
|
141
|
+
}
|
|
142
|
+
return n;
|
|
143
|
+
};
|
|
144
|
+
fileCount = count(cwd, 0);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
return { name, hasHooks, hasAgents: hasAnyAgent, isGitRepo, fileCount };
|
|
150
|
+
}
|
|
151
|
+
function printWelcome() {
|
|
152
|
+
const state = detectState();
|
|
153
|
+
const logo = createCodehereLogo();
|
|
154
|
+
const ver = colors.dim(`v${getVersion()}`);
|
|
155
|
+
console.log();
|
|
156
|
+
console.log(` ${logo} ${ver}`);
|
|
157
|
+
console.log();
|
|
158
|
+
const badges = [
|
|
159
|
+
state.hasHooks ? colors.green('● hooks') : colors.dim('○ hooks'),
|
|
160
|
+
state.hasAgents ? colors.green('● agents') : colors.dim('○ agents'),
|
|
161
|
+
];
|
|
162
|
+
console.log(' ' + badges.join(' '));
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(colors.dim(` ${state.name}`) + colors.dim(` · ${state.isGitRepo ? 'git' : 'dir'} · ${state.fileCount} files`));
|
|
165
|
+
console.log();
|
|
166
|
+
if (!state.hasHooks)
|
|
167
|
+
console.log(' ' + colors.yellow('→') + ' ' + colors.cyan('/hook install') + colors.dim(' to activate safety scanning'));
|
|
168
|
+
else if (!state.hasAgents)
|
|
169
|
+
console.log(' ' + colors.yellow('→') + ' Install ' + colors.cyan('claude-code') + colors.dim(' / ') + colors.cyan('codex') + colors.dim(' / aider / opencode to delegate tasks'));
|
|
170
|
+
console.log(colors.dim(' Type / for commands, or ask anything'));
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
// ── Main session ─────────────────────────────────────────────────────────
|
|
17
174
|
export async function runInteractiveRepl() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log(colors.dim('Keyboard Shortcuts: ↑/↓ for history, Ctrl+C to cancel, Ctrl+D to exit, Ctrl+L to clear'));
|
|
24
|
-
newline();
|
|
25
|
-
console.log(colors.dim('────────────────────────────────────────────────────'));
|
|
26
|
-
newline();
|
|
27
|
-
const rl = readline.createInterface({
|
|
28
|
-
input: process.stdin,
|
|
29
|
-
output: process.stdout,
|
|
30
|
-
prompt: colors.cyan('codehere> '),
|
|
31
|
-
historySize: 100,
|
|
32
|
-
terminal: true,
|
|
33
|
-
});
|
|
175
|
+
printWelcome();
|
|
176
|
+
if (stdin.isTTY)
|
|
177
|
+
stdin.setRawMode(true);
|
|
178
|
+
stdin.resume();
|
|
179
|
+
stdin.setEncoding('utf8');
|
|
34
180
|
const state = { history: [] };
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
181
|
+
const historyBuf = [];
|
|
182
|
+
let historyIdx = -1;
|
|
183
|
+
let line = '';
|
|
184
|
+
let selected = 0;
|
|
185
|
+
let pLines = 0;
|
|
186
|
+
let processing = false;
|
|
187
|
+
let escBuf = '';
|
|
188
|
+
let escTimer = null;
|
|
189
|
+
const cols = () => stdout.columns || 80;
|
|
190
|
+
function truncate(s, maxWidth) {
|
|
191
|
+
const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
192
|
+
if (visible.length <= maxWidth)
|
|
193
|
+
return s;
|
|
194
|
+
let vis = 0;
|
|
195
|
+
let cut = 0;
|
|
196
|
+
for (let i = 0; i < s.length; i++) {
|
|
197
|
+
if (s[i] === '\x1b') {
|
|
198
|
+
const end = s.indexOf('m', i);
|
|
199
|
+
if (end !== -1) {
|
|
200
|
+
i = end;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
vis++;
|
|
205
|
+
if (vis >= maxWidth - 1) {
|
|
206
|
+
cut = i + 1;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return cut > 0 ? s.slice(0, cut) + '\x1b[0m' : s;
|
|
211
|
+
}
|
|
212
|
+
function render() {
|
|
213
|
+
stdout.write('\r\x1b[J');
|
|
214
|
+
stdout.write(brandBlue('> ') + line);
|
|
215
|
+
if (line.startsWith('/') && !line.includes(' ')) {
|
|
216
|
+
const filter = line.slice(1).toLowerCase();
|
|
217
|
+
const allMatching = PALETTE_CMDS.filter((c) => c.name.startsWith(filter));
|
|
218
|
+
const limit = maxPalette();
|
|
219
|
+
const filtered = allMatching.slice(0, limit);
|
|
220
|
+
const overflow = allMatching.length - filtered.length;
|
|
221
|
+
if (filtered.length > 0) {
|
|
222
|
+
const maxLen = Math.max(...filtered.map((c) => c.sig.length));
|
|
223
|
+
const w = cols();
|
|
224
|
+
let drawnLines = 0;
|
|
225
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
226
|
+
const c = filtered[i];
|
|
227
|
+
const pad = ' '.repeat(Math.max(1, maxLen - c.sig.length + 2));
|
|
228
|
+
const badge = c.noKey ? colors.dim(' (free)') : '';
|
|
229
|
+
const prefix = i === selected ? brandBlue('▸') : ' ';
|
|
230
|
+
const text = i === selected ? brandBlue(c.sig) : c.sig;
|
|
231
|
+
const row = ` ${prefix} ${text}${pad}${colors.dim(c.desc)}${badge}`;
|
|
232
|
+
stdout.write('\n' + truncate(row, w));
|
|
233
|
+
drawnLines++;
|
|
234
|
+
}
|
|
235
|
+
if (overflow > 0) {
|
|
236
|
+
stdout.write('\n' + colors.dim(` ... +${overflow} more (type to filter)`));
|
|
237
|
+
drawnLines++;
|
|
238
|
+
}
|
|
239
|
+
stdout.write(`\x1b[${drawnLines}A`);
|
|
240
|
+
stdout.write(`\x1b[${line.length + 3}G`);
|
|
241
|
+
pLines = drawnLines;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
stdout.write('\n' + colors.dim(' No matching commands'));
|
|
245
|
+
stdout.write('\x1b[1A');
|
|
246
|
+
stdout.write(`\x1b[${line.length + 3}G`);
|
|
247
|
+
pLines = 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
pLines = 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function handle(key) {
|
|
255
|
+
if (processing)
|
|
256
|
+
return;
|
|
257
|
+
// Ctrl+C
|
|
258
|
+
if (key === '\x03') {
|
|
259
|
+
if (line.length > 0) {
|
|
260
|
+
line = '';
|
|
261
|
+
selected = 0;
|
|
262
|
+
stdout.write('\r\x1b[J\n');
|
|
263
|
+
render();
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
stdout.write('\r\x1b[J\n');
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
40
269
|
return;
|
|
41
270
|
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
271
|
+
// Ctrl+D
|
|
272
|
+
if (key === '\x04') {
|
|
273
|
+
stdout.write('\r\x1b[J\n' + colors.dim(' See you.') + '\n');
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
// Ctrl+L
|
|
277
|
+
if (key === '\x0c') {
|
|
278
|
+
line = '';
|
|
279
|
+
selected = 0;
|
|
280
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
281
|
+
printWelcome();
|
|
282
|
+
render();
|
|
46
283
|
return;
|
|
47
284
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
285
|
+
// Arrow up
|
|
286
|
+
if (key === '\x1b[A') {
|
|
287
|
+
if (pLines > 0) {
|
|
288
|
+
selected = Math.max(0, selected - 1);
|
|
289
|
+
}
|
|
290
|
+
else if (historyBuf.length > 0 && historyIdx < historyBuf.length - 1) {
|
|
291
|
+
historyIdx++;
|
|
292
|
+
line = historyBuf[historyIdx];
|
|
293
|
+
}
|
|
294
|
+
render();
|
|
52
295
|
return;
|
|
53
296
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
297
|
+
// Arrow down
|
|
298
|
+
if (key === '\x1b[B') {
|
|
299
|
+
if (pLines > 0) {
|
|
300
|
+
selected = Math.min(pLines - 1, selected + 1);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
if (historyIdx > 0) {
|
|
304
|
+
historyIdx--;
|
|
305
|
+
line = historyBuf[historyIdx];
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
historyIdx = -1;
|
|
309
|
+
line = '';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
render();
|
|
59
313
|
return;
|
|
60
314
|
}
|
|
61
|
-
|
|
62
|
-
|
|
315
|
+
// Tab
|
|
316
|
+
if (key === '\t' && pLines > 0) {
|
|
317
|
+
const filter = line.slice(1).toLowerCase();
|
|
318
|
+
const filtered = PALETTE_CMDS.filter((c) => c.name.startsWith(filter)).slice(0, maxPalette());
|
|
319
|
+
if (filtered[selected]) {
|
|
320
|
+
line = `/${filtered[selected].name} `;
|
|
321
|
+
selected = 0;
|
|
322
|
+
}
|
|
323
|
+
render();
|
|
324
|
+
return;
|
|
63
325
|
}
|
|
64
|
-
|
|
65
|
-
|
|
326
|
+
// Enter
|
|
327
|
+
if (key === '\r' || key === '\n') {
|
|
328
|
+
if (pLines > 0 && line.startsWith('/') && !line.includes(' ')) {
|
|
329
|
+
const filter = line.slice(1).toLowerCase();
|
|
330
|
+
const filtered = PALETTE_CMDS.filter((c) => c.name.startsWith(filter)).slice(0, maxPalette());
|
|
331
|
+
if (filtered[selected]) {
|
|
332
|
+
line = `/${filtered[selected].name}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
stdout.write('\r\x1b[J\n');
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (trimmed) {
|
|
338
|
+
historyBuf.unshift(trimmed);
|
|
339
|
+
historyIdx = -1;
|
|
340
|
+
}
|
|
341
|
+
line = '';
|
|
342
|
+
selected = 0;
|
|
343
|
+
pLines = 0;
|
|
344
|
+
if (!trimmed) {
|
|
345
|
+
render();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
processing = true;
|
|
349
|
+
executeInput(trimmed, state)
|
|
350
|
+
.catch((err) => {
|
|
351
|
+
console.log(error(' ' + (err instanceof Error ? err.message : String(err))));
|
|
352
|
+
})
|
|
353
|
+
.finally(() => {
|
|
354
|
+
processing = false;
|
|
355
|
+
stdin.resume();
|
|
356
|
+
stdin.ref();
|
|
357
|
+
if (stdin.isTTY && !stdin.isRaw)
|
|
358
|
+
stdin.setRawMode(true);
|
|
359
|
+
console.log();
|
|
360
|
+
render();
|
|
361
|
+
});
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// Backspace
|
|
365
|
+
if (key === '\x7f' || key === '\b') {
|
|
366
|
+
if (line.length > 0) {
|
|
367
|
+
line = line.slice(0, -1);
|
|
368
|
+
selected = 0;
|
|
369
|
+
render();
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
// Ignore stray escape sequences
|
|
374
|
+
if (key.startsWith('\x1b'))
|
|
375
|
+
return;
|
|
376
|
+
// Regular character
|
|
377
|
+
line += key;
|
|
378
|
+
selected = 0;
|
|
379
|
+
render();
|
|
380
|
+
}
|
|
381
|
+
render();
|
|
382
|
+
stdin.on('data', (raw) => {
|
|
383
|
+
if (processing)
|
|
384
|
+
return;
|
|
385
|
+
if (raw.startsWith('\x1b') && raw.length >= 2) {
|
|
386
|
+
handle(raw);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (let i = 0; i < raw.length; i++) {
|
|
390
|
+
if (processing)
|
|
391
|
+
return;
|
|
392
|
+
const ch = raw[i];
|
|
393
|
+
if (escBuf.length > 0) {
|
|
394
|
+
escBuf += ch;
|
|
395
|
+
if (escTimer)
|
|
396
|
+
clearTimeout(escTimer);
|
|
397
|
+
if (escBuf.length >= 3) {
|
|
398
|
+
const seq = escBuf;
|
|
399
|
+
escBuf = '';
|
|
400
|
+
handle(seq);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
escTimer = setTimeout(() => {
|
|
404
|
+
const seq = escBuf;
|
|
405
|
+
escBuf = '';
|
|
406
|
+
handle(seq);
|
|
407
|
+
}, 50);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (ch === '\x1b') {
|
|
412
|
+
escBuf = ch;
|
|
413
|
+
escTimer = setTimeout(() => {
|
|
414
|
+
const seq = escBuf;
|
|
415
|
+
escBuf = '';
|
|
416
|
+
handle(seq);
|
|
417
|
+
}, 50);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
handle(ch);
|
|
66
421
|
}
|
|
67
|
-
rl.prompt();
|
|
68
|
-
});
|
|
69
|
-
rl.on('close', () => {
|
|
70
|
-
newline();
|
|
71
|
-
console.log(colors.dim('Session ended.'));
|
|
72
|
-
process.exit(0);
|
|
73
422
|
});
|
|
74
423
|
}
|
|
75
424
|
/** One-shot · `codehere "review src/auth.ts"` runs that task and exits. */
|
|
@@ -87,245 +436,126 @@ export async function runOneShot(prompt) {
|
|
|
87
436
|
process.exit(1);
|
|
88
437
|
}
|
|
89
438
|
}
|
|
90
|
-
|
|
91
|
-
|
|
439
|
+
// ── Dispatch ─────────────────────────────────────────────────────────────
|
|
440
|
+
async function executeInput(input, state) {
|
|
441
|
+
const lower = input.toLowerCase();
|
|
92
442
|
if (lower.startsWith('/')) {
|
|
93
|
-
|
|
443
|
+
const parts = input.substring(1).trim().split(/\s+/);
|
|
444
|
+
const cmd = parts[0].toLowerCase();
|
|
445
|
+
const args = parts.slice(1).join(' ');
|
|
446
|
+
await handleSlash(cmd, args, state);
|
|
94
447
|
return;
|
|
95
448
|
}
|
|
96
449
|
// Legacy non-slash syntax (v0.2 backwards compat)
|
|
97
|
-
if (lower.startsWith('explain ')
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const rest = input.substring(
|
|
103
|
-
const parts = rest.split(
|
|
104
|
-
|
|
105
|
-
const instruction = parts.slice(1).join(' ');
|
|
106
|
-
await handleFix(file, instruction);
|
|
450
|
+
if (lower.startsWith('explain '))
|
|
451
|
+
return runTask(`Explain this file in plain English: ${input.substring(8).trim()}`, state);
|
|
452
|
+
if (lower.startsWith('e '))
|
|
453
|
+
return runTask(`Explain this file in plain English: ${input.substring(2).trim()}`, state);
|
|
454
|
+
if (lower.startsWith('fix ')) {
|
|
455
|
+
const rest = input.substring(4).trim();
|
|
456
|
+
const parts = rest.split(/\s+/);
|
|
457
|
+
return runTask(`fix ${parts[0]}: ${parts.slice(1).join(' ')}`, state);
|
|
107
458
|
}
|
|
108
|
-
|
|
109
|
-
|
|
459
|
+
if (lower.startsWith('review '))
|
|
460
|
+
return runTask(`Review ${input.substring(7).trim()} for bugs, security, and risks.`, state);
|
|
461
|
+
if (lower.startsWith('plan '))
|
|
462
|
+
return runTask(`Plan: ${input.substring(5).trim()}. Break into steps.`, state);
|
|
463
|
+
// Default: ask via Conductor
|
|
464
|
+
await runTask(input, state);
|
|
465
|
+
}
|
|
466
|
+
async function handleSlash(cmd, args, state) {
|
|
467
|
+
if (cmd === 'help' || cmd === '?') {
|
|
468
|
+
showSlashCommandMenu();
|
|
469
|
+
return;
|
|
110
470
|
}
|
|
111
|
-
|
|
112
|
-
|
|
471
|
+
if (cmd === 'clear') {
|
|
472
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
473
|
+
printWelcome();
|
|
474
|
+
return;
|
|
113
475
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
476
|
+
if (cmd === 'exit' || cmd === 'quit' || cmd === 'q') {
|
|
477
|
+
console.log(colors.dim('Goodbye! 👋'));
|
|
478
|
+
process.exit(0);
|
|
117
479
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const fixParts = args.split(/\s+/);
|
|
127
|
-
if (fixParts.length < 2) {
|
|
128
|
-
console.log(warning('Usage: /fix <file> <instruction>'));
|
|
129
|
-
console.log(colors.dim('Example: /fix src/utils.ts "add error handling"'));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
await handleFix(fixParts[0], fixParts.slice(1).join(' '));
|
|
133
|
-
break;
|
|
480
|
+
if (cmd === 'agents') {
|
|
481
|
+
const all = availableAgents();
|
|
482
|
+
newline();
|
|
483
|
+
console.log(colors.bold(' Installed specialists:'));
|
|
484
|
+
for (const a of all) {
|
|
485
|
+
const tag = a.installed ? colors.green(' ✓ ') : colors.dim(' · ');
|
|
486
|
+
const status = a.installed ? '' : colors.dim(' (not installed)');
|
|
487
|
+
console.log(tag + colors.cyan(a.id.padEnd(14)) + colors.dim(a.label) + status);
|
|
134
488
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
await handleExplain(args.split(/\s+/)[0]);
|
|
143
|
-
break;
|
|
144
|
-
case 'review':
|
|
145
|
-
case 'r':
|
|
146
|
-
if (!args) {
|
|
147
|
-
console.log(warning('Usage: /review <file>'));
|
|
148
|
-
console.log(colors.dim('Example: /review src/auth.ts'));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
await handleReview(args.split(/\s+/)[0]);
|
|
152
|
-
break;
|
|
153
|
-
case 'plan':
|
|
154
|
-
case 'p':
|
|
155
|
-
if (!args) {
|
|
156
|
-
console.log(warning('Usage: /plan <instruction>'));
|
|
157
|
-
console.log(colors.dim('Example: /plan "add user authentication"'));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
await handlePlan(args);
|
|
161
|
-
break;
|
|
162
|
-
case 'cost':
|
|
163
|
-
case 'status': {
|
|
164
|
-
// v0.3: no separate cost tracking; both surface the audit-log dashboard
|
|
165
|
-
try {
|
|
166
|
-
const { executeStatusCommand } = await import('./commands/status-command.js');
|
|
167
|
-
await executeStatusCommand({});
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
console.log(error('Failed to get status'));
|
|
171
|
-
}
|
|
172
|
-
break;
|
|
489
|
+
newline();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (cmd === 'history' || cmd === 'h') {
|
|
493
|
+
if (state.history.length === 0) {
|
|
494
|
+
console.log(colors.dim('No command history yet.'));
|
|
173
495
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const { join, dirname } = await import('path');
|
|
180
|
-
const here = fileURLToPath(import.meta.url);
|
|
181
|
-
const cliPath = join(dirname(here), '..', '..', 'index.js');
|
|
182
|
-
await new Promise((resolve) => {
|
|
183
|
-
const child = spawn('node', [cliPath, 'trust', target], { stdio: 'inherit' });
|
|
184
|
-
child.on('close', () => resolve());
|
|
185
|
-
child.on('error', (err) => {
|
|
186
|
-
console.log(error('Trust scan failed: ' + err.message));
|
|
187
|
-
resolve();
|
|
188
|
-
});
|
|
496
|
+
else {
|
|
497
|
+
console.log(colors.bold('Recent Commands:'));
|
|
498
|
+
state.history.slice(-10).forEach((h, i) => {
|
|
499
|
+
const preview = h.command.length > 60 ? h.command.slice(0, 57) + '…' : h.command;
|
|
500
|
+
console.log(colors.dim(` ${i + 1}. ${preview}`));
|
|
189
501
|
});
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case 'agents': {
|
|
193
|
-
const all = availableAgents();
|
|
194
|
-
newline();
|
|
195
|
-
console.log(colors.bold(' Installed specialists:'));
|
|
196
|
-
for (const a of all) {
|
|
197
|
-
const tag = a.installed ? colors.green(' ✓ ') : colors.dim(' · ');
|
|
198
|
-
const status = a.installed ? '' : colors.dim(' (not installed)');
|
|
199
|
-
console.log(tag + colors.cyan(a.id.padEnd(14)) + colors.dim(a.label) + status);
|
|
200
|
-
}
|
|
201
|
-
newline();
|
|
202
|
-
break;
|
|
203
502
|
}
|
|
204
|
-
|
|
205
|
-
console.log(colors.dim(' /undo is queued for v0.5 (F3 memory layer).'));
|
|
206
|
-
console.log(colors.dim(' For now: ') + colors.cyan('git diff') + colors.dim(' / ') + colors.cyan('git checkout -- <file>'));
|
|
207
|
-
break;
|
|
208
|
-
case 'history':
|
|
209
|
-
case 'h':
|
|
210
|
-
if (state.history.length === 0) {
|
|
211
|
-
console.log(colors.dim('No command history yet.'));
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
console.log(colors.bold('Recent Commands:'));
|
|
215
|
-
state.history.slice(-10).forEach((h, i) => {
|
|
216
|
-
const preview = h.command.length > 60 ? h.command.slice(0, 57) + '...' : h.command;
|
|
217
|
-
console.log(colors.dim(` ${i + 1}. ${preview}`));
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
break;
|
|
221
|
-
case 'clear':
|
|
222
|
-
console.clear();
|
|
223
|
-
console.log(createWelcomeMessage());
|
|
224
|
-
newline();
|
|
225
|
-
break;
|
|
226
|
-
case 'help':
|
|
227
|
-
case '?':
|
|
228
|
-
showSlashCommandHelp();
|
|
229
|
-
break;
|
|
230
|
-
default:
|
|
231
|
-
console.log(warning(`Unknown command: /${command}`));
|
|
232
|
-
console.log(colors.dim('Type /help to see available commands'));
|
|
503
|
+
return;
|
|
233
504
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
console.log(colors.bold('Slash Commands'));
|
|
238
|
-
console.log(colors.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
239
|
-
newline();
|
|
240
|
-
const commands = [
|
|
241
|
-
['/fix <file> <instruction>', 'Apply AI-generated edit to a file'],
|
|
242
|
-
['/explain <file>', 'Get AI explanation of a file'],
|
|
243
|
-
['/review <file>', 'Review code quality and get suggestions'],
|
|
244
|
-
['/plan <instruction>', 'Create an implementation plan'],
|
|
245
|
-
['/agents', 'List installed specialist agents'],
|
|
246
|
-
['/trust [path]', 'Scan path for security issues (default: cwd)'],
|
|
247
|
-
['/status', 'Show project / audit dashboard'],
|
|
248
|
-
['/cost', 'Alias for /status (no separate cost layer in v0.3)'],
|
|
249
|
-
['/undo', 'Undo last edit (queued for v0.5)'],
|
|
250
|
-
['/history', 'Show command history this session'],
|
|
251
|
-
['/clear', 'Clear the screen'],
|
|
252
|
-
['/help', 'Show this help message'],
|
|
253
|
-
];
|
|
254
|
-
commands.forEach(([cmd, desc]) => {
|
|
255
|
-
console.log(` ${colors.cyan(cmd.padEnd(30))} ${colors.dim(desc)}`);
|
|
256
|
-
});
|
|
257
|
-
newline();
|
|
258
|
-
console.log(colors.dim('Tip: You can also just type your question naturally!'));
|
|
259
|
-
console.log(colors.dim('Example: "how does authentication work?"'));
|
|
260
|
-
newline();
|
|
261
|
-
}
|
|
262
|
-
function showHelp() {
|
|
263
|
-
newline();
|
|
264
|
-
console.log(colors.bold('Slash Commands (recommended):'));
|
|
265
|
-
newline();
|
|
266
|
-
console.log(colors.cyan(' /fix <file> <instruction>') + ' Apply AI-generated edit');
|
|
267
|
-
console.log(colors.cyan(' /explain <file>') + ' Get AI explanation');
|
|
268
|
-
console.log(colors.cyan(' /review <file>') + ' Review code quality');
|
|
269
|
-
console.log(colors.cyan(' /plan <instruction>') + ' Create implementation plan');
|
|
270
|
-
console.log(colors.cyan(' /agents') + ' List installed specialists');
|
|
271
|
-
console.log(colors.cyan(' /trust [path]') + ' Scan for security issues');
|
|
272
|
-
console.log(colors.cyan(' /status') + ' Show project status');
|
|
273
|
-
console.log(colors.cyan(' /history') + ' Show command history');
|
|
274
|
-
console.log(colors.cyan(' /help') + ' Show all commands');
|
|
275
|
-
newline();
|
|
276
|
-
console.log(colors.bold('Quick Commands:'));
|
|
277
|
-
newline();
|
|
278
|
-
console.log(colors.cyan(' <question>') + ' Ask anything · Conductor routes to the right specialist');
|
|
279
|
-
console.log(colors.cyan(' clear') + ' Clear screen');
|
|
280
|
-
console.log(colors.cyan(' exit') + ' Exit session (or: quit, q)');
|
|
281
|
-
newline();
|
|
282
|
-
console.log(colors.dim('Examples:'));
|
|
283
|
-
console.log(colors.dim(' How does authentication work?'));
|
|
284
|
-
console.log(colors.dim(' /explain src/auth.ts'));
|
|
285
|
-
console.log(colors.dim(' /fix src/utils.js "Add error handling"'));
|
|
286
|
-
newline();
|
|
287
|
-
}
|
|
288
|
-
function showKeyboardShortcuts() {
|
|
289
|
-
console.log(colors.bold('Keyboard Shortcuts:'));
|
|
290
|
-
newline();
|
|
291
|
-
console.log(colors.cyan(' ↑/↓') + ' Navigate command history');
|
|
292
|
-
console.log(colors.cyan(' Ctrl+C') + ' Cancel current command');
|
|
293
|
-
console.log(colors.cyan(' Ctrl+D') + ' Exit session');
|
|
294
|
-
console.log(colors.cyan(' Ctrl+L') + ' Clear screen');
|
|
295
|
-
newline();
|
|
296
|
-
}
|
|
297
|
-
// ── Handlers · all delegate via Conductor in v0.3 ────────────────────────
|
|
298
|
-
async function handleAsk(question, state) {
|
|
299
|
-
await spawnViaConductor(question, state);
|
|
300
|
-
}
|
|
301
|
-
async function handleExplain(file) {
|
|
302
|
-
if (!file) {
|
|
303
|
-
console.log(warning('Usage: explain <file>'));
|
|
505
|
+
if (cmd === 'undo') {
|
|
506
|
+
console.log(colors.dim(' /undo is queued for v0.5 (F3 memory layer).'));
|
|
507
|
+
console.log(colors.dim(' For now: ') + colors.cyan('git diff') + colors.dim(' / ') + colors.cyan('git checkout -- <file>'));
|
|
304
508
|
return;
|
|
305
509
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
console.log(warning('Usage: fix <file> <instruction>'));
|
|
311
|
-
console.log(colors.dim('Example: fix src/utils.ts "add error handling"'));
|
|
510
|
+
const def = SLASH_COMMANDS.find((c) => c.name === cmd);
|
|
511
|
+
if (!def) {
|
|
512
|
+
console.log(warning(`Unknown command: /${cmd}`));
|
|
513
|
+
console.log(colors.dim('Type /help to see available commands'));
|
|
312
514
|
return;
|
|
313
515
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
516
|
+
if (def.kind === 'task' && def.promptTemplate) {
|
|
517
|
+
if (!args && def.args) {
|
|
518
|
+
console.log(warning(`Usage: /${def.name} ${def.args}`));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
await runTask(def.promptTemplate(args), state);
|
|
319
522
|
return;
|
|
320
523
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
async function handlePlan(instruction) {
|
|
324
|
-
if (!instruction) {
|
|
325
|
-
console.log(warning('Usage: plan <instruction>'));
|
|
524
|
+
if (def.kind === 'sub' && def.subcommand) {
|
|
525
|
+
await runSubcommand(def.subcommand, args ? args.split(/\s+/) : []);
|
|
326
526
|
return;
|
|
327
527
|
}
|
|
328
|
-
|
|
528
|
+
}
|
|
529
|
+
function showSlashCommandMenu() {
|
|
530
|
+
newline();
|
|
531
|
+
console.log(colors.bold(' Slash commands'));
|
|
532
|
+
newline();
|
|
533
|
+
const maxNameLen = Math.max(...SLASH_COMMANDS.map((c) => c.name.length + c.args.length)) + 1;
|
|
534
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
535
|
+
const signature = `/${cmd.name}${cmd.args ? ' ' + cmd.args : ''}`;
|
|
536
|
+
const pad = ' '.repeat(Math.max(0, maxNameLen + 2 - signature.length + 8));
|
|
537
|
+
const badge = cmd.noKey ? colors.dim(' (free)') : '';
|
|
538
|
+
console.log(` ${colors.cyan(signature)}${pad}${colors.dim(cmd.description)}${badge}`);
|
|
539
|
+
}
|
|
540
|
+
newline();
|
|
541
|
+
console.log(colors.dim(' Type any command with / prefix, or just ask a question directly.'));
|
|
542
|
+
newline();
|
|
543
|
+
}
|
|
544
|
+
// ── Run-task / Run-subcommand helpers ────────────────────────────────────
|
|
545
|
+
async function runTask(prompt, state) {
|
|
546
|
+
await spawnViaConductor(prompt, state);
|
|
547
|
+
}
|
|
548
|
+
async function runSubcommand(name, extraArgs) {
|
|
549
|
+
const here = fileURLToPath(import.meta.url);
|
|
550
|
+
const cliPath = join(dirname(here), '..', '..', 'index.js');
|
|
551
|
+
await new Promise((resolve) => {
|
|
552
|
+
const child = childSpawn('node', [cliPath, name, ...extraArgs], { stdio: 'inherit' });
|
|
553
|
+
child.on('close', () => resolve());
|
|
554
|
+
child.on('error', (err) => {
|
|
555
|
+
console.log(error(' ' + err.message));
|
|
556
|
+
resolve();
|
|
557
|
+
});
|
|
558
|
+
});
|
|
329
559
|
}
|
|
330
560
|
async function spawnViaConductor(prompt, state) {
|
|
331
561
|
const installed = availableAgents().filter((a) => a.installed);
|
|
@@ -358,11 +588,7 @@ async function spawnViaConductor(prompt, state) {
|
|
|
358
588
|
: error(' └─ ' + status);
|
|
359
589
|
console.log(verdict);
|
|
360
590
|
if (state) {
|
|
361
|
-
state.history.push({
|
|
362
|
-
command: prompt,
|
|
363
|
-
response: captured,
|
|
364
|
-
timestamp: new Date().toISOString(),
|
|
365
|
-
});
|
|
591
|
+
state.history.push({ command: prompt, response: captured, timestamp: new Date().toISOString() });
|
|
366
592
|
}
|
|
367
593
|
}
|
|
368
594
|
function renderEvent(event) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codehere",
|
|
3
|
-
"version": "0.3.0-rc.
|
|
3
|
+
"version": "0.3.0-rc.12",
|
|
4
4
|
"description": "A PM for your AI coding agents. Delegate, orchestrate, and audit Claude Code, Codex, Aider, and OpenCode from one local web UI — every AI action traced, every file scored.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|