codehere 0.5.0-alpha.6 → 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.
- package/README.md +18 -8
- package/dist/application/use-cases/generate-narration.js +111 -0
- package/dist/application/use-cases/generate-thread-title.js +58 -4
- package/dist/application/use-cases/narrator-prompt-template.js +158 -0
- package/dist/index.js +75 -6
- package/dist/infrastructure/cost/budget-config.js +113 -0
- package/dist/infrastructure/cost/spend-aggregator.js +147 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +280 -4
- package/dist/infrastructure/hooks/hook-mode.js +71 -0
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +43 -0
- package/dist/infrastructure/hooks/pre-prompt-scan.js +122 -0
- package/dist/infrastructure/orchestration/aider-adapter.js +28 -0
- package/dist/infrastructure/orchestration/artifacts-extractor.js +128 -8
- package/dist/infrastructure/orchestration/claude-code-adapter.js +1 -0
- package/dist/infrastructure/orchestration/codex-adapter.js +14 -0
- package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
- package/dist/infrastructure/orchestration/failover-policy.js +106 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
- package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
- package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
- package/dist/infrastructure/orchestration/session-manager.js +95 -9
- package/dist/infrastructure/protocols/mcp-server-impl.js +533 -14
- package/dist/infrastructure/protocols/ui-page.js +2598 -212
- package/dist/infrastructure/security/permissions-config.js +234 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +346 -24
- package/dist/infrastructure/storage/project-thread-reader.js +23 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
- package/dist/presentation/cli/commands/budget-command.js +79 -0
- package/dist/presentation/cli/commands/narrator-command.js +101 -0
- package/dist/presentation/cli/commands/permission-command.js +116 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
- package/dist/utils/typo-guard.js +77 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# codehere
|
|
2
2
|
|
|
3
|
-
> **
|
|
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
|
[](https://www.npmjs.com/package/codehere)
|
|
6
7
|
[](../LICENSE)
|
|
7
|
-
[]()
|
|
8
9
|
[]()
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
- **[
|
|
186
|
-
- **[
|
|
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
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 (
|
|
66
|
-
const trimmed =
|
|
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 '${
|
|
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
|