codehere 0.5.0-alpha.5 → 0.6.0-alpha.1

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.
Files changed (34) hide show
  1. package/README.md +18 -8
  2. package/dist/application/use-cases/generate-narration.js +111 -0
  3. package/dist/application/use-cases/generate-thread-title.js +58 -4
  4. package/dist/application/use-cases/narrator-prompt-template.js +158 -0
  5. package/dist/index.js +75 -6
  6. package/dist/infrastructure/cost/budget-config.js +113 -0
  7. package/dist/infrastructure/cost/spend-aggregator.js +147 -0
  8. package/dist/infrastructure/hooks/claude-sdk-hook.js +280 -4
  9. package/dist/infrastructure/hooks/hook-mode.js +71 -0
  10. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +43 -0
  11. package/dist/infrastructure/hooks/pre-prompt-scan.js +122 -0
  12. package/dist/infrastructure/orchestration/aider-adapter.js +28 -0
  13. package/dist/infrastructure/orchestration/artifacts-extractor.js +128 -8
  14. package/dist/infrastructure/orchestration/claude-code-adapter.js +1 -0
  15. package/dist/infrastructure/orchestration/codex-adapter.js +14 -0
  16. package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
  17. package/dist/infrastructure/orchestration/failover-policy.js +106 -0
  18. package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
  19. package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
  20. package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
  21. package/dist/infrastructure/orchestration/session-manager.js +117 -10
  22. package/dist/infrastructure/orchestration/session-persistence.js +28 -0
  23. package/dist/infrastructure/protocols/mcp-server-impl.js +564 -13
  24. package/dist/infrastructure/protocols/ui-page.js +2728 -245
  25. package/dist/infrastructure/security/permissions-config.js +234 -0
  26. package/dist/infrastructure/storage/project-thread-mirror.js +379 -24
  27. package/dist/infrastructure/storage/project-thread-reader.js +23 -1
  28. package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
  29. package/dist/presentation/cli/commands/budget-command.js +79 -0
  30. package/dist/presentation/cli/commands/narrator-command.js +101 -0
  31. package/dist/presentation/cli/commands/permission-command.js +116 -0
  32. package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
  33. package/dist/utils/typo-guard.js +77 -17
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # codehere
2
2
 
3
- > **Every AI action, traced. Every agent, verified.**
3
+ > **Your AI agents shouldn't lock you in.**
4
+ > Codehere keeps your memory, audit, safety, and reports portable across every coding agent you use.
4
5
 
