codehere 0.3.0-rc.4 → 0.3.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -14,7 +14,6 @@ import { Command } from 'commander';
|
|
|
14
14
|
import { config } from 'dotenv';
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
-
import { existsSync, readFileSync } from 'fs';
|
|
18
17
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
19
18
|
import { executeConfigCommand } from './presentation/cli/commands/config-command.js';
|
|
20
19
|
import { executeHelpCommand } from './presentation/cli/commands/help-command.js';
|
|
@@ -44,54 +43,25 @@ program
|
|
|
44
43
|
.description('A PM for your AI coding agents. Delegate a task, watch it run, get a plain-English report with a trust score on every file.')
|
|
45
44
|
.version(getVersion())
|
|
46
45
|
.configureHelp({ helpWidth: 80, sortSubcommands: true });
|
|
47
|
-
// Default action · show welcome
|
|
46
|
+
// Default action · show welcome, then drop into the interactive REPL on TTY.
|
|
47
|
+
// codehere offers TWO surfaces, both first-class: the CLI REPL (here) and the
|
|
48
|
+
// web UI (`codehere start`). Users pick whichever fits their workflow.
|
|
48
49
|
program
|
|
49
|
-
.argument('[query]', '
|
|
50
|
+
.argument('[query]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
|
|
50
51
|
.action(async (query) => {
|
|
52
|
+
// One-shot delegate: `codehere "review src/auth.ts"` runs that task and exits.
|
|
53
|
+
// Useful for scripting or quick one-offs without entering the REPL.
|
|
51
54
|
if (query && query.trim()) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log(' ' + colors.cyan(`codehere task "${query.trim().slice(0, 40)}..."`) + ' → spawn via CLI');
|
|
56
|
-
process.exit(1);
|
|
55
|
+
const { runOneShot } = await import('./presentation/cli/repl.js');
|
|
56
|
+
await runOneShot(query.trim());
|
|
57
|
+
return;
|
|
57
58
|
}
|
|
58
59
|
if (!process.stdin.isTTY) {
|
|
59
60
|
console.log(createWelcomeMessage());
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
const hasHooks = existsSync(settingsPath) && readFileSync(settingsPath, 'utf-8').includes('codehere');
|
|
65
|
-
const hasAuditLog = existsSync(join(codehereDir, 'audit', 'hook-events.jsonl'));
|
|
66
|
-
console.log(createWelcomeMessage());
|
|
67
|
-
newline();
|
|
68
|
-
if (!hasHooks && !hasAuditLog) {
|
|
69
|
-
console.log(colors.bold("Welcome! Let's get you set up."));
|
|
70
|
-
newline();
|
|
71
|
-
console.log(' ' + colors.bold('Safety first — no API key, instant'));
|
|
72
|
-
console.log(colors.dim(' Every AI write gets scanned. Catches injections, hardcoded secrets,'));
|
|
73
|
-
console.log(colors.dim(' destructive ops, and more.'));
|
|
74
|
-
newline();
|
|
75
|
-
console.log(' → Run ' + colors.cyan('codehere hook install') + ' to activate safety');
|
|
76
|
-
console.log(' → Run ' + colors.cyan('codehere trust .') + ' to scan this project');
|
|
77
|
-
newline();
|
|
78
|
-
console.log(' ' + colors.bold('Delegate tasks — the PM for your AI coding agents'));
|
|
79
|
-
console.log(colors.dim(' Pick Claude Code, Codex, Aider, or OpenCode. codehere coordinates.'));
|
|
80
|
-
newline();
|
|
81
|
-
console.log(' → Run ' + colors.cyan('codehere start') + ' to open the task UI');
|
|
82
|
-
newline();
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
console.log(colors.bold('Quick start:'));
|
|
86
|
-
newline();
|
|
87
|
-
console.log(' ' + colors.cyan('codehere start') + ' Task intake UI at http://127.0.0.1:3773');
|
|
88
|
-
console.log(' ' + colors.cyan('codehere status') + ' Dashboard · sessions · blocked calls');
|
|
89
|
-
console.log(' ' + colors.cyan('codehere report') + ' Generate a shareable audit report');
|
|
90
|
-
console.log(' ' + colors.cyan('codehere trust .') + ' Scan this project now');
|
|
91
|
-
newline();
|
|
92
|
-
console.log(colors.dim(' Run `codehere --help` for all commands.'));
|
|
93
|
-
newline();
|
|
94
|
-
}
|
|
63
|
+
const { runInteractiveRepl } = await import('./presentation/cli/repl.js');
|
|
64
|
+
await runInteractiveRepl();
|
|
95
65
|
});
|
|
96
66
|
// ── setup ────────────────────────────────────────────────────────────────────
|
|
97
67
|
program
|
|
@@ -354,10 +324,30 @@ program
|
|
|
354
324
|
const port = parseInt(options?.port ?? '3773', 10);
|
|
355
325
|
const host = options?.host ?? '127.0.0.1';
|
|
356
326
|
const shouldOpen = options?.open !== false;
|
|
327
|
+
const url = `http://${host}:${port}/`;
|
|
328
|
+
const openBrowser = async () => {
|
|
329
|
+
if (!shouldOpen)
|
|
330
|
+
return;
|
|
331
|
+
const { spawn } = await import('child_process');
|
|
332
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
333
|
+
try {
|
|
334
|
+
spawn(opener, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* browser open is best-effort */
|
|
338
|
+
}
|
|
339
|
+
};
|
|
357
340
|
try {
|
|
358
341
|
const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
|
|
359
342
|
const server = startMCPServer({ port, host });
|
|
360
|
-
|
|
343
|
+
// Wait for the server to either start listening or fail. The default
|
|
344
|
+
// Node behavior on listen() failure is an unhandled 'error' event which
|
|
345
|
+
// crashes the process with a stack trace. We catch it here so the user
|
|
346
|
+
// sees a friendly message instead.
|
|
347
|
+
await new Promise((resolve, reject) => {
|
|
348
|
+
server.once('listening', () => resolve());
|
|
349
|
+
server.once('error', reject);
|
|
350
|
+
});
|
|
361
351
|
newline();
|
|
362
352
|
console.log(success('codehere is running'));
|
|
363
353
|
newline();
|
|
@@ -367,16 +357,7 @@ program
|
|
|
367
357
|
newline();
|
|
368
358
|
console.log(colors.dim(' Press Ctrl+C to stop'));
|
|
369
359
|
newline();
|
|
370
|
-
|
|
371
|
-
const { spawn } = await import('child_process');
|
|
372
|
-
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
373
|
-
try {
|
|
374
|
-
spawn(opener, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
375
|
-
}
|
|
376
|
-
catch {
|
|
377
|
-
/* browser open is best-effort */
|
|
378
|
-
}
|
|
379
|
-
}
|
|
360
|
+
await openBrowser();
|
|
380
361
|
process.on('SIGINT', () => {
|
|
381
362
|
server.close();
|
|
382
363
|
console.log(colors.dim('\ncodehere stopped'));
|
|
@@ -387,6 +368,28 @@ program
|
|
|
387
368
|
});
|
|
388
369
|
}
|
|
389
370
|
catch (err) {
|
|
371
|
+
const code = err?.code;
|
|
372
|
+
if (code === 'EADDRINUSE') {
|
|
373
|
+
// Port is taken. Probe to find out by what — if it's a running codehere,
|
|
374
|
+
// we should hand the user the existing URL; if it's something else,
|
|
375
|
+
// tell them to pick a different port.
|
|
376
|
+
const { isCodehereOnPort } = await import('./infrastructure/protocols/probe-port.js');
|
|
377
|
+
const existing = await isCodehereOnPort(host, port);
|
|
378
|
+
newline();
|
|
379
|
+
if (existing.isCodehere) {
|
|
380
|
+
console.log(success(`codehere is already running at ${url}`));
|
|
381
|
+
if (existing.version) {
|
|
382
|
+
console.log(colors.dim(` version: ${existing.version}`));
|
|
383
|
+
}
|
|
384
|
+
newline();
|
|
385
|
+
console.log(colors.dim(' Opening browser...'));
|
|
386
|
+
await openBrowser();
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
console.error(error(`Port ${port} is in use by another process.`));
|
|
390
|
+
console.error(colors.dim(` Try: codehere start --port ${port + 1}`));
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
390
393
|
console.error(error('Failed to start:'), err instanceof Error ? err.message : String(err));
|
|
391
394
|
process.exit(1);
|
|
392
395
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect whether a given host:port is already serving a codehere instance.
|
|
3
|
+
*
|
|
4
|
+
* Used by `codehere start` when the listen() fails with EADDRINUSE: we'd
|
|
5
|
+
* rather give the user a friendly "codehere already running, opening browser"
|
|
6
|
+
* than a Node stack trace. If something ELSE holds the port we tell them to
|
|
7
|
+
* pick a different one.
|
|
8
|
+
*
|
|
9
|
+
* Approach: hit the existing server's /health endpoint. The codehere MCP
|
|
10
|
+
* server returns a JSON body with `server: 'codehere-mcp'` — no other tool
|
|
11
|
+
* uses that exact identifier. Generous 1.5-second timeout so a slow loopback
|
|
12
|
+
* doesn't false-negative.
|
|
13
|
+
*/
|
|
14
|
+
export async function isCodehereOnPort(host, port, timeoutMs = 1500) {
|
|
15
|
+
const ac = new AbortController();
|
|
16
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`http://${host}:${port}/health`, { signal: ac.signal });
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return { isCodehere: false };
|
|
21
|
+
const body = (await res.json());
|
|
22
|
+
if (body?.server === 'codehere-mcp') {
|
|
23
|
+
return { isCodehere: true, version: body.version };
|
|
24
|
+
}
|
|
25
|
+
return { isCodehere: false };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Connection refused, timeout, malformed response — anything that's not
|
|
29
|
+
// a positive codehere identification. Treat as "not codehere."
|
|
30
|
+
return { isCodehere: false };
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=probe-port.js.map
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive REPL for codehere.
|
|
3
|
+
*
|
|
4
|
+
* Bare `codehere` (no args) drops here — the CLI surface, parallel to the
|
|
5
|
+
* web UI at `codehere start`. Type a task in plain English; the Conductor
|
|
6
|
+
* routes to the right specialist; events stream to the terminal; you get
|
|
7
|
+
* the next prompt when the task completes.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists:
|
|
10
|
+
* - The v0.2 REPL was removed in D7 along with the rest of the v0.2 CLI
|
|
11
|
+
* surface. That trim went too far — it removed a CLI entry point users
|
|
12
|
+
* relied on. rc.6 brings it back, this time backed by the v0.3
|
|
13
|
+
* Conductor (rule-based routing) instead of v0.2's smart-route.
|
|
14
|
+
* - codehere is "PM you talk to" — terminal users should be able to talk
|
|
15
|
+
* to it in the terminal, not be forced to a web UI. Both surfaces, both
|
|
16
|
+
* first-class.
|
|
17
|
+
*
|
|
18
|
+
* What's here:
|
|
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
|
|
24
|
+
*/
|
|
25
|
+
import readline from 'readline';
|
|
26
|
+
import { colors, newline } from '../../ui.js';
|
|
27
|
+
import { getVersion } from '../../utils/version.js';
|
|
28
|
+
import { SessionManager, availableAgents } from '../../infrastructure/orchestration/session-manager.js';
|
|
29
|
+
export async function runInteractiveRepl(options = {}) {
|
|
30
|
+
const cwd = options.cwd ?? process.cwd();
|
|
31
|
+
newline();
|
|
32
|
+
console.log(colors.bold('codehere ') + colors.dim('v' + getVersion()));
|
|
33
|
+
console.log(colors.dim('Talk to your AI coding agents. Type a task; codehere routes to the right specialist.'));
|
|
34
|
+
newline();
|
|
35
|
+
console.log(colors.dim(' /agents list installed specialists + routing rules'));
|
|
36
|
+
console.log(colors.dim(' /status recent activity from the audit log'));
|
|
37
|
+
console.log(colors.dim(' /web open the web UI at http://127.0.0.1:3773'));
|
|
38
|
+
console.log(colors.dim(' /quit exit (or Ctrl+C / Ctrl+D)'));
|
|
39
|
+
newline();
|
|
40
|
+
const installed = availableAgents().filter((a) => a.installed);
|
|
41
|
+
if (installed.length === 0) {
|
|
42
|
+
console.log(colors.yellow(' No specialist agents installed. Install at least one to delegate tasks:'));
|
|
43
|
+
console.log(colors.dim(' npm install -g @anthropic-ai/claude-code'));
|
|
44
|
+
console.log(colors.dim(' npm install -g @openai/codex'));
|
|
45
|
+
console.log(colors.dim(' brew install aider'));
|
|
46
|
+
console.log(colors.dim(' curl -fsSL https://opencode.ai/install | bash'));
|
|
47
|
+
newline();
|
|
48
|
+
console.log(colors.dim('You can still use ') +
|
|
49
|
+
colors.cyan('/status') +
|
|
50
|
+
colors.dim(', ') +
|
|
51
|
+
colors.cyan('codehere trust .') +
|
|
52
|
+
colors.dim(', and the safety hook here.'));
|
|
53
|
+
newline();
|
|
54
|
+
}
|
|
55
|
+
const rl = readline.createInterface({
|
|
56
|
+
input: process.stdin,
|
|
57
|
+
output: process.stdout,
|
|
58
|
+
prompt: colors.cyan('› '),
|
|
59
|
+
terminal: true,
|
|
60
|
+
});
|
|
61
|
+
const manager = SessionManager.getInstance();
|
|
62
|
+
const prompt = () => {
|
|
63
|
+
rl.setPrompt(colors.cyan('› '));
|
|
64
|
+
rl.prompt();
|
|
65
|
+
};
|
|
66
|
+
rl.on('close', () => {
|
|
67
|
+
newline();
|
|
68
|
+
console.log(colors.dim('Goodbye.'));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
});
|
|
71
|
+
rl.on('SIGINT', () => {
|
|
72
|
+
// Ctrl+C: if we're idle at the prompt, exit; if a task is mid-flight the
|
|
73
|
+
// task's own kill hook handles it via session.kill() (TODO: wire up).
|
|
74
|
+
rl.close();
|
|
75
|
+
});
|
|
76
|
+
prompt();
|
|
77
|
+
for await (const line of rl) {
|
|
78
|
+
const trimmed = line.trim();
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
prompt();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (trimmed === '/quit' || trimmed === '/exit') {
|
|
84
|
+
rl.close();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (trimmed === '/agents') {
|
|
88
|
+
printAgentsAndRoutingRules();
|
|
89
|
+
prompt();
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (trimmed === '/status') {
|
|
93
|
+
await printStatusSummary();
|
|
94
|
+
prompt();
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (trimmed === '/web' || trimmed === '/start') {
|
|
98
|
+
console.log(colors.dim(' Tip: run ') + colors.cyan('codehere start') + colors.dim(' in another terminal to launch the web UI.'));
|
|
99
|
+
prompt();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.startsWith('/')) {
|
|
103
|
+
console.log(colors.yellow(` Unknown REPL command: ${trimmed}`));
|
|
104
|
+
console.log(colors.dim(' Try /agents, /status, /web, or /quit.'));
|
|
105
|
+
prompt();
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Plain text → delegate via Conductor
|
|
109
|
+
if (installed.length === 0) {
|
|
110
|
+
console.log(colors.yellow(' No specialist installed — install one and try again.'));
|
|
111
|
+
prompt();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await runTask(trimmed, cwd, manager);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
|
|
119
|
+
}
|
|
120
|
+
newline();
|
|
121
|
+
prompt();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run a single task and exit — bypass the REPL prompt loop.
|
|
126
|
+
* Use case: `codehere "review src/auth.ts"` or scripts piping a task in.
|
|
127
|
+
*/
|
|
128
|
+
export async function runOneShot(prompt, cwd = process.cwd()) {
|
|
129
|
+
const installed = availableAgents().filter((a) => a.installed);
|
|
130
|
+
if (installed.length === 0) {
|
|
131
|
+
console.log(colors.yellow(' No specialist installed. Install one (see `codehere setup`) and try again.'));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const manager = SessionManager.getInstance();
|
|
135
|
+
try {
|
|
136
|
+
await runTask(prompt, cwd, manager);
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function runTask(prompt, cwd, manager) {
|
|
144
|
+
const session = manager.spawn(prompt, 'conductor', { cwd });
|
|
145
|
+
const startedLabel = colors.dim(' ┌─ ') + colors.bold(session.sessionId.slice(0, 8)) + colors.dim(' · ' + new Date(session.startedAt).toLocaleTimeString());
|
|
146
|
+
console.log(startedLabel);
|
|
147
|
+
await new Promise((resolve) => {
|
|
148
|
+
const unsubscribe = session.subscribe((event) => {
|
|
149
|
+
renderEvent(event);
|
|
150
|
+
if (event.type === 'codehere:complete' || event.type === 'codehere:error' || event.type === 'exit') {
|
|
151
|
+
unsubscribe();
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
const finalStatus = session.status;
|
|
157
|
+
const verdict = finalStatus === 'completed' ? colors.green(' └─ done') : finalStatus === 'killed' ? colors.yellow(' └─ killed') : colors.red(' └─ ' + finalStatus);
|
|
158
|
+
console.log(verdict);
|
|
159
|
+
}
|
|
160
|
+
function renderEvent(event) {
|
|
161
|
+
switch (event.type) {
|
|
162
|
+
case 'codehere:conductor-routing': {
|
|
163
|
+
const data = event.data;
|
|
164
|
+
console.log(colors.dim(' │ ') +
|
|
165
|
+
colors.bold('routing → ') +
|
|
166
|
+
colors.cyan(data.delegatedTo ?? '?') +
|
|
167
|
+
colors.dim(' · ' + (data.reason ?? '')));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'codehere:steps': {
|
|
171
|
+
// Step snapshots replay frequently — skip the noisy ones; the UI handles them
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'stdout':
|
|
175
|
+
case 'stderr': {
|
|
176
|
+
const data = event.data;
|
|
177
|
+
const line = data?.line;
|
|
178
|
+
if (line) {
|
|
179
|
+
const text = String(line).trimEnd();
|
|
180
|
+
if (text)
|
|
181
|
+
console.log(colors.dim(' │ ') + text);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'error': {
|
|
186
|
+
const data = event.data;
|
|
187
|
+
if (data?.message)
|
|
188
|
+
console.log(colors.red(' │ ✗ ') + data.message);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'codehere:complete':
|
|
192
|
+
case 'exit': {
|
|
193
|
+
// handled by runTask's verdict line
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
default:
|
|
197
|
+
// Other event types — quiet by default to keep the REPL readable
|
|
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))));
|
|
262
|
+
}
|
|
263
|
+
newline();
|
|
264
|
+
}
|
|
265
|
+
//# 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.6",
|
|
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",
|