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.
@@ -1,370 +1,595 @@
1
1
  /**
2
- * Interactive REPL for codehere.
2
+ * Interactive REPL · raw-mode live command palette.
3
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.
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
- * 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.
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
- * 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
18
+ * Citing f4828c2 for lineage. Don't redesign.
24
19
  */
25
- import readline from 'readline';
26
- import { colors, newline } from '../../ui.js';
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
- // Task commands · describe the work, the Conductor routes
33
+ // Tasks · delegate via Conductor
31
34
  {
32
- name: 'fix',
33
- aliases: ['edit'],
34
- args: '<file> <instruction>',
35
- description: 'fix or edit a file (Conductor routes to Aider by default)',
36
- action: 'task',
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: 'explain a file (Conductor routes to Claude Code by default)',
49
- action: 'task',
50
- promptTemplate: (args) => `explain this file in plain English: ${args}`,
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 a file for bugs and risks (routes to Claude Code)',
57
- action: 'task',
58
- promptTemplate: (args) => `review ${args} for bugs, security issues, and risks. List concrete findings.`,
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
- aliases: ['p'],
63
- args: '<instruction>',
64
- description: 'plan a complex task into steps (routes to Codex/Claude Code)',
65
- action: 'task',
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 or area>',
71
- description: 'add or write tests (routes to Codex)',
72
- action: 'task',
73
- promptTemplate: (args) => `add tests for ${args}. Cover edge cases and likely failure modes.`,
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
- // Meta · don't spawn agents · run locally
76
- { name: 'agents', aliases: ['a'], description: 'list installed specialists + routing rules', action: 'meta' },
77
- { name: 'status', aliases: ['s'], description: 'last 24h activity from the audit log', action: 'meta' },
78
- { name: 'trust', args: '[path]', description: 'scan a path for security issues (default: current dir)', action: 'meta' },
79
- { name: 'history', aliases: ['hist'], description: 'recent prompts in this session', action: 'meta' },
80
- { name: 'web', description: 'launch the web UI at :3773 (in another terminal)', action: 'meta' },
81
- { name: 'undo', description: 'undo the last edit (queued for v0.5 · F3 memory layer)', action: 'meta' },
82
- { name: 'cost', description: 'usage / cost (alias for /status in v0.3)', action: 'meta' },
83
- { name: 'clear', description: 'clear the screen', action: 'meta' },
84
- { name: 'help', aliases: ['?'], description: 'show this help', action: 'meta' },
85
- { name: 'quit', aliases: ['exit', 'q'], description: 'exit (Ctrl+C / Ctrl+D also work)', action: 'meta' },
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
- function findSlashCommand(input) {
88
- const parts = input.trim().split(/\s+/);
89
- const head = parts[0].toLowerCase();
90
- for (const cmd of SLASH_COMMANDS) {
91
- if (cmd.name === head || (cmd.aliases ?? []).includes(head)) {
92
- return { command: cmd, args: parts.slice(1).join(' ').trim() };
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
- return null;
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
- export async function runInteractiveRepl(options = {}) {
98
- const cwd = options.cwd ?? process.cwd();
99
- const history = [];
100
- newline();
101
- console.log(colors.bold('codehere ') + colors.dim('v' + getVersion()));
102
- console.log(colors.dim('Talk to your AI coding agents. Type a task to delegate; or use a /command.'));
103
- newline();
104
- console.log(colors.dim(' /fix /explain /review /plan delegate a task; Conductor picks the specialist'));
105
- console.log(colors.dim(' /agents /status /trust /history show state, no agent spawn'));
106
- console.log(colors.dim(' /help full command list'));
107
- console.log(colors.dim(' ↑/↓ history · Ctrl+L clear · Ctrl+C / Ctrl+D exit'));
108
- newline();
109
- const installed = availableAgents().filter((a) => a.installed);
110
- if (installed.length === 0) {
111
- console.log(colors.yellow(' No specialist agents installed. Install at least one to delegate tasks:'));
112
- console.log(colors.dim(' npm install -g @anthropic-ai/claude-code'));
113
- console.log(colors.dim(' npm install -g @openai/codex'));
114
- console.log(colors.dim(' brew install aider'));
115
- console.log(colors.dim(' curl -fsSL https://opencode.ai/install | bash'));
116
- newline();
117
- console.log(colors.dim('You can still use ') +
118
- colors.cyan('/status') +
119
- colors.dim(', ') +
120
- colors.cyan('codehere trust .') +
121
- colors.dim(', and the safety hook here.'));
122
- newline();
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
- const rl = readline.createInterface({
125
- input: process.stdin,
126
- output: process.stdout,
127
- prompt: colors.cyan('› '),
128
- terminal: true,
129
- historySize: 200,
130
- });
131
- const manager = SessionManager.getInstance();
132
- const promptNext = () => {
133
- rl.setPrompt(colors.cyan('› '));
134
- rl.prompt();
135
- };
136
- rl.on('close', () => {
137
- newline();
138
- console.log(colors.dim('Goodbye.'));
139
- process.exit(0);
140
- });
141
- rl.on('SIGINT', () => {
142
- rl.close();
143
- });
144
- promptNext();
145
- for await (const line of rl) {
146
- const trimmed = line.trim();
147
- if (!trimmed) {
148
- promptNext();
149
- continue;
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
- history.push(trimmed);
152
- // Slash commands first
153
- if (trimmed.startsWith('/')) {
154
- const found = findSlashCommand(trimmed.slice(1));
155
- if (!found) {
156
- console.log(colors.yellow(` Unknown command: ${trimmed.split(/\s+/)[0]}`));
157
- console.log(colors.dim(' Run /help to see all commands.'));
158
- promptNext();
159
- continue;
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
- const { command, args } = found;
162
- switch (command.name) {
163
- case 'quit':
164
- rl.close();
165
- return;
166
- case 'help':
167
- printSlashCommandHelp();
168
- break;
169
- case 'clear':
170
- console.clear();
171
- newline();
172
- console.log(colors.bold('codehere ') + colors.dim('v' + getVersion()));
173
- newline();
174
- break;
175
- case 'agents':
176
- printAgentsAndRoutingRules();
177
- break;
178
- case 'status':
179
- await printStatusSummary();
180
- break;
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
- promptNext();
223
- continue;
312
+ render();
313
+ return;
224
314
  }
225
- // Legacy non-slash syntax for backwards-compat with v0.2 muscle memory
226
- // (commit 796d4b7's REPL accepted these forms without /).
227
- const lower = trimmed.toLowerCase();
228
- const legacyMatch = lower.startsWith('explain ') || lower.startsWith('e ')
229
- ? { name: 'explain', argsAt: lower.startsWith('explain ') ? 8 : 2 }
230
- : lower.startsWith('fix ') || lower.startsWith('edit ')
231
- ? { name: 'fix', argsAt: lower.startsWith('fix ') ? 4 : 5 }
232
- : lower.startsWith('review ')
233
- ? { name: 'review', argsAt: 7 }
234
- : lower.startsWith('plan ')
235
- ? { name: 'plan', argsAt: 5 }
236
- : null;
237
- if (legacyMatch) {
238
- const cmd = SLASH_COMMANDS.find((c) => c.name === legacyMatch.name);
239
- if (cmd?.promptTemplate) {
240
- const args = trimmed.substring(legacyMatch.argsAt).trim();
241
- if (!args) {
242
- console.log(colors.yellow(` Usage: /${cmd.name}${cmd.args ? ' ' + cmd.args : ''}`));
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
- else if (installed.length === 0) {
245
- console.log(colors.yellow(' No specialist installed — install one and try again.'));
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
- try {
249
- await runTask(cmd.promptTemplate(args), cwd, manager);
250
- }
251
- catch (err) {
252
- console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
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
- // Plain text → delegate via Conductor
261
- if (installed.length === 0) {
262
- console.log(colors.yellow(' No specialist installed — install one and try again.'));
263
- promptNext();
264
- continue;
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
- try {
267
- await runTask(trimmed, cwd, manager);
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
- catch (err) {
270
- console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
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
- newline();
273
- promptNext();
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
- * Run `codehere trust <path>` inline. Spawns the trust command as a child
278
- * process so the existing rendering / scoring logic is reused as-is.
279
- */
280
- async function runTrustScan(target) {
281
- const { spawn } = await import('child_process');
282
- const { fileURLToPath } = await import('url');
283
- const { join, dirname } = await import('path');
284
- // dist/presentation/cli/repl.js → walk up to dist/index.js
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 = spawn('node', [cliPath, 'trust', target], { stdio: 'inherit' });
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(colors.red(' ✗ ') + err.message);
555
+ console.log(error(' ' + err.message));
292
556
  resolve();
293
557
  });
294
558
  });
295
559
  }
296
- function printSlashCommandHelp() {
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(colors.yellow(' No specialist installed. Install one (see `codehere setup`) and try again.'));
341
- process.exit(1);
563
+ console.log(warning('No specialist installed — install one and try again.'));
564
+ return;
342
565
  }
343
566
  const manager = SessionManager.getInstance();
344
- try {
345
- await runTask(prompt, cwd, manager);
346
- }
347
- catch (err) {
348
- console.log(colors.red(' ✗ ') + (err instanceof Error ? err.message : String(err)));
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 finalStatus = session.status;
366
- const verdict = finalStatus === 'completed' ? colors.green(' └─ done') : finalStatus === 'killed' ? colors.yellow(' └─ killed') : colors.red(' └─ ' + finalStatus);
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
- break;
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
- break;
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
- break;
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
- // Other event types — quiet by default to keep the REPL readable
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.10",
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",