5
6
  [![npm](https://img.shields.io/npm/v/codehere)](https://www.npmjs.com/package/codehere)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow)](../LICENSE)
7
- [![Tests](https://img.shields.io/badge/tests-443%2F443-brightgreen)]()
8
+ [![Tests](https://img.shields.io/badge/tests-668%2F668-brightgreen)]()
8
9
  [![Vulnerabilities](https://img.shields.io/badge/vulnerabilities-0-brightgreen)]()
9
10
 
10
- codehere is **a PM for your AI coding agents.** Delegate a task. codehere picks the right specialist (Claude Code, Codex, Aider, OpenCode, or Gemini), spawns it, streams its work back to you, and hands you a plain-English Final Report with a **trust score on every file changed**. Today, Claude Code gets full pre-execution safety-hook enforcement on every tool call; other adapters are session-traced and reported, with per-tool hook parity planned for v0.4.
11
+ **Codehere is the vendor-neutral layer between you and the coding agents you already use** — Claude Code, Codex, Aider, Gemini, OpenCode. One `npm install`. Local-first, OSS, no account, no IAM, no compliance prereqs. Switch agent, keep your memory. End the day with one Final Report covering everything they did.
11
12
 
12
- One manager, many specialists, every action recorded.
13
+ Today, Claude Code gets full pre-execution safety-hook enforcement on every tool call; the other adapters are session-traced and reported, with per-tool hook parity in progress for v0.6. The vendor-neutral *memory*, *audit log*, and *Final Report* surfaces ship across all 5 adapters today.
14
+
15
+ The enterprise tier is owned by [OpenAI Frontier](https://openai.com/index/introducing-openai-frontier/) (8-14 weeks of deploy prereqs, sales-team gated) and [Anthropic Cowork](https://www.anthropic.com/product/claude-cowork) (paid Claude subscription, single-vendor). Codehere fills the gap they structurally can't reach: **the individual / small-team / no-IT-department tier, vendor-neutral**.
16
+
17
+ > See [`docs/STRATEGIC_VISION.md`](../docs/STRATEGIC_VISION.md) Appendix C and [`docs/figures/positioning-matrix.svg`](../docs/figures/positioning-matrix.svg) for the full positioning chart.
18
+
19
+ One layer, every agent, your work stays portable.
13
20
 
14
21
  ---
15
22
 
@@ -22,7 +29,7 @@ One manager, many specialists, every action recorded.
22
29
 
23
30
  | | | |
24
31
  |---|---|---|
25
- | 1 | **Install** | `npm install -g codehere` (Node 20+) |
32
+ | 1 | **Install** | `npm install -g codehere@alpha` (Node 20+) — current alpha track is v0.5; `@latest` still on v0.4 |
26
33
  | 2 | **Detect environment** | `codehere setup` — checks API key, finds your repo |
27
34
  | 3 | **Install the safety hook** | `codehere hook install` — every Claude Code tool call now audited |
28
35
  | 4 | **Install at least one specialist** | Claude Code: `npm i -g @anthropic-ai/claude-code` (Codex / Aider / OpenCode / Gemini also supported · see [docs/integrations/](../docs/integrations/) for Cursor MCP setup) |
@@ -142,6 +149,9 @@ codehere status · dashboard · sessions, blocked calls, audit trend
142
149
  codehere trust [path] · static trust scan · score 0-100, grade A-F
143
150
  codehere report · generate a shareable markdown audit report
144
151
  codehere hook · install/uninstall/inspect the PreToolUse safety hook
152
+ codehere permission · manage this folder's allow-list (skip false-positive blocks)
153
+ codehere budget · daily-spend ceiling · `set <usd>` blocks paid spawns at the cap
154
+ codehere narrator · customize the PM narrator prompt (~/.codehere/narrator-prompt.md)
145
155
  codehere setup · interactive first-run setup wizard
146
156
  codehere config · manage configuration and preferences
147
157
  codehere health · system health check
@@ -182,10 +192,10 @@ Exposes 4 read-only tools to any MCP client (Claude Desktop, Cursor, Zed, Contin
182
192
 
183
193
  For depth beyond this README:
184
194
 
185
- - **[CODEHERE.md](../CODEHERE.md)** · product spec · positioning, six-ring adoption funnel, F0–F6 foundations, defensibility, GTM, revenue model
186
- - **[EXPERIENCE.md](../EXPERIENCE.md)** · the human story · how engineering teams, CISOs, and regulated-industry buyers adopt codehere
187
- - **[AUDIT.md](../AUDIT.md)** · pre-publish audit report · methodology, findings, remediation
195
+ - **[docs/STRATEGIC_VISION.md](../docs/STRATEGIC_VISION.md)** · 20-year invariants + v4 positioning (individual-tier vendor-neutral) · figures in `docs/figures/`
196
+ - **[docs/PRD.md](../docs/PRD.md)** · master decision document · features, architecture, roadmap
188
197
  - **[docs/SECURITY_AUDIT_2026-05-01.md](../docs/SECURITY_AUDIT_2026-05-01.md)** · first-party security audit · OWASP Top 10 + STRIDE · 0 critical / 0 high · 2 medium fixed inline · methodology disclosed · reproducible
198
+ - **[docs/archive/](../docs/archive/)** · earlier living docs (CODEHERE spec, EXPERIENCE story, AUDIT) preserved for history
189
199
  - **[CONTRIBUTING.md](../CONTRIBUTING.md)** · how to contribute · setup, commit discipline, testing philosophy
190
200
  - **[SECURITY.md](../SECURITY.md)** · responsible disclosure · `security@codehere.uk`
191
201
  - **[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)** · community standards
@@ -0,0 +1,111 @@
1
+ /**
2
+ * PM Narration — v0.6 M3 mid-task summary.
3
+ *
4
+ * While a task is in flight, periodically generates a one-paragraph
5
+ * plain-English summary of what the agent has done so far. Fires on
6
+ * step-count or elapsed-time thresholds (caller decides when).
7
+ *
8
+ * Per claim_user_persona HIGH: non-technical users primary. They don't
9
+ * read tool calls; they read narration. Mid-task narration tells them
10
+ * "the agent is currently editing your auth file and ran the tests"
11
+ * instead of forcing them to interpret a list of Bash/Edit/Read steps.
12
+ *
13
+ * Per feedback_overconfidence_guard.md: narration is an LLM call.
14
+ * Hallucination is possible. Surface MUST frame it as "narrator says…"
15
+ * not authoritative truth. The PROMPT_TEMPLATE includes the same
16
+ * anti-hallucination rule as generate-final-report (ground every claim
17
+ * in the structured data).
18
+ *
19
+ * Provider strategy: same as generate-final-report (try claude-free
20
+ * first, fall back to claude). Returns null if neither is installed.
21
+ */
22
+ import { spawn } from 'child_process';
23
+ import { loadTemplate, renderTemplate } from './narrator-prompt-template.js';
24
+ /** v0.6 S6 — prompt template lifted into ~/.codehere/narrator-prompt.md so
25
+ * the user can edit the agent's voice. loadTemplate() falls back to the
26
+ * baked-in default on missing file or missing required placeholders, so
27
+ * the prompt is never silently broken. See narrator-prompt-template.ts. */
28
+ function buildPrompt(input) {
29
+ const { template } = loadTemplate();
30
+ return renderTemplate(template, input);
31
+ }
32
+ function selectProviderBinary() {
33
+ // Same precedence as generate-final-report: env override → claude-free → claude.
34
+ const override = process.env.CODEHERE_NARRATOR_BINARY?.trim();
35
+ if (override)
36
+ return [override];
37
+ return ['claude-free', 'claude'];
38
+ }
39
+ export async function generateNarration(input) {
40
+ const prompt = buildPrompt(input);
41
+ const candidates = selectProviderBinary();
42
+ for (const binary of candidates) {
43
+ const result = await tryProvider(binary, prompt, input.sessionId, input.steps.length);
44
+ if (result)
45
+ return result;
46
+ }
47
+ return null;
48
+ }
49
+ function tryProvider(binary, prompt, sessionId, stepCount) {
50
+ return new Promise((resolve) => {
51
+ const startedAt = Date.now();
52
+ let stdout = '';
53
+ let stderr = '';
54
+ let resolved = false;
55
+ const finish = (val) => {
56
+ if (resolved)
57
+ return;
58
+ resolved = true;
59
+ resolve(val);
60
+ };
61
+ let child;
62
+ try {
63
+ // Same shape as generate-final-report's tryProvider — claude --bare -p.
64
+ child = spawn(binary, ['--bare', '-p', prompt], {
65
+ stdio: ['ignore', 'pipe', 'pipe'],
66
+ });
67
+ }
68
+ catch {
69
+ finish(null);
70
+ return;
71
+ }
72
+ // 20s timeout — narration should be SNAPPIER than Final Report
73
+ // (which is 60s). Mid-task = the user is actively waiting.
74
+ const timeout = setTimeout(() => {
75
+ try {
76
+ child.kill();
77
+ }
78
+ catch { /* ignore */ }
79
+ finish(null);
80
+ }, 20_000);
81
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf-8'); });
82
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf-8'); });
83
+ child.on('close', (code) => {
84
+ clearTimeout(timeout);
85
+ if (code !== 0) {
86
+ // Non-zero usually means binary missing OR the model refused. Either
87
+ // way silent fail; caller treats null as "no narration this cycle".
88
+ void stderr;
89
+ void sessionId;
90
+ finish(null);
91
+ return;
92
+ }
93
+ const summary = stdout.trim();
94
+ if (!summary || summary.length < 10) {
95
+ finish(null);
96
+ return;
97
+ }
98
+ finish({
99
+ summary,
100
+ generatedAt: Date.now(),
101
+ durationMs: Date.now() - startedAt,
102
+ stepCountAtSnapshot: stepCount,
103
+ });
104
+ });
105
+ child.on('error', () => {
106
+ clearTimeout(timeout);
107
+ finish(null);
108
+ });
109
+ });
110
+ }
111
+ //# sourceMappingURL=generate-narration.js.map
@@ -88,9 +88,21 @@ function tryProvider(binary, prompt) {
88
88
  });
89
89
  });
90
90
  }
91
- /** Strip whitespace / quotes / trailing punctuation. Cap at 80 chars (safety). */
92
- function sanitizeTitle(raw) {
93
- const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean);
91
+ /**
92
+ * Strip whitespace / quotes / trailing punctuation. Cap at 80 chars.
93
+ *
94
+ * Garbage rejection (added 2026-05-06 after dogfood Bug A4): the underlying
95
+ * `claude --bare` call can return error-shaped text on stdout with exit 0
96
+ * (rate-limit messages, auth errors, refusals). Pre-fix those flowed straight
97
+ * into the project sidebar as titles like "Request ID: req_xyz...". We now
98
+ * detect obvious error / refusal patterns and return '' so the caller falls
99
+ * back to the cwd-derived title.
100
+ */
101
+ export function sanitizeTitle(raw) {
102
+ const lines = raw
103
+ .split('\n')
104
+ .map((l) => l.trim())
105
+ .filter(Boolean);
94
106
  if (lines.length === 0)
95
107
  return '';
96
108
  let title = lines[lines.length - 1]; // last non-empty line is the answer
@@ -101,7 +113,49 @@ function sanitizeTitle(raw) {
101
113
  // Cap at 80 chars
102
114
  if (title.length > 80)
103
115
  title = title.slice(0, 77) + '...';
104
- return title.trim();
116
+ title = title.trim();
117
+ if (!title)
118
+ return '';
119
+ // Garbage rejection — patterns that indicate an error response, NOT a title.
120
+ // Each pattern matches a real failure mode observed in dogfood:
121
+ // - "Request ID: req_..." → API error envelope leaking through
122
+ // - "Error:" / "ERROR" / "error " → generic error prefix
123
+ // - "I cannot" / "I can't" → model refusal
124
+ // - "I apologize" / "I'm sorry" → model refusal
125
+ // - "Rate limit" / "rate-limited" → quota error
126
+ // - "Authentication" / "Unauthorized" / "401" / "403" → auth failure
127
+ // - Long sentences (>10 words) → not a title; model ignored the rules
128
+ // - Contains http:// or https:// → URL leakage from error envelope
129
+ const lower = title.toLowerCase();
130
+ const garbageMarkers = [
131
+ /^request\s+id\s*[:=]/i,
132
+ /^error\b/i,
133
+ /\berror[:\s]/i,
134
+ /^i\s+(cannot|can'?t|am\s+unable|won'?t)\b/i,
135
+ /^i\s+(apologi[sz]e|am\s+sorry)\b/i,
136
+ /^i'?m\s+sorry\b/i,
137
+ /\brate\s*[-_]?\s*limit/i,
138
+ /\b(authentication|unauthori[sz]ed|forbidden)\b/i,
139
+ /\b(401|403|429|500|502|503)\b/i,
140
+ /https?:\/\//i,
141
+ /api[_-]?key/i,
142
+ ];
143
+ if (garbageMarkers.some((re) => re.test(lower))) {
144
+ if (process.env.CODEHERE_DEBUG === '1') {
145
+ console.error(`[title-agent] rejected garbage title: ${title.slice(0, 60)}`);
146
+ }
147
+ return '';
148
+ }
149
+ // Length sanity: a real title is 4-6 words per the prompt; allow up to 10
150
+ // for slack. Anything longer is the model ignoring instructions.
151
+ const wordCount = title.split(/\s+/).filter(Boolean).length;
152
+ if (wordCount > 10) {
153
+ if (process.env.CODEHERE_DEBUG === '1') {
154
+ console.error(`[title-agent] rejected long title (${wordCount} words): ${title.slice(0, 60)}`);
155
+ }
156
+ return '';
157
+ }
158
+ return title;
105
159
  }
106
160
  /**
107
161
  * Generate a thread title from the first user prompt + cwd context.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * v0.6 S6 — editable PM narrator prompt template.
3
+ *
4
+ * The mid-task narration (M3) used to ship a hard-coded prompt baked into
5
+ * generate-narration.ts. That made the agent's "voice" un-tunable: a user
6
+ * who wanted shorter / longer / more-jargony / less-jargony narration
7
+ * had no lever short of forking the source.
8
+ *
9
+ * S6 lifts the template into ~/.codehere/narrator-prompt.md, a plain
10
+ * Markdown file the user edits with whatever editor they like. The file
11
+ * uses `{{placeholder}}` substitution for the runtime values:
12
+ * {{prompt}} — the user's original task ask (truncated 500ch)
13
+ * {{agent}} — which adapter is running ("claude-code" etc.)
14
+ * {{elapsed_seconds}} — how long the task has been running
15
+ * {{step_count}} — how many steps completed/in-progress
16
+ * {{steps}} — last 15 steps, " N. [state] label"
17
+ *
18
+ * If the file is missing OR the user-edited file is missing required
19
+ * placeholders, we fall back to the baked-in DEFAULT_TEMPLATE so a typo
20
+ * never breaks narration silently.
21
+ *
22
+ * Per claim_user_persona HIGH: the narrator IS the product voice for
23
+ * non-technical users. Letting them control it is a v1 promise.
24
+ *
25
+ * Per feedback_overconfidence_guard.md: the DEFAULT_TEMPLATE keeps the
26
+ * anti-hallucination rule + "frame as narrator not authority" cue. Users
27
+ * editing the template can drop those rules at their own risk; the file
28
+ * comment warns about it.
29
+ */
30
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
31
+ import { dirname, join } from 'path';
32
+ import { homedir } from 'os';
33
+ /** The required placeholders. If a user-edited template is missing ANY of
34
+ * these we fall back to the default so the rendered prompt is never
35
+ * silently broken. {{step_count}} is omitted from the required list — it's
36
+ * derivable from {{steps}} and a custom template might intentionally drop it. */
37
+ const REQUIRED_PLACEHOLDERS = ['{{prompt}}', '{{agent}}', '{{elapsed_seconds}}', '{{steps}}'];
38
+ export const DEFAULT_TEMPLATE = `<!--
39
+ codehere narrator prompt — drives the mid-task summary the user sees as
40
+ "narrator says…" while their AI coding agent is working. Edit freely.
41
+
42
+ Available placeholders (substituted at render time):
43
+ {{prompt}} user's original task ask
44
+ {{agent}} which adapter is running
45
+ {{elapsed_seconds}} how long the task has been running
46
+ {{step_count}} number of steps completed/in-progress
47
+ {{steps}} last 15 steps, formatted as " N. [state] label"
48
+
49
+ REQUIRED: {{prompt}}, {{agent}}, {{elapsed_seconds}}, {{steps}}.
50
+ Missing any of those → codehere falls back to its baked-in default.
51
+
52
+ Reset to default any time with: codehere narrator reset
53
+ -->
54
+
55
+ You are a PM giving a 1-sentence-or-2 mid-task update to a non-technical user
56
+ about what their AI coding agent is doing RIGHT NOW. Write ONE short paragraph
57
+ (2-3 sentences). No headers, no bullet lists, no markdown.
58
+
59
+ CRITICAL anti-hallucination rule: ground every claim in the structured data
60
+ below. Do not invent file names, status, or actions not in the steps list.
61
+ If you don't know what something does, say "the agent is working on …" with
62
+ the literal target. Better to under-claim than to over-claim.
63
+
64
+ Tone: like a colleague checking in. "It's reading the auth file and ran the
65
+ tests — looks like 2 failed so far." Not robotic, not breathless.
66
+
67
+ Reference frame for the user (not for the LLM): the user did NOT see this
68
+ list directly; you're translating it for them. The user only sees your
69
+ paragraph.
70
+
71
+ ────────── Structured data (the only source of truth) ──────────
72
+ Task: "{{prompt}}"
73
+ Agent: {{agent}}
74
+ Elapsed: {{elapsed_seconds}}s
75
+ Steps so far ({{step_count}}):
76
+ {{steps}}
77
+
78
+ Write the paragraph now. Do not preface with "Update:" or anything similar.
79
+ Plain prose. No "I'm currently…" — narrate the AGENT, not yourself.
80
+ `;
81
+ function templateFile() {
82
+ return join(homedir(), '.codehere', 'narrator-prompt.md');
83
+ }
84
+ /** Read the user-customized template if present and valid. Falls back to
85
+ * baked-in default on missing file, parse failure, or missing required
86
+ * placeholders (so a typo never silently breaks narration). */
87
+ export function loadTemplate() {
88
+ const path = templateFile();
89
+ if (!existsSync(path))
90
+ return { source: 'default', template: DEFAULT_TEMPLATE };
91
+ let raw;
92
+ try {
93
+ raw = readFileSync(path, 'utf-8');
94
+ }
95
+ catch {
96
+ return { source: 'default', template: DEFAULT_TEMPLATE };
97
+ }
98
+ for (const ph of REQUIRED_PLACEHOLDERS) {
99
+ if (!raw.includes(ph)) {
100
+ return { source: 'default', template: DEFAULT_TEMPLATE };
101
+ }
102
+ }
103
+ return { source: 'user', template: raw };
104
+ }
105
+ /** Persist the default template to disk so the user has a starting point
106
+ * to edit. Used by `codehere narrator edit`. Doesn't overwrite an existing
107
+ * user-edited file unless `force` is set. */
108
+ export function writeDefaultTemplate(force = false) {
109
+ const path = templateFile();
110
+ if (!force && existsSync(path))
111
+ return { wrote: false, path };
112
+ const dir = dirname(path);
113
+ if (!existsSync(dir))
114
+ mkdirSync(dir, { recursive: true });
115
+ writeFileSync(path, DEFAULT_TEMPLATE, 'utf-8');
116
+ return { wrote: true, path };
117
+ }
118
+ /** Delete the user-customized template. Future loads use the baked-in
119
+ * default. Idempotent: missing file is a no-op success. */
120
+ export function resetTemplate() {
121
+ const path = templateFile();
122
+ if (!existsSync(path))
123
+ return { removed: false, path };
124
+ try {
125
+ unlinkSync(path);
126
+ return { removed: true, path };
127
+ }
128
+ catch {
129
+ return { removed: false, path };
130
+ }
131
+ }
132
+ /** Substitute the runtime values into the template. Same {{placeholder}}
133
+ * protocol as the template-file comments document. */
134
+ export function renderTemplate(template, input) {
135
+ const stepsBlock = input.steps
136
+ .slice(-15)
137
+ .map((s, idx) => ` ${idx + 1}. [${s.state}] ${(s.label ?? '').slice(0, 120)}`)
138
+ .join('\n');
139
+ const replacements = {
140
+ '{{prompt}}': input.prompt.slice(0, 500),
141
+ '{{agent}}': input.agent,
142
+ '{{elapsed_seconds}}': String(Math.round(input.elapsedMs / 1000)),
143
+ '{{step_count}}': String(input.steps.length),
144
+ '{{steps}}': stepsBlock,
145
+ };
146
+ let out = template;
147
+ for (const [k, v] of Object.entries(replacements)) {
148
+ // Replace ALL occurrences. Use split/join to avoid regex escaping
149
+ // pain on the curly braces.
150
+ out = out.split(k).join(v);
151
+ }
152
+ return out;
153
+ }
154
+ /** Path to the user-customized template (for CLI status display). */
155
+ export function templatePath() {
156
+ return templateFile();
157
+ }
158
+ //# sourceMappingURL=narrator-prompt-template.js.map
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ import { executeConfigCommand } from './presentation/cli/commands/config-command
23
23
  import { executeHelpCommand } from './presentation/cli/commands/help-command.js';
24
24
  import { executeStatusCommand } from './presentation/cli/commands/status-command.js';
25
25
  import { executeHookCommand } from './presentation/cli/commands/hook-command.js';
26
+ import { executePermissionCommand } from './presentation/cli/commands/permission-command.js';
27
+ import { executeBudgetCommand } from './presentation/cli/commands/budget-command.js';
28
+ import { executeNarratorCommand } from './presentation/cli/commands/narrator-command.js';
26
29
  import { executeTrustCommand } from './presentation/cli/commands/trust-command.js';
27
30
  import { executeReportCommand } from './presentation/cli/commands/report-command.js';
28
31
  import { executeScanToolCommand } from './presentation/cli/commands/scan-tool-command.js';
@@ -41,7 +44,7 @@ catch {
41
44
  }
42
45
  config();
43
46
  import { getVersion } from './utils/version.js';
44
- import { checkTypo } from './utils/typo-guard.js';
47
+ import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
45
48
  const program = new Command();
46
49
  program
47
50
  .name('codehere')
@@ -58,18 +61,24 @@ program
58
61
  // codehere offers TWO surfaces, both first-class: the CLI REPL (here) and the
59
62
  // web UI (`codehere start`). Users pick whichever fits their workflow.
60
63
  program
61
- .argument('[query]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
62
- .action(async (query) => {
64
+ // Variadic so multi-token input is preserved end-to-end. Without this,
65
+ // `codehere fix the bug` would silently drop "the bug" and only delegate
66
+ // "fix" — and `codehere daemon start` would never see "start", letting the
67
+ // typo guard miss the unknown-command shape. See typo-guard.ts.
68
+ .argument('[query...]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
69
+ .action(async (queryParts) => {
70
+ const joined = (queryParts && queryParts.length > 0) ? queryParts.join(' ') : '';
63
71
  // One-shot delegate: `codehere "review src/auth.ts"` runs that task and exits.
64
72
  // Useful for scripting or quick one-offs without entering the REPL.
65
- if (query && query.trim()) {
66
- const trimmed = query.trim();
73
+ if (joined && joined.trim()) {
74
+ const trimmed = joined.trim();
67
75
  // Typo guard — see src/utils/typo-guard.ts for heuristics.
68
76
  // Returns suggestions when input looks like a typo; null falls through.
69
77
  const known = program.commands.map((c) => c.name());
78
+ const firstToken = trimmed.split(/\s+/)[0];
70
79
  const suggestions = checkTypo(trimmed, known);
71
80
  if (suggestions) {
72
- console.error(`codehere: unknown command '${trimmed}'.`);
81
+ console.error(`codehere: unknown command '${firstToken}'.`);
73
82
  console.error('');
74
83
  console.error(' Did you mean:');
75
84
  for (const s of suggestions)
@@ -78,6 +87,21 @@ program
78
87
  console.error(` If you meant to ask a question, quote it: codehere "${trimmed}"`);
79
88
  process.exit(1);
80
89
  }
90
+ // Second guard — `cmd subcmd` shape (e.g. `daemon start`) where the first
91
+ // token isn't close enough for typo suggestions but is clearly a command
92
+ // attempt rather than a natural-language prompt. Without this, the input
93
+ // silently spawns an LLM task on the user's behalf and burns API tokens.
94
+ const unknownFirst = looksLikeUnknownCommand(trimmed, known);
95
+ if (unknownFirst) {
96
+ console.error(`codehere: unknown command '${unknownFirst}'.`);
97
+ console.error('');
98
+ console.error(' Run `codehere help` to see available commands.');
99
+ console.error('');
100
+ console.error(' If you meant to delegate this as a task, either:');
101
+ console.error(` codehere task ${trimmed}`);
102
+ console.error(` codehere "${trimmed}"`);
103
+ process.exit(1);
104
+ }
81
105
  const { runOneShot } = await import('./presentation/cli/repl.js');
82
106
  await runOneShot(trimmed);
83
107
  return;
@@ -343,6 +367,51 @@ program
343
367
  process.exit(1);
344
368
  }
345
369
  });
370
+ // ── permission (P18 — per-cwd allow/deny list for the trust-scorer) ─────────
371
+ program
372
+ .command('permission')
373
+ .description("Manage this folder's .codehere/permissions.yaml allow/deny list")
374
+ .argument('[subcommand]', 'list | allow | deny | remove')
375
+ .argument('[pattern]', 'the substring pattern (for allow/deny/remove)')
376
+ .option('--reason <text>', 'Why this pattern is allow-listed (recorded in audit)')
377
+ .action(async (subcommand, pattern, options) => {
378
+ try {
379
+ await executePermissionCommand(subcommand, pattern, { reason: options?.reason });
380
+ }
381
+ catch (err) {
382
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
383
+ process.exit(1);
384
+ }
385
+ });
386
+ // ── narrator (S6 — editable PM narrator prompt) ──────────────────────────────
387
+ program
388
+ .command('narrator')
389
+ .description('Customize the PM narrator prompt template (~/.codehere/narrator-prompt.md)')
390
+ .argument('[subcommand]', 'show | edit | reset')
391
+ .action(async (subcommand) => {
392
+ try {
393
+ await executeNarratorCommand(subcommand);
394
+ }
395
+ catch (err) {
396
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
397
+ process.exit(1);
398
+ }
399
+ });
400
+ // ── budget (S3 — daily-spend ceiling enforcement) ────────────────────────────
401
+ program
402
+ .command('budget')
403
+ .description('Set or check the daily spend budget for spawns')
404
+ .argument('[subcommand]', 'show | set | off')
405
+ .argument('[value]', 'amount in USD (for `set`)')
406
+ .action(async (subcommand, value) => {
407
+ try {
408
+ await executeBudgetCommand(subcommand, value);
409
+ }
410
+ catch (err) {
411
+ console.error(error('Error:'), err instanceof Error ? err.message : String(err));
412
+ process.exit(1);
413
+ }
414
+ });
346
415
  // ── serve (MCP server) ───────────────────────────────────────────────────────
347
416
  program
348
417
  .command('serve')
@@ -0,0 +1,113 @@
1
+ /**
2
+ * v0.6 S3 slice 2 — daily spend budget config.
3
+ *
4
+ * Reads ~/.codehere/budget.yaml. The pre-spawn check in /api/task compares
5
+ * computeSpendToday().totalUsd against `daily_usd`; over-budget refuses with
6
+ * a friendly message instead of silently spawning another paid call.
7
+ *
8
+ * Why this is the v1 promise · claim_market_position v3 lists "cost-bounded
9
+ * execution" as one of the few capture-strategy edges still possibly open.
10
+ * Slice 1 surfaced spend; slice 2 enforces it. Together they make the edge
11
+ * demonstrable in 60 seconds: set a $1 budget, spawn until you hit it,
12
+ * watch codehere refuse the next spawn.
13
+ *
14
+ * Schema (~/.codehere/budget.yaml):
15
+ *
16
+ * daily_usd: 5.00 # null / omitted = no budget enforcement
17
+ * warn_at_pct: 70 # default 70 — chip turns yellow at 70%
18
+ *
19
+ * Single-file, hand-editable, plain YAML. Per claim_user_persona HIGH:
20
+ * non-technical users can edit it directly. Per overconfidence-guard:
21
+ * the budget is a USER ceiling, not a codehere guarantee — we can't
22
+ * see the actual provider invoice in real time.
23
+ */
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
25
+ import { dirname, join } from 'path';
26
+ import { homedir } from 'os';
27
+ const DEFAULT_CONFIG = {
28
+ dailyUsd: null,
29
+ warnAtPct: 70,
30
+ };
31
+ function configFile() {
32
+ return join(homedir(), '.codehere', 'budget.yaml');
33
+ }
34
+ /** Parse a tiny subset of YAML — `key: value` pairs, # comments, blank lines.
35
+ * No nesting, no lists, no multi-line. Same approach as permissions-config:
36
+ * keep the dependency surface zero, schema simple. */
37
+ function parseYaml(content) {
38
+ const out = {};
39
+ for (const rawLine of content.split('\n')) {
40
+ const line = rawLine.replace(/#.*$/, '').trim();
41
+ if (!line)
42
+ continue;
43
+ const colonIdx = line.indexOf(':');
44
+ if (colonIdx === -1)
45
+ continue;
46
+ const key = line.slice(0, colonIdx).trim();
47
+ const val = line.slice(colonIdx + 1).trim();
48
+ if (key)
49
+ out[key] = val;
50
+ }
51
+ return out;
52
+ }
53
+ /** Read the budget config from disk. Missing file or parse failure → defaults
54
+ * (no enforcement). Best-effort: never throws — a corrupt file should not
55
+ * break the spawn path. */
56
+ export function loadBudget() {
57
+ const path = configFile();
58
+ if (!existsSync(path))
59
+ return { ...DEFAULT_CONFIG };
60
+ try {
61
+ const raw = readFileSync(path, 'utf-8');
62
+ const parsed = parseYaml(raw);
63
+ const cfg = { ...DEFAULT_CONFIG };
64
+ if ('daily_usd' in parsed) {
65
+ const n = Number(parsed.daily_usd);
66
+ cfg.dailyUsd = Number.isFinite(n) && n > 0 ? n : null;
67
+ }
68
+ if ('warn_at_pct' in parsed) {
69
+ const n = Number(parsed.warn_at_pct);
70
+ cfg.warnAtPct = Number.isFinite(n) && n > 0 && n <= 100 ? n : DEFAULT_CONFIG.warnAtPct;
71
+ }
72
+ return cfg;
73
+ }
74
+ catch {
75
+ return { ...DEFAULT_CONFIG };
76
+ }
77
+ }
78
+ /** Persist the budget config. Creates ~/.codehere/ if missing. Comments
79
+ * in the emitted YAML help a non-technical user edit it later. */
80
+ export function saveBudget(cfg) {
81
+ const path = configFile();
82
+ const dir = dirname(path);
83
+ if (!existsSync(dir))
84
+ mkdirSync(dir, { recursive: true });
85
+ const lines = [
86
+ '# codehere daily-spend budget. Edit by hand or via `codehere budget set <amount>`.',
87
+ '# daily_usd: hard ceiling for today\'s estimated spend. Set to a number to enable.',
88
+ '# Leave commented out / omit to disable budget enforcement entirely.',
89
+ ];
90
+ if (cfg.dailyUsd !== null) {
91
+ lines.push(`daily_usd: ${cfg.dailyUsd}`);
92
+ }
93
+ else {
94
+ lines.push('# daily_usd: 5.00');
95
+ }
96
+ lines.push(`warn_at_pct: ${cfg.warnAtPct}`);
97
+ lines.push('');
98
+ writeFileSync(path, lines.join('\n'), 'utf-8');
99
+ }
100
+ /** True when today's estimated spend has hit or exceeded the daily ceiling.
101
+ * Returns false when no budget is set (no enforcement). */
102
+ export function isOverBudget(spentTodayUsd, cfg = loadBudget()) {
103
+ if (cfg.dailyUsd === null)
104
+ return false;
105
+ return spentTodayUsd >= cfg.dailyUsd;
106
+ }
107
+ /** Percentage of today's budget consumed (0-100, or null when no budget set). */
108
+ export function budgetUsedPct(spentTodayUsd, cfg = loadBudget()) {
109
+ if (cfg.dailyUsd === null || cfg.dailyUsd === 0)
110
+ return null;
111
+ return Math.min(100, Math.round((spentTodayUsd / cfg.dailyUsd) * 100));
112
+ }
113
+ //# sourceMappingURL=budget-config.js.map