codehere 0.3.0-rc.5 → 0.3.0-rc.7

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 + quick start (no REPL; v0.3 is web-UI-first)
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]', 'Reserved — the PM intake lives at http://127.0.0.1:3773 after you run `codehere start`')
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
- console.error(error('Direct-query routing was removed in v0.3 (D7 trim).'));
53
- console.log(colors.dim('Use the PM intake instead:'));
54
- console.log(' ' + colors.cyan('codehere start') + ' → web UI at http://127.0.0.1:3773');
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 codehereDir = join(process.env.HOME || '', '.codehere');
63
- const settingsPath = join(process.env.HOME || '', '.claude', 'settings.json');
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
@@ -65,36 +65,94 @@ const ROUTING_RULES = [
65
65
  ];
66
66
  /** Default specialist when no routing rule matches. */
67
67
  const DEFAULT_SPECIALIST = 'claude-code';
68
+ /**
69
+ * Tokenize a prompt for trigger matching. Splits on any non-alphanumeric
70
+ * boundary (whitespace, hyphens, punctuation), lowercases everything. So:
71
+ *
72
+ * "Bug fix" → ["bug", "fix"]
73
+ * "set-up auth" → ["set", "up", "auth"] (trigger "set up" still matches via phrase below)
74
+ * "unit-test it" → ["unit", "test", "it"]
75
+ * "prefix matter" → ["prefix", "matter"] (does NOT match `fix`)
76
+ * "hotfixes" → ["hotfixes"] (does NOT match `fix`)
77
+ *
78
+ * Multi-word triggers ("add tests", "create new") are matched against the
79
+ * normalized prompt directly using word boundaries — so they catch
80
+ * "add tests" but not "added testing harness".
81
+ */
82
+ function tokenize(prompt) {
83
+ const tokens = new Set(prompt
84
+ .toLowerCase()
85
+ .split(/[^a-z0-9]+/)
86
+ .filter(Boolean));
87
+ // Normalized form for multi-word phrase matching: hyphens/underscores → space,
88
+ // collapse runs of whitespace. Leaves alphanumerics intact.
89
+ const normalized = ' ' + prompt.toLowerCase().replace(/[-_]+/g, ' ').replace(/\s+/g, ' ').trim() + ' ';
90
+ return { tokens, normalized };
91
+ }
92
+ /**
93
+ * Test whether a routing trigger matches a prompt. Single-word triggers must
94
+ * appear as whole tokens (no substring matches). Multi-word triggers must
95
+ * appear as a phrase in the normalized prompt (with leading/trailing spaces
96
+ * to enforce word boundaries on the ends).
97
+ */
98
+ function triggerMatches(trigger, tokens, normalized) {
99
+ const t = trigger.toLowerCase().trim();
100
+ if (!t)
101
+ return false;
102
+ if (t.includes(' ')) {
103
+ return normalized.includes(' ' + t + ' ');
104
+ }
105
+ return tokens.has(t);
106
+ }
68
107
  /**
69
108
  * Pick the right specialist for a given prompt.
70
109
  *
71
110
  * Algorithm:
72
- * 1. Walk routing rules in order.
73
- * 2. For the first rule whose triggers match, walk its `preferred` list.
74
- * 3. Return the first preferred adapter that's installed.
75
- * 4. If no rule matches OR no preferred adapter is installed, fall back to
76
- * `DEFAULT_SPECIALIST` if it's installed; otherwise the first-installed
77
- * adapter on the list.
111
+ * 1. Tokenize the prompt (whole-word matching, no substring false-positives).
112
+ * 2. Walk routing rules in order. Stop at the first rule whose triggers match.
113
+ * 3. Within that rule, return the first preferred adapter that's installed.
114
+ * 4. If that rule had a match but none of its preferred adapters are
115
+ * installed, return DEFAULT_SPECIALIST (or first installed) with a
116
+ * HONEST reason that names the matched rule and the fallback.
117
+ * 5. If no rule matched at all, return DEFAULT_SPECIALIST (or first
118
+ * installed) with a "no intent matched" reason.
78
119
  *
79
120
  * Returns null if NO specialist is installed (caller must handle).
121
+ *
122
+ * Known limitation: a prompt with multiple intents (e.g. "fix the bug and
123
+ * add tests") routes by the first matching rule, not the dominant intent.
124
+ * The v0.4 LLM classifier inherently handles dominance via confidence
125
+ * scoring; the rule-based path here keeps the deterministic-priority
126
+ * contract instead.
80
127
  */
81
128
  export function pickSpecialist(prompt, installedAgentIds) {
82
129
  if (installedAgentIds.length === 0) {
83
130
  return null;
84
131
  }
85
- const lowered = prompt.toLowerCase();
132
+ const installedSet = new Set(installedAgentIds);
133
+ const { tokens, normalized } = tokenize(prompt);
86
134
  for (const rule of ROUTING_RULES) {
87
- const matches = rule.triggers.some((t) => lowered.includes(t));
135
+ const matches = rule.triggers.some((t) => triggerMatches(t, tokens, normalized));
88
136
  if (!matches)
89
137
  continue;
138
+ // Rule matched. Walk preferred specialists in order.
90
139
  for (const candidate of rule.preferred) {
91
- if (installedAgentIds.includes(candidate)) {
140
+ if (installedSet.has(candidate)) {
92
141
  return { agentId: candidate, reason: rule.reason };
93
142
  }
94
143
  }
144
+ // Rule matched but none of its preferred specialists are installed.
145
+ // Fall through with an HONEST reason that names the matched rule + fallback,
146
+ // so the audit log records what actually happened (Codex review caught
147
+ // the prior version saying "default routing" here, which was a lie).
148
+ const fallbackId = installedSet.has(DEFAULT_SPECIALIST) ? DEFAULT_SPECIALIST : installedAgentIds[0];
149
+ return {
150
+ agentId: fallbackId,
151
+ reason: `${rule.reason} — but none of [${rule.preferred.join(', ')}] installed; falling back to ${fallbackId}`,
152
+ };
95
153
  }
96
- // No rule matched (or no preferred specialist installed) — default routing.
97
- if (installedAgentIds.includes(DEFAULT_SPECIALIST)) {
154
+ // No rule matched. Default routing.
155
+ if (installedSet.has(DEFAULT_SPECIALIST)) {
98
156
  return { agentId: DEFAULT_SPECIALIST, reason: 'default routing (no specific intent matched)' };
99
157
  }
100
158
  return { agentId: installedAgentIds[0], reason: 'fallback to first available specialist' };
@@ -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.5",
3
+ "version": "0.3.0-rc.7",
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",