codehere 0.3.0-rc.10 → 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 +532 -379
- package/package.json +1 -1
|
@@ -1,370 +1,595 @@
|
|
|
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';
|
|
28
|
+
import chalk from 'chalk';
|
|
29
|
+
const brandBlue = chalk.rgb(74, 144, 226);
|
|
30
|
+
const stdin = process.stdin;
|
|
31
|
+
const stdout = process.stdout;
|
|
29
32
|
const SLASH_COMMANDS = [
|
|
30
|
-
//
|
|
33
|
+
// Tasks · delegate via Conductor
|
|
31
34
|
{
|
|
32
|
-
name: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
promptTemplate: (args) => {
|
|
38
|
-
const parts = args.split(/\s+/);
|
|
39
|
-
const file = parts[0];
|
|
40
|
-
const instr = parts.slice(1).join(' ');
|
|
41
|
-
return `fix ${file}: ${instr}`;
|
|
42
|
-
},
|
|
35
|
+
name: 'ask',
|
|
36
|
+
args: '<question>',
|
|
37
|
+
description: 'Ask anything about your codebase',
|
|
38
|
+
kind: 'task',
|
|
39
|
+
promptTemplate: (a) => a,
|
|
43
40
|
},
|
|
44
41
|
{
|
|
45
42
|
name: 'explain',
|
|
46
|
-
aliases: ['e'],
|
|
47
43
|
args: '<file>',
|
|
48
|
-
description: '
|
|
49
|
-
|
|
50
|
-
promptTemplate: (
|
|
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
|
+
},
|
|
51
59
|
},
|
|
52
60
|
{
|
|
53
61
|
name: 'review',
|
|
54
|
-
aliases: ['r'],
|
|
55
62
|
args: '<file>',
|
|
56
|
-
description: 'review
|
|
57
|
-
|
|
58
|
-
promptTemplate: (
|
|
63
|
+
description: 'Code review with findings',
|
|
64
|
+
kind: 'task',
|
|
65
|
+
promptTemplate: (a) => `Review ${a} for bugs, security issues, and risks. Concrete findings only.`,
|
|
59
66
|
},
|
|
60
67
|
{
|
|
61
68
|
name: 'plan',
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
promptTemplate: (args) => `plan: ${args}. Break down into concrete steps before any execution.`,
|
|
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.`,
|
|
67
73
|
},
|
|
68
74
|
{
|
|
69
75
|
name: 'test',
|
|
70
|
-
args: '<file
|
|
71
|
-
description: '
|
|
72
|
-
|
|
73
|
-
promptTemplate: (
|
|
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}`,
|
|
74
87
|
},
|
|
75
|
-
//
|
|
76
|
-
{ name: '
|
|
77
|
-
{ name: '
|
|
78
|
-
{ name: '
|
|
79
|
-
{ name: '
|
|
80
|
-
{ name: '
|
|
81
|
-
{ name: '
|
|
82
|
-
{ name: '
|
|
83
|
-
{ name: '
|
|
84
|
-
{ name: '
|
|
85
|
-
{ name: '
|
|
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 },
|
|
86
103
|
];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 */
|
|
94
125
|
}
|
|
95
|
-
|
|
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 };
|
|
96
150
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
console.log(
|
|
102
|
-
console.log(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.log(colors.
|
|
114
|
-
|
|
115
|
-
console.log(colors.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 ─────────────────────────────────────────────────────────
|
|
174
|
+
export async function runInteractiveRepl() {
|
|
175
|
+
printWelcome();
|
|
176
|
+
if (stdin.isTTY)
|
|
177
|
+
stdin.setRawMode(true);
|
|
178
|
+
stdin.resume();
|
|
179
|
+
stdin.setEncoding('utf8');
|
|
180
|
+
const state = { history: [] };
|
|
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;
|
|
123
211
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|
|
150
249
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
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();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Arrow up
|
|
286
|
+
if (key === '\x1b[A') {
|
|
287
|
+
if (pLines > 0) {
|
|
288
|
+
selected = Math.max(0, selected - 1);
|
|
160
289
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
case 'history':
|
|
182
|
-
printHistory(history);
|
|
183
|
-
break;
|
|
184
|
-
case 'web':
|
|
185
|
-
console.log(colors.dim(' Tip: run ') +
|
|
186
|
-
colors.cyan('codehere start') +
|
|
187
|
-
colors.dim(' in another terminal to launch the web UI at http://127.0.0.1:3773'));
|
|
188
|
-
break;
|
|
189
|
-
case 'trust':
|
|
190
|
-
await runTrustScan(args || '.');
|
|
191
|
-
break;
|
|
192
|
-
case 'cost':
|
|
193
|
-
// v0.3 doesn't separate cost tracking — show the same dashboard as /status
|
|
194
|
-
await printStatusSummary();
|
|
195
|
-
break;
|
|
196
|
-
case 'undo':
|
|
197
|
-
console.log(colors.dim(' /undo is queued for v0.5 (F3 memory layer).'));
|
|
198
|
-
console.log(colors.dim(' For now, use git: ') + colors.cyan('git diff') + colors.dim(' / ') + colors.cyan('git checkout -- <file>'));
|
|
199
|
-
break;
|
|
200
|
-
default: {
|
|
201
|
-
// Task slash command (/fix, /explain, /review, /plan, /test) → Conductor
|
|
202
|
-
if (command.action !== 'task' || !command.promptTemplate)
|
|
203
|
-
break;
|
|
204
|
-
if (!args) {
|
|
205
|
-
console.log(colors.yellow(` Usage: /${command.name}${command.args ? ' ' + command.args : ''}`));
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
if (installed.length === 0) {
|
|
209
|
-
console.log(colors.yellow(' No specialist installed — install one and try again.'));
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
const promptText = command.promptTemplate(args);
|
|
213
|
-
try {
|
|
214
|
-
await runTask(promptText, cwd, manager);
|
|
215
|
-
}
|
|
216
|
-
catch (err) {
|
|
217
|
-
console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
|
|
218
|
-
}
|
|
219
|
-
newline();
|
|
290
|
+
else if (historyBuf.length > 0 && historyIdx < historyBuf.length - 1) {
|
|
291
|
+
historyIdx++;
|
|
292
|
+
line = historyBuf[historyIdx];
|
|
293
|
+
}
|
|
294
|
+
render();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
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 = '';
|
|
220
310
|
}
|
|
221
311
|
}
|
|
222
|
-
|
|
223
|
-
|
|
312
|
+
render();
|
|
313
|
+
return;
|
|
224
314
|
}
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
|
|
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;
|
|
325
|
+
}
|
|
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}`;
|
|
243
333
|
}
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
|
246
401
|
}
|
|
247
402
|
else {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
403
|
+
escTimer = setTimeout(() => {
|
|
404
|
+
const seq = escBuf;
|
|
405
|
+
escBuf = '';
|
|
406
|
+
handle(seq);
|
|
407
|
+
}, 50);
|
|
254
408
|
}
|
|
255
|
-
newline();
|
|
256
|
-
promptNext();
|
|
257
409
|
continue;
|
|
258
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);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
/** One-shot · `codehere "review src/auth.ts"` runs that task and exits. */
|
|
425
|
+
export async function runOneShot(prompt) {
|
|
426
|
+
const installed = availableAgents().filter((a) => a.installed);
|
|
427
|
+
if (installed.length === 0) {
|
|
428
|
+
console.log(warning('No specialist installed. Run `codehere setup` and try again.'));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await spawnViaConductor(prompt);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
console.error(error('Error:'), err instanceof Error ? err.message : String(err));
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// ── Dispatch ─────────────────────────────────────────────────────────────
|
|
440
|
+
async function executeInput(input, state) {
|
|
441
|
+
const lower = input.toLowerCase();
|
|
442
|
+
if (lower.startsWith('/')) {
|
|
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);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
// Legacy non-slash syntax (v0.2 backwards compat)
|
|
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);
|
|
458
|
+
}
|
|
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;
|
|
470
|
+
}
|
|
471
|
+
if (cmd === 'clear') {
|
|
472
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
473
|
+
printWelcome();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (cmd === 'exit' || cmd === 'quit' || cmd === 'q') {
|
|
477
|
+
console.log(colors.dim('Goodbye! 👋'));
|
|
478
|
+
process.exit(0);
|
|
479
|
+
}
|
|
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);
|
|
259
488
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.'));
|
|
265
495
|
}
|
|
266
|
-
|
|
267
|
-
|
|
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}`));
|
|
501
|
+
});
|
|
268
502
|
}
|
|
269
|
-
|
|
270
|
-
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
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>'));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
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'));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (def.kind === 'task' && def.promptTemplate) {
|
|
517
|
+
if (!args && def.args) {
|
|
518
|
+
console.log(warning(`Usage: /${def.name} ${def.args}`));
|
|
519
|
+
return;
|
|
271
520
|
}
|
|
272
|
-
|
|
273
|
-
|
|
521
|
+
await runTask(def.promptTemplate(args), state);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (def.kind === 'sub' && def.subcommand) {
|
|
525
|
+
await runSubcommand(def.subcommand, args ? args.split(/\s+/) : []);
|
|
526
|
+
return;
|
|
274
527
|
}
|
|
275
528
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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) {
|
|
285
549
|
const here = fileURLToPath(import.meta.url);
|
|
286
550
|
const cliPath = join(dirname(here), '..', '..', 'index.js');
|
|
287
551
|
await new Promise((resolve) => {
|
|
288
|
-
const child =
|
|
552
|
+
const child = childSpawn('node', [cliPath, name, ...extraArgs], { stdio: 'inherit' });
|
|
289
553
|
child.on('close', () => resolve());
|
|
290
554
|
child.on('error', (err) => {
|
|
291
|
-
console.log(
|
|
555
|
+
console.log(error(' ' + err.message));
|
|
292
556
|
resolve();
|
|
293
557
|
});
|
|
294
558
|
});
|
|
295
559
|
}
|
|
296
|
-
function
|
|
297
|
-
const formatRow = (cmd) => {
|
|
298
|
-
const aliases = cmd.aliases?.length ? colors.dim(' (/' + cmd.aliases.join(', /') + ')') : '';
|
|
299
|
-
const cmdLabel = '/' + cmd.name + (cmd.args ? ' ' + cmd.args : '');
|
|
300
|
-
console.log(' ' + colors.cyan(cmdLabel.padEnd(34)) + colors.dim(cmd.description) + aliases);
|
|
301
|
-
};
|
|
302
|
-
newline();
|
|
303
|
-
console.log(colors.bold('Slash commands'));
|
|
304
|
-
console.log(colors.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
305
|
-
newline();
|
|
306
|
-
console.log(colors.bold(' Tasks') + colors.dim(' — describe the work; Conductor routes to the right specialist'));
|
|
307
|
-
for (const cmd of SLASH_COMMANDS.filter((c) => c.action === 'task'))
|
|
308
|
-
formatRow(cmd);
|
|
309
|
-
newline();
|
|
310
|
-
console.log(colors.bold(' Meta') + colors.dim(' — run locally, no agent spawn'));
|
|
311
|
-
for (const cmd of SLASH_COMMANDS.filter((c) => c.action === 'meta'))
|
|
312
|
-
formatRow(cmd);
|
|
313
|
-
newline();
|
|
314
|
-
console.log(colors.bold(' Or just type'));
|
|
315
|
-
console.log(colors.dim(' Plain text without / → Conductor routes (e.g. "review src/auth.ts" → claude-code)'));
|
|
316
|
-
console.log(colors.dim(' Legacy v0.2 syntax: ') + colors.cyan('explain src/auth.ts') + colors.dim(' / ') + colors.cyan('fix file "instr"') + colors.dim(' also work without /'));
|
|
317
|
-
newline();
|
|
318
|
-
}
|
|
319
|
-
function printHistory(history) {
|
|
320
|
-
newline();
|
|
321
|
-
if (history.length === 0) {
|
|
322
|
-
console.log(colors.dim(' No prompts yet in this session.'));
|
|
323
|
-
newline();
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
console.log(colors.bold(' Recent prompts:'));
|
|
327
|
-
history.slice(-10).forEach((h, i) => {
|
|
328
|
-
const preview = h.length > 70 ? h.slice(0, 67) + '…' : h;
|
|
329
|
-
console.log(colors.dim(` ${(i + 1).toString().padStart(2, ' ')}. `) + preview);
|
|
330
|
-
});
|
|
331
|
-
newline();
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Run a single task and exit — bypass the REPL prompt loop.
|
|
335
|
-
* Use case: `codehere "review src/auth.ts"` or scripts piping a task in.
|
|
336
|
-
*/
|
|
337
|
-
export async function runOneShot(prompt, cwd = process.cwd()) {
|
|
560
|
+
async function spawnViaConductor(prompt, state) {
|
|
338
561
|
const installed = availableAgents().filter((a) => a.installed);
|
|
339
562
|
if (installed.length === 0) {
|
|
340
|
-
console.log(
|
|
341
|
-
|
|
563
|
+
console.log(warning('No specialist installed — install one and try again.'));
|
|
564
|
+
return;
|
|
342
565
|
}
|
|
343
566
|
const manager = SessionManager.getInstance();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
process.exit(1);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
async function runTask(prompt, cwd, manager) {
|
|
353
|
-
const session = manager.spawn(prompt, 'conductor', { cwd });
|
|
354
|
-
const startedLabel = colors.dim(' ┌─ ') + colors.bold(session.sessionId.slice(0, 8)) + colors.dim(' · ' + new Date(session.startedAt).toLocaleTimeString());
|
|
355
|
-
console.log(startedLabel);
|
|
567
|
+
const session = manager.spawn(prompt, 'conductor', { cwd: process.cwd() });
|
|
568
|
+
console.log(colors.dim(' ┌─ ') +
|
|
569
|
+
colors.bold(session.sessionId.slice(0, 8)) +
|
|
570
|
+
colors.dim(' · ' + new Date(session.startedAt).toLocaleTimeString()));
|
|
571
|
+
let captured = '';
|
|
356
572
|
await new Promise((resolve) => {
|
|
357
573
|
const unsubscribe = session.subscribe((event) => {
|
|
358
|
-
renderEvent(event);
|
|
574
|
+
const text = renderEvent(event);
|
|
575
|
+
if (text)
|
|
576
|
+
captured += text + '\n';
|
|
359
577
|
if (event.type === 'codehere:complete' || event.type === 'codehere:error' || event.type === 'exit') {
|
|
360
578
|
unsubscribe();
|
|
361
579
|
resolve();
|
|
362
580
|
}
|
|
363
581
|
});
|
|
364
582
|
});
|
|
365
|
-
const
|
|
366
|
-
const verdict =
|
|
583
|
+
const status = session.status;
|
|
584
|
+
const verdict = status === 'completed'
|
|
585
|
+
? success(' └─ done')
|
|
586
|
+
: status === 'killed'
|
|
587
|
+
? warning(' └─ killed')
|
|
588
|
+
: error(' └─ ' + status);
|
|
367
589
|
console.log(verdict);
|
|
590
|
+
if (state) {
|
|
591
|
+
state.history.push({ command: prompt, response: captured, timestamp: new Date().toISOString() });
|
|
592
|
+
}
|
|
368
593
|
}
|
|
369
594
|
function renderEvent(event) {
|
|
370
595
|
switch (event.type) {
|
|
@@ -374,11 +599,7 @@ function renderEvent(event) {
|
|
|
374
599
|
colors.bold('routing → ') +
|
|
375
600
|
colors.cyan(data.delegatedTo ?? '?') +
|
|
376
601
|
colors.dim(' · ' + (data.reason ?? '')));
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
case 'codehere:steps': {
|
|
380
|
-
// Step snapshots replay frequently — skip the noisy ones; the UI handles them
|
|
381
|
-
break;
|
|
602
|
+
return null;
|
|
382
603
|
}
|
|
383
604
|
case 'stdout':
|
|
384
605
|
case 'stderr': {
|
|
@@ -386,89 +607,21 @@ function renderEvent(event) {
|
|
|
386
607
|
const line = data?.line;
|
|
387
608
|
if (line) {
|
|
388
609
|
const text = String(line).trimEnd();
|
|
389
|
-
if (text)
|
|
610
|
+
if (text) {
|
|
390
611
|
console.log(colors.dim(' │ ') + text);
|
|
612
|
+
return text;
|
|
613
|
+
}
|
|
391
614
|
}
|
|
392
|
-
|
|
615
|
+
return null;
|
|
393
616
|
}
|
|
394
617
|
case 'error': {
|
|
395
618
|
const data = event.data;
|
|
396
619
|
if (data?.message)
|
|
397
620
|
console.log(colors.red(' │ ✗ ') + data.message);
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
case 'codehere:complete':
|
|
401
|
-
case 'exit': {
|
|
402
|
-
// handled by runTask's verdict line
|
|
403
|
-
break;
|
|
621
|
+
return null;
|
|
404
622
|
}
|
|
405
623
|
default:
|
|
406
|
-
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
function printAgentsAndRoutingRules() {
|
|
411
|
-
newline();
|
|
412
|
-
console.log(colors.bold(' Installed specialists:'));
|
|
413
|
-
const all = availableAgents();
|
|
414
|
-
for (const a of all) {
|
|
415
|
-
const tag = a.installed ? colors.green(' ✓ ') : colors.dim(' · ');
|
|
416
|
-
const status = a.installed ? '' : colors.dim(' (not installed)');
|
|
417
|
-
console.log(tag + colors.cyan(a.id.padEnd(14)) + colors.dim(a.label) + status);
|
|
624
|
+
return null;
|
|
418
625
|
}
|
|
419
|
-
newline();
|
|
420
|
-
console.log(colors.bold(' Routing (rule-based; LLM classifier in v0.4):'));
|
|
421
|
-
console.log(colors.dim(' review · audit · explain → claude-code'));
|
|
422
|
-
console.log(colors.dim(' refactor · restructure → codex'));
|
|
423
|
-
console.log(colors.dim(' fix · patch · bugfix → aider'));
|
|
424
|
-
console.log(colors.dim(' scaffold · generate · bootstrap → opencode'));
|
|
425
|
-
console.log(colors.dim(' add tests · write tests → codex'));
|
|
426
|
-
console.log(colors.dim(' (anything else) → claude-code'));
|
|
427
|
-
newline();
|
|
428
|
-
}
|
|
429
|
-
async function printStatusSummary() {
|
|
430
|
-
newline();
|
|
431
|
-
try {
|
|
432
|
-
const { existsSync, readFileSync } = await import('fs');
|
|
433
|
-
const { join } = await import('path');
|
|
434
|
-
const { homedir } = await import('os');
|
|
435
|
-
const auditPath = process.env.CODEHERE_AUDIT_FILE || join(homedir(), '.codehere', 'audit', 'hook-events.jsonl');
|
|
436
|
-
if (!existsSync(auditPath)) {
|
|
437
|
-
console.log(colors.dim(' No audit log yet. Run a task or `codehere hook install`.'));
|
|
438
|
-
newline();
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const lines = readFileSync(auditPath, 'utf-8').split('\n').filter(Boolean).slice(-10);
|
|
442
|
-
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
443
|
-
let actions = 0;
|
|
444
|
-
let blocked = 0;
|
|
445
|
-
let trustSum = 0;
|
|
446
|
-
let trustN = 0;
|
|
447
|
-
for (const line of lines) {
|
|
448
|
-
try {
|
|
449
|
-
const entry = JSON.parse(line);
|
|
450
|
-
const ts = entry.timestamp ? Date.parse(entry.timestamp) : 0;
|
|
451
|
-
if (ts < cutoff)
|
|
452
|
-
continue;
|
|
453
|
-
actions++;
|
|
454
|
-
if (entry.blocked)
|
|
455
|
-
blocked++;
|
|
456
|
-
if (typeof entry.riskScore === 'number') {
|
|
457
|
-
trustSum += 100 - entry.riskScore;
|
|
458
|
-
trustN++;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
/* skip malformed line */
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
const avgTrust = trustN > 0 ? Math.round(trustSum / trustN) : null;
|
|
466
|
-
console.log(colors.bold(' Last 24h:'));
|
|
467
|
-
console.log(` ${actions} actions · ${blocked} blocked${avgTrust !== null ? ` · avg trust ${avgTrust}` : ''}`);
|
|
468
|
-
}
|
|
469
|
-
catch (err) {
|
|
470
|
-
console.log(colors.dim(' Could not read audit log: ' + (err instanceof Error ? err.message : String(err))));
|
|
471
|
-
}
|
|
472
|
-
newline();
|
|
473
626
|
}
|
|
474
627
|
//# sourceMappingURL=repl.js.map
|
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",
|