codehere 0.5.0-alpha.6 → 0.6.0

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 (39) 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 +89 -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/context-handoff.js +143 -0
  17. package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
  18. package/dist/infrastructure/orchestration/failover-config.js +77 -0
  19. package/dist/infrastructure/orchestration/failover-policy.js +106 -0
  20. package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
  21. package/dist/infrastructure/orchestration/graphify-context.js +256 -0
  22. package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
  23. package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
  24. package/dist/infrastructure/orchestration/session-manager.js +515 -15
  25. package/dist/infrastructure/protocols/cloud-sync.js +208 -0
  26. package/dist/infrastructure/protocols/mcp-server-impl.js +655 -24
  27. package/dist/infrastructure/protocols/ui-page-v2.js +505 -0
  28. package/dist/infrastructure/protocols/ui-page.js +2872 -220
  29. package/dist/infrastructure/security/permissions-config.js +234 -0
  30. package/dist/infrastructure/storage/project-thread-mirror.js +371 -39
  31. package/dist/infrastructure/storage/project-thread-reader.js +23 -1
  32. package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
  33. package/dist/presentation/cli/commands/budget-command.js +79 -0
  34. package/dist/presentation/cli/commands/narrator-command.js +101 -0
  35. package/dist/presentation/cli/commands/permission-command.js +116 -0
  36. package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
  37. package/dist/presentation/cli/repl.js +29 -0
  38. package/dist/utils/typo-guard.js +77 -17
  39. package/package.json +2 -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')
@@ -411,10 +480,12 @@ program
411
480
  .option('-p, --port <number>', 'Port to listen on', '3773')
412
481
  .option('--host <host>', 'Host to bind to', '127.0.0.1')
413
482
  .option('--no-open', "Don't auto-open the browser")
483
+ .option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
414
484
  .action(async (options) => {
415
485
  const port = parseInt(options?.port ?? '3773', 10);
416
486
  const host = options?.host ?? '127.0.0.1';
417
487
  const shouldOpen = options?.open !== false;
488
+ const cloud = options?.cloud === true;
418
489
  const url = `http://${host}:${port}/`;
419
490
  const openBrowser = async () => {
420
491
  if (!shouldOpen)
@@ -482,11 +553,23 @@ program
482
553
  console.log(colors.bold('Tasks:') + ' ' + url);
483
554
  console.log(colors.bold('History:') + ' ' + `http://${host}:${port}/history`);
484
555
  console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
556
+ if (cloud) {
557
+ console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
558
+ }
485
559
  newline();
486
560
  console.log(colors.dim(' Press Ctrl+C to stop'));
487
561
  newline();
562
+ // Start cloud sync if enabled
563
+ if (cloud) {
564
+ const { startCloudSync } = await import('./infrastructure/protocols/cloud-sync.js');
565
+ startCloudSync();
566
+ }
488
567
  await openBrowser();
489
568
  process.on('SIGINT', () => {
569
+ if (cloud) {
570
+ const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
571
+ stopCloudSync();
572
+ }
490
573
  server.close();
491
574
  console.log(colors.dim('\ncodehere stopped'));
492
575
  process.exit(0);