codehere 0.6.1 → 0.7.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.
- package/README.md +5 -3
- package/dist/application/use-cases/domain-trust.js +110 -0
- package/dist/application/use-cases/generate-final-report.js +84 -5
- package/dist/application/use-cases/generate-narration.js +20 -3
- package/dist/application/use-cases/generate-thread-title.js +4 -0
- package/dist/application/use-cases/thread-export.js +155 -0
- package/dist/application/use-cases/verify-claim.js +274 -0
- package/dist/application/use-cases/write-pass.js +277 -0
- package/dist/index.js +218 -19
- package/dist/infrastructure/completion/bash-completion.js +2 -2
- package/dist/infrastructure/config/config-manager.js +1 -1
- package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
- package/dist/infrastructure/context/adapters/figma-source.js +163 -0
- package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
- package/dist/infrastructure/context/adapters/linear-source.js +190 -0
- package/dist/infrastructure/context/context-resolver.js +144 -0
- package/dist/infrastructure/context/context-source.js +114 -0
- package/dist/infrastructure/context/oauth-flow.js +207 -0
- package/dist/infrastructure/context/oauth-store.js +119 -0
- package/dist/infrastructure/context/registry-bootstrap.js +99 -0
- package/dist/infrastructure/context/write-pass-resolver.js +106 -0
- package/dist/infrastructure/cost/budget-config.js +1 -1
- package/dist/infrastructure/cost/spend-aggregator.js +5 -5
- package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
- package/dist/infrastructure/hooks/session-analyzer.js +65 -8
- package/dist/infrastructure/observability/execution-tracer.js +0 -5
- package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
- package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
- package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
- package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
- package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
- package/dist/infrastructure/orchestration/context-handoff.js +4 -3
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
- package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
- package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
- package/dist/infrastructure/orchestration/graphify-context.js +6 -13
- package/dist/infrastructure/orchestration/memory-injector.js +11 -16
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
- package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
- package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
- package/dist/infrastructure/orchestration/session-manager.js +142 -29
- package/dist/infrastructure/orchestration/session-persistence.js +139 -30
- package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
- package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
- package/dist/infrastructure/protocols/cloud-sync.js +10 -11
- package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
- package/dist/infrastructure/protocols/thread-phase.js +105 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
- package/dist/infrastructure/protocols/ui-page.js +12 -3299
- package/dist/infrastructure/protocols/verify-page.js +941 -0
- package/dist/infrastructure/security/trust-scorer.js +76 -1
- package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
- package/dist/infrastructure/storage/project-thread-migration.js +14 -9
- package/dist/infrastructure/storage/project-thread-reader.js +11 -1
- package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
- package/dist/monitoring.js +1 -1
- package/dist/presentation/cli/commands/audit-command.js +126 -0
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/config-command.js +2 -2
- package/dist/presentation/cli/commands/context-source-command.js +198 -0
- package/dist/presentation/cli/commands/help-command.js +51 -1
- package/dist/presentation/cli/commands/init-command.js +5 -4
- package/dist/presentation/cli/commands/memory-command.js +29 -15
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
- package/dist/presentation/cli/commands/report-command.js +6 -4
- package/dist/presentation/cli/commands/setup-command.js +71 -18
- package/dist/presentation/cli/commands/status-command.js +0 -4
- package/dist/presentation/cli/commands/thread-command.js +288 -0
- package/dist/presentation/cli/commands/verify-command.js +233 -0
- package/dist/presentation/cli/error-display.js +1 -1
- package/dist/presentation/cli/repl.js +52 -2
- package/dist/presentation/cli/task-deliverable.js +127 -0
- package/dist/testing/cassette.js +125 -0
- package/dist/testing/replay-adapter.js +166 -0
- package/dist/ui.js +54 -29
- package/dist/utils/typo-guard.js +1 -1
- package/package.json +4 -2
|
@@ -9,6 +9,30 @@
|
|
|
9
9
|
*
|
|
10
10
|
* All passes run locally, zero API calls.
|
|
11
11
|
*/
|
|
12
|
+
/**
|
|
13
|
+
* Extract the child_process bindings actually imported in a file, so the
|
|
14
|
+
* exec() command-injection pattern can tell a real `cp.exec(cmd)` from a
|
|
15
|
+
* `regex.exec(str)` that happens to live in a file which also uses
|
|
16
|
+
* child_process (e.g. our own artifacts-extractor: spawns git AND runs a
|
|
17
|
+
* regex loop). namespaceAliases = the `cp` in `import * as cp` / `const cp =
|
|
18
|
+
* require(...)`; namedExec = whether `exec`/`execSync` was imported by name so
|
|
19
|
+
* a bare `exec(cmd)` is callable.
|
|
20
|
+
*/
|
|
21
|
+
export function childProcessBindings(src) {
|
|
22
|
+
const namespaceAliases = new Set();
|
|
23
|
+
let namedExec = false;
|
|
24
|
+
for (const m of src.matchAll(/import\s+(?:\*\s+as\s+)?(\w+)\s+from\s+['"](?:node:)?child_process['"]/g)) {
|
|
25
|
+
namespaceAliases.add(m[1]);
|
|
26
|
+
}
|
|
27
|
+
for (const m of src.matchAll(/(?:const|let|var|import)\s+(\w+)\s*=\s*require\(\s*['"](?:node:)?child_process['"]/g)) {
|
|
28
|
+
namespaceAliases.add(m[1]);
|
|
29
|
+
}
|
|
30
|
+
for (const m of src.matchAll(/(?:import|const|let|var)\s*\{([^}]*)\}\s*(?:from\s*|=\s*require\(\s*)['"](?:node:)?child_process['"]/g)) {
|
|
31
|
+
if (/\b(?:exec|execSync)\b/.test(m[1]))
|
|
32
|
+
namedExec = true;
|
|
33
|
+
}
|
|
34
|
+
return { namespaceAliases, namedExec };
|
|
35
|
+
}
|
|
12
36
|
export const CODE_PATTERNS = [
|
|
13
37
|
// Secrets (scan with strings visible — must see inside string literals)
|
|
14
38
|
{
|
|
@@ -238,12 +262,22 @@ export const CODE_PATTERNS = [
|
|
|
238
262
|
// requiresContext must match an actual import, not the literal string
|
|
239
263
|
// child_process (which this scanner file itself contains as pattern data).
|
|
240
264
|
{
|
|
241
|
-
|
|
265
|
+
// Capture an optional receiver (group 1) so matchFilter can check it:
|
|
266
|
+
// cp.exec(cmd) → receiver "cp" | exec(cmd) → no receiver | g.exec(s) → "g"
|
|
267
|
+
regex: /(?:\b(\w+)\s*\.\s*)?\bexec(?:Sync)?\s*\(\s*(?!['"`])(\w+)\s*[,)]/g,
|
|
242
268
|
severity: 'high',
|
|
243
269
|
category: 'Command Injection',
|
|
244
270
|
description: 'exec() called with variable argument — verify input is not user-controlled',
|
|
245
271
|
cwe: 'CWE-78',
|
|
246
272
|
requiresContext: /(?:from|require\()\s*['"](?:node:)?child_process['"]/,
|
|
273
|
+
// Only a real child_process call counts: a method call on the imported
|
|
274
|
+
// child_process binding, or a bare exec() when exec was named-imported.
|
|
275
|
+
// A method call on anything else (a RegExp, a parser, …) is not.
|
|
276
|
+
matchFilter: (m, src) => {
|
|
277
|
+
const receiver = m[1];
|
|
278
|
+
const b = childProcessBindings(src);
|
|
279
|
+
return receiver === undefined ? b.namedExec : b.namespaceAliases.has(receiver);
|
|
280
|
+
},
|
|
247
281
|
},
|
|
248
282
|
// SSRF
|
|
249
283
|
{
|
|
@@ -286,6 +320,42 @@ export const CODE_PATTERNS = [
|
|
|
286
320
|
cwe: 'CWE-347',
|
|
287
321
|
scanRaw: true,
|
|
288
322
|
},
|
|
323
|
+
// Secret-file read / exfiltration. Code that touches a private key, cloud
|
|
324
|
+
// credential, or system password file is almost always exfil or theft.
|
|
325
|
+
// dogfood 2026-05-27 Phase F: reading ~/.ssh/id_rsa was a false-negative on a
|
|
326
|
+
// product that claims a trust score on every file. scanRaw — the path lives
|
|
327
|
+
// inside a string literal. Tight path set keeps false positives near zero.
|
|
328
|
+
{
|
|
329
|
+
regex: /(?:\.ssh\/id_(?:rsa|dsa|ecdsa|ed25519)|(?:^|[^\w])id_(?:rsa|ed25519)\b|\.aws\/credentials|\.config\/gcloud\/|\.kube\/config|\.docker\/config\.json|\/etc\/shadow|\.npmrc\b|\.pem['"`])/gi,
|
|
330
|
+
severity: 'critical',
|
|
331
|
+
category: 'Secret Exfiltration',
|
|
332
|
+
description: 'Reads a private key or credential file — likely secret exfiltration',
|
|
333
|
+
cwe: 'CWE-522',
|
|
334
|
+
scanRaw: true,
|
|
335
|
+
},
|
|
336
|
+
// Destructive shell command. `rm -rf` in agent-written code is worth flagging
|
|
337
|
+
// even when it looks scoped — a stray variable or trailing slash turns it into
|
|
338
|
+
// a filesystem wipe. dogfood 2026-05-27 Phase F named this a false-negative.
|
|
339
|
+
// The runtime PreToolUse hook also blocks the worst forms at execution time;
|
|
340
|
+
// this is the static signal on the file itself. scanRaw — the command is a
|
|
341
|
+
// string literal.
|
|
342
|
+
{
|
|
343
|
+
regex: /rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r/gi,
|
|
344
|
+
severity: 'high',
|
|
345
|
+
category: 'Destructive Operation',
|
|
346
|
+
description: 'Recursive force-delete (rm -rf) — verify the target can never be empty or a parent path',
|
|
347
|
+
cwe: 'CWE-78',
|
|
348
|
+
scanRaw: true,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
// rm -rf targeting root, a home glob, or a variable/concatenation → wipe risk.
|
|
352
|
+
regex: /rm\s+-[a-z]*[rf][a-z]*\s+(?:\/(?:\s|$|["'`])|~|\$\{?\w|["'`][^"'`]*["'`]\s*\+|\.\.)/gi,
|
|
353
|
+
severity: 'critical',
|
|
354
|
+
category: 'Destructive Operation',
|
|
355
|
+
description: 'rm -rf targeting root, home, or a dynamic path — can wipe unintended files',
|
|
356
|
+
cwe: 'CWE-78',
|
|
357
|
+
scanRaw: true,
|
|
358
|
+
},
|
|
289
359
|
];
|
|
290
360
|
export function measureComplexity(code) {
|
|
291
361
|
const lines = code.split('\n').length;
|
|
@@ -535,6 +605,11 @@ export function scoreFile(filepath, code) {
|
|
|
535
605
|
let match;
|
|
536
606
|
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
537
607
|
while ((match = re.exec(scannable)) !== null) {
|
|
608
|
+
if (pattern.matchFilter && !pattern.matchFilter(match, contextTarget)) {
|
|
609
|
+
if (!re.global)
|
|
610
|
+
break;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
538
613
|
const line = code.substring(0, match.index).split('\n').length;
|
|
539
614
|
allFindings.push({
|
|
540
615
|
severity: pattern.severity,
|
|
@@ -143,7 +143,10 @@ export function parseAgentFile(filePath, defaultFormat = 'unknown') {
|
|
|
143
143
|
// Tools normalization
|
|
144
144
|
let tools = null;
|
|
145
145
|
if (typeof fm.tools === 'string') {
|
|
146
|
-
tools = fm.tools
|
|
146
|
+
tools = fm.tools
|
|
147
|
+
.split(',')
|
|
148
|
+
.map((t) => t.trim().toLowerCase())
|
|
149
|
+
.filter(Boolean);
|
|
147
150
|
}
|
|
148
151
|
else if (hasObjectTools) {
|
|
149
152
|
tools = [];
|
|
@@ -162,7 +165,21 @@ export function parseAgentFile(filePath, defaultFormat = 'unknown') {
|
|
|
162
165
|
? 'subagent'
|
|
163
166
|
: 'all';
|
|
164
167
|
// Track unknown frontmatter keys (everything outside our normalized set)
|
|
165
|
-
const knownKeys = new Set([
|
|
168
|
+
const knownKeys = new Set([
|
|
169
|
+
'name',
|
|
170
|
+
'description',
|
|
171
|
+
'mode',
|
|
172
|
+
'model',
|
|
173
|
+
'temperature',
|
|
174
|
+
'tools',
|
|
175
|
+
'permission',
|
|
176
|
+
'color',
|
|
177
|
+
'hidden',
|
|
178
|
+
'top_p',
|
|
179
|
+
'steps',
|
|
180
|
+
'prompt',
|
|
181
|
+
'disable',
|
|
182
|
+
]);
|
|
166
183
|
const unknownKeys = Object.keys(fm).filter((k) => !knownKeys.has(k));
|
|
167
184
|
return {
|
|
168
185
|
name: typeof fm.name === 'string' ? fm.name : filenameName,
|
|
@@ -219,7 +236,7 @@ export function readCrossToolAgents(cwd, options) {
|
|
|
219
236
|
const dirs = agentDirsToScan(cwd, options?.homeDir);
|
|
220
237
|
for (const { dir, defaultFormat } of dirs) {
|
|
221
238
|
for (const filePath of listMdFilesRecursive(dir)) {
|
|
222
|
-
let agent
|
|
239
|
+
let agent;
|
|
223
240
|
try {
|
|
224
241
|
agent = parseAgentFile(filePath, defaultFormat);
|
|
225
242
|
}
|
|
@@ -32,12 +32,7 @@ import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync, readdirSync
|
|
|
32
32
|
import { join } from 'path';
|
|
33
33
|
import { createHash } from 'crypto';
|
|
34
34
|
/** Path patterns considered ephemeral and skipped from project list. */
|
|
35
|
-
const DEFAULT_TEMP_DIR_PATTERNS = [
|
|
36
|
-
/^\/tmp\//,
|
|
37
|
-
/^\/var\/folders\//,
|
|
38
|
-
/^\/private\/var\/folders\//,
|
|
39
|
-
/^\/private\/tmp\//,
|
|
40
|
-
];
|
|
35
|
+
const DEFAULT_TEMP_DIR_PATTERNS = [/^\/tmp\//, /^\/var\/folders\//, /^\/private\/var\/folders\//, /^\/private\/tmp\//];
|
|
41
36
|
/** sha1(cwd)[:12] — stable, short, filesystem-safe. */
|
|
42
37
|
function slugForCwd(cwd, unknownLabel) {
|
|
43
38
|
if (!cwd)
|
|
@@ -79,7 +74,17 @@ function rollupSessions(events) {
|
|
|
79
74
|
completedIds.add(id);
|
|
80
75
|
const existing = byId.get(id) ?? { id, abandoned: true };
|
|
81
76
|
const existingAny = existing;
|
|
82
|
-
for (const k of [
|
|
77
|
+
for (const k of [
|
|
78
|
+
'agent',
|
|
79
|
+
'prompt',
|
|
80
|
+
'cwd',
|
|
81
|
+
'startedAt',
|
|
82
|
+
'status',
|
|
83
|
+
'durationMs',
|
|
84
|
+
'completedAt',
|
|
85
|
+
'steps',
|
|
86
|
+
'artifacts',
|
|
87
|
+
]) {
|
|
83
88
|
if (e[k] !== undefined && existingAny[k] === undefined) {
|
|
84
89
|
existingAny[k] = e[k];
|
|
85
90
|
}
|
|
@@ -153,7 +158,7 @@ export function migrate(options) {
|
|
|
153
158
|
let threadsWritten = 0;
|
|
154
159
|
let threadsSkippedExisting = 0;
|
|
155
160
|
let unknownCwdSessions = 0;
|
|
156
|
-
|
|
161
|
+
const abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
157
162
|
let tempDirSessionsSkipped = 0;
|
|
158
163
|
for (const group of groups) {
|
|
159
164
|
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
@@ -193,7 +198,7 @@ export function dryRun(options) {
|
|
|
193
198
|
let threadsWritten = 0;
|
|
194
199
|
let threadsSkippedExisting = 0;
|
|
195
200
|
let unknownCwdSessions = 0;
|
|
196
|
-
|
|
201
|
+
const abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
197
202
|
let tempDirSessionsSkipped = 0;
|
|
198
203
|
for (const group of groups) {
|
|
199
204
|
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
@@ -133,7 +133,17 @@ export function readProjectThread(slug, options) {
|
|
|
133
133
|
};
|
|
134
134
|
// Merge fields — later entries override earlier (covers
|
|
135
135
|
// spawn → completion update pattern from forward-mirror)
|
|
136
|
-
for (const k of [
|
|
136
|
+
for (const k of [
|
|
137
|
+
'agent',
|
|
138
|
+
'prompt',
|
|
139
|
+
'cwd',
|
|
140
|
+
'startedAt',
|
|
141
|
+
'status',
|
|
142
|
+
'durationMs',
|
|
143
|
+
'completedAt',
|
|
144
|
+
'steps',
|
|
145
|
+
'artifacts',
|
|
146
|
+
]) {
|
|
137
147
|
if (rec[k] !== undefined) {
|
|
138
148
|
existing[k] = rec[k];
|
|
139
149
|
}
|
|
@@ -145,7 +145,7 @@ export async function backfillThreadTitles(options) {
|
|
|
145
145
|
result.noTasks++;
|
|
146
146
|
continue;
|
|
147
147
|
}
|
|
148
|
-
let title
|
|
148
|
+
let title;
|
|
149
149
|
try {
|
|
150
150
|
title = await generateThreadTitle(scanned.firstPrompt, scanned.firstCwd);
|
|
151
151
|
}
|
|
@@ -178,10 +178,14 @@ export async function backfillThreadTitles(options) {
|
|
|
178
178
|
break;
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
-
catch {
|
|
181
|
+
catch {
|
|
182
|
+
/* skip malformed */
|
|
183
|
+
}
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
catch {
|
|
186
|
+
catch {
|
|
187
|
+
/* if we can't read, fall through and let the append fail */
|
|
188
|
+
}
|
|
185
189
|
if (userBeatUs) {
|
|
186
190
|
result.alreadyTitled++;
|
|
187
191
|
continue;
|
package/dist/monitoring.js
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: `codehere audit`
|
|
3
|
+
*
|
|
4
|
+
* F-05 of the demo-v12 backlog · expose the hash-chained audit log verifier
|
|
5
|
+
* + a "show" subcommand for forensic inspection. The chain is the source of
|
|
6
|
+
* truth, NOT codehere itself · this command lets reviewers and CI verify it
|
|
7
|
+
* without trusting our daemon.
|
|
8
|
+
*
|
|
9
|
+
* Subcommands:
|
|
10
|
+
* codehere audit verify [--json] Walk the chain · report intact/tampered
|
|
11
|
+
* + entry counts + every break point.
|
|
12
|
+
* Exit 0 = valid, 1 = invalid.
|
|
13
|
+
* codehere audit show <seq> [--json]
|
|
14
|
+
* Print one entry by sequence number.
|
|
15
|
+
*
|
|
16
|
+
* Default audit file: ~/.codehere/audit/hook-events.jsonl
|
|
17
|
+
* Override via CODEHERE_AUDIT_FILE env var (mirrors the hook's own resolution).
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { colors, symbols } from '../../../ui.js';
|
|
21
|
+
import { AUDIT_FILE, verifyAuditChain } from '../../../infrastructure/hooks/claude-sdk-hook.js';
|
|
22
|
+
export function executeAuditCommand(sub, arg, options = {}) {
|
|
23
|
+
if (!sub || sub === 'verify')
|
|
24
|
+
return verify(options.json ?? false);
|
|
25
|
+
if (sub === 'show')
|
|
26
|
+
return show(arg, options.json ?? false);
|
|
27
|
+
process.stderr.write(colors.error(`${symbols.error} Unknown audit subcommand: ${sub}\n`) +
|
|
28
|
+
colors.dim('Try: codehere audit verify | codehere audit show <seq>\n'));
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
32
|
+
/* verify · the F-05 headline */
|
|
33
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
34
|
+
function verify(json) {
|
|
35
|
+
const result = verifyAuditChain(AUDIT_FILE);
|
|
36
|
+
if (json) {
|
|
37
|
+
process.stdout.write(JSON.stringify({
|
|
38
|
+
path: AUDIT_FILE,
|
|
39
|
+
valid: result.valid,
|
|
40
|
+
totalEntries: result.totalEntries,
|
|
41
|
+
chainedEntries: result.chainedEntries,
|
|
42
|
+
legacyEntries: result.legacyEntries,
|
|
43
|
+
errors: result.errors,
|
|
44
|
+
}, null, 2) + '\n');
|
|
45
|
+
return result.valid ? 0 : 1;
|
|
46
|
+
}
|
|
47
|
+
// Human output · header, verdict line, counts, then errors if any.
|
|
48
|
+
process.stdout.write('\n' + colors.dim('audit log: ') + colors.gray(AUDIT_FILE) + '\n\n');
|
|
49
|
+
if (result.totalEntries === 0) {
|
|
50
|
+
process.stdout.write(colors.gray(`${symbols.dot} No audit entries yet.`) +
|
|
51
|
+
colors.dim(' Install the hook first: ') +
|
|
52
|
+
colors.bold('codehere hook install') +
|
|
53
|
+
'\n\n');
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
if (result.valid) {
|
|
57
|
+
process.stdout.write(colors.green(`${symbols.success} Chain intact`) +
|
|
58
|
+
colors.dim(' every entry verified against its predecessor hash.') +
|
|
59
|
+
'\n\n');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
process.stdout.write(colors.error(`${symbols.error} Chain BROKEN`) +
|
|
63
|
+
colors.dim(` ${result.errors.length} break point${result.errors.length === 1 ? '' : 's'} detected.`) +
|
|
64
|
+
'\n\n');
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(colors.bold('Counts:\n'));
|
|
67
|
+
process.stdout.write(` ${result.totalEntries.toString().padStart(6)} total entries\n`);
|
|
68
|
+
process.stdout.write(` ${result.chainedEntries.toString().padStart(6)} hash-chained (verifiable)\n`);
|
|
69
|
+
process.stdout.write(` ${result.legacyEntries.toString().padStart(6)} legacy pre-chain (un-verifiable)\n\n`);
|
|
70
|
+
if (result.errors.length > 0) {
|
|
71
|
+
process.stdout.write(colors.bold('Break points:\n'));
|
|
72
|
+
for (const err of result.errors) {
|
|
73
|
+
process.stdout.write(' ' +
|
|
74
|
+
colors.error(`line ${err.lineNumber}`) +
|
|
75
|
+
colors.dim(' ') +
|
|
76
|
+
colors.gray(err.reason) +
|
|
77
|
+
colors.dim(' ') +
|
|
78
|
+
err.detail +
|
|
79
|
+
'\n');
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write('\n');
|
|
82
|
+
}
|
|
83
|
+
return result.valid ? 0 : 1;
|
|
84
|
+
}
|
|
85
|
+
function show(seqStr, json) {
|
|
86
|
+
if (!seqStr || !/^\d+$/.test(seqStr)) {
|
|
87
|
+
process.stderr.write(colors.error(`${symbols.error} Need a numeric sequence: codehere audit show <seq>\n`));
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
const target = parseInt(seqStr, 10);
|
|
91
|
+
if (!existsSync(AUDIT_FILE)) {
|
|
92
|
+
process.stderr.write(colors.gray(`${symbols.dot} No audit log yet.\n`));
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
const lines = readFileSync(AUDIT_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
let entry;
|
|
98
|
+
try {
|
|
99
|
+
entry = JSON.parse(line);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (entry.seq !== target)
|
|
105
|
+
continue;
|
|
106
|
+
if (json) {
|
|
107
|
+
process.stdout.write(JSON.stringify(entry, null, 2) + '\n');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
process.stdout.write('\n' +
|
|
111
|
+
colors.bold(`Entry #${target}`) +
|
|
112
|
+
(entry.ts ? colors.dim(' ' + new Date(entry.ts).toISOString()) : '') +
|
|
113
|
+
'\n\n');
|
|
114
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
115
|
+
const val = typeof v === 'string' && v.length > 120 ? v.slice(0, 117) + '...' : JSON.stringify(v);
|
|
116
|
+
process.stdout.write(` ${colors.dim(k.padEnd(12))} ${val}\n`);
|
|
117
|
+
}
|
|
118
|
+
process.stdout.write('\n');
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
process.stderr.write(colors.error(`${symbols.error} No entry with seq=${target}.\n`) +
|
|
123
|
+
colors.dim(' Tip: run `codehere audit verify` to see the chained-entry count.\n'));
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=audit-command.js.map
|
|
@@ -39,7 +39,7 @@ function showBudget() {
|
|
|
39
39
|
const cfg = loadBudget();
|
|
40
40
|
const spend = computeSpendToday();
|
|
41
41
|
newline();
|
|
42
|
-
console.log(info(
|
|
42
|
+
console.log(info("Today's estimated spend:"), fmtUsd(spend.totalUsd), `(${spend.sessionCount} session${spend.sessionCount === 1 ? '' : 's'})`);
|
|
43
43
|
if (cfg.dailyUsd === null) {
|
|
44
44
|
console.log(info('Daily budget:'), 'none — no enforcement');
|
|
45
45
|
console.log(info('Set one with:'), 'codehere budget set 5');
|
|
@@ -67,7 +67,7 @@ function setBudget(value) {
|
|
|
67
67
|
cfg.dailyUsd = n;
|
|
68
68
|
saveBudget(cfg);
|
|
69
69
|
console.log(success('Daily budget set to'), fmtUsd(n));
|
|
70
|
-
console.log(info(
|
|
70
|
+
console.log(info("Spawns will be blocked once today's estimated spend reaches this amount."));
|
|
71
71
|
console.log(info('Disable with:'), 'codehere budget off');
|
|
72
72
|
}
|
|
73
73
|
function disableBudget() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Config Command
|
|
3
3
|
* Manage Codehere configuration and preferences
|
|
4
4
|
*/
|
|
5
|
-
import { colors, separator, newline, sectionHeader, success, error, createTable, isMinimalStyle
|
|
5
|
+
import { colors, separator, newline, sectionHeader, success, error, createTable, isMinimalStyle } from '../../../ui.js';
|
|
6
6
|
import { getConfigManager } from '../../../infrastructure/config/config-manager.js';
|
|
7
7
|
import { executionTracer } from '../../../infrastructure/observability/execution-tracer.js';
|
|
8
8
|
import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
|
|
@@ -43,7 +43,7 @@ function displayConfig(config) {
|
|
|
43
43
|
export async function executeConfigCommand(action, key, value) {
|
|
44
44
|
// Start execution tracing
|
|
45
45
|
const codebaseInfo = findCodebaseRoot(process.cwd());
|
|
46
|
-
|
|
46
|
+
executionTracer.startRun('config', {
|
|
47
47
|
codebaseId: codebaseInfo?.rootPath || process.cwd(),
|
|
48
48
|
});
|
|
49
49
|
try {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: `codehere context-source` · v0.8 S2.5
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* list · show every registered provider + connection status
|
|
6
|
+
* connect <provider> · open the browser to begin OAuth · prints URL
|
|
7
|
+
* disconnect <provider> · clear the stored OAuth token
|
|
8
|
+
*
|
|
9
|
+
* Talks to the running daemon at 127.0.0.1:3773. If the daemon isn't running,
|
|
10
|
+
* we print a clear message rather than starting one silently.
|
|
11
|
+
*/
|
|
12
|
+
import { colors, symbols } from '../../../ui.js';
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
const DAEMON_BASE = process.env.CODEHERE_DAEMON_URL || 'http://127.0.0.1:3773';
|
|
15
|
+
export async function executeContextSourceCommand(sub, arg, options = {}) {
|
|
16
|
+
switch (sub) {
|
|
17
|
+
case undefined:
|
|
18
|
+
case 'list':
|
|
19
|
+
return list(options.json ?? false);
|
|
20
|
+
case 'connect':
|
|
21
|
+
return connect(arg, options.json ?? false, options.open !== false);
|
|
22
|
+
case 'disconnect':
|
|
23
|
+
return disconnect(arg, options.json ?? false);
|
|
24
|
+
default:
|
|
25
|
+
process.stderr.write(colors.error(`${symbols.error} Unknown subcommand: ${sub}\n`) +
|
|
26
|
+
colors.dim('Try: codehere context-source list | connect <provider> | disconnect <provider>\n'));
|
|
27
|
+
return 2;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function fetchJson(path, init = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const r = await fetch(DAEMON_BASE + path, init);
|
|
33
|
+
if (!r.ok) {
|
|
34
|
+
const text = await r.text();
|
|
35
|
+
return { ok: false, status: r.status, error: text };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, status: r.status, data: (await r.json()) };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function daemonUnreachableNote(err) {
|
|
44
|
+
return (colors.error(`${symbols.error} Daemon not reachable at ${DAEMON_BASE}.\n`) +
|
|
45
|
+
colors.dim(` Start it with: `) +
|
|
46
|
+
colors.bold('codehere start') +
|
|
47
|
+
'\n' +
|
|
48
|
+
colors.dim(` Detail: ${err}\n`));
|
|
49
|
+
}
|
|
50
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
51
|
+
/* list */
|
|
52
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
53
|
+
async function list(json) {
|
|
54
|
+
const r = await fetchJson('/api/context-sources');
|
|
55
|
+
if (!r.ok) {
|
|
56
|
+
if (json) {
|
|
57
|
+
process.stdout.write(JSON.stringify({ error: 'daemon_unreachable', detail: r.error }, null, 2) + '\n');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
process.stderr.write(daemonUnreachableNote(r.error ?? ''));
|
|
61
|
+
}
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
const items = r.data?.providers ?? [];
|
|
65
|
+
if (json) {
|
|
66
|
+
process.stdout.write(JSON.stringify({ providers: items }, null, 2) + '\n');
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write('\n' + colors.bold('Context sources') + '\n\n');
|
|
70
|
+
if (!items.length) {
|
|
71
|
+
process.stdout.write(colors.dim(' (no providers registered)\n\n'));
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
for (const p of items) {
|
|
75
|
+
const tag = statusTag(p.status);
|
|
76
|
+
const who = p.userLabel ? colors.gray('· ' + p.userLabel) : '';
|
|
77
|
+
process.stdout.write(` ${tag} ${colors.bold(p.label.padEnd(10))} ${colors.dim(p.scheme + '://')} ${who}\n`);
|
|
78
|
+
if (!p.clientIdConfigured) {
|
|
79
|
+
process.stdout.write(' ' + colors.warning(`set CODEHERE_${p.id.toUpperCase()}_CLIENT_ID before connect()`) + '\n');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write('\n' + colors.dim('Connect: ') + colors.bold('codehere context-source connect <provider>') + '\n\n');
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
function statusTag(s) {
|
|
86
|
+
switch (s) {
|
|
87
|
+
case 'connected':
|
|
88
|
+
return colors.green('● connected ');
|
|
89
|
+
case 'expired':
|
|
90
|
+
return colors.warning('● expired ');
|
|
91
|
+
case 'manual':
|
|
92
|
+
return colors.gray('● manual ');
|
|
93
|
+
case 'disconnected':
|
|
94
|
+
return colors.gray('○ disconnected');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
98
|
+
/* connect */
|
|
99
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
100
|
+
async function connect(providerArg, json, openBrowser) {
|
|
101
|
+
if (!providerArg) {
|
|
102
|
+
process.stderr.write(colors.error(`${symbols.error} Need a provider id: codehere context-source connect <id>\n`));
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
const r = await fetchJson(`/api/context-sources/${encodeURIComponent(providerArg)}/connect`, { method: 'POST' });
|
|
106
|
+
if (!r.ok) {
|
|
107
|
+
let detail = null;
|
|
108
|
+
try {
|
|
109
|
+
detail = JSON.parse(r.error ?? '{}');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* ignore */
|
|
113
|
+
}
|
|
114
|
+
if (detail?.error === 'provider_not_registered') {
|
|
115
|
+
const envVar = detail.envVar ?? `CODEHERE_${providerArg.toUpperCase()}_CLIENT_ID`;
|
|
116
|
+
if (json) {
|
|
117
|
+
process.stdout.write(JSON.stringify({ error: 'provider_not_registered', envVar, message: detail.message }, null, 2) + '\n');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
process.stderr.write(colors.error(`${symbols.error} ${providerArg} OAuth app not registered.\n`) +
|
|
121
|
+
colors.dim(` ${detail.message ?? ''}\n`) +
|
|
122
|
+
colors.dim(` Set the env var: `) +
|
|
123
|
+
colors.bold(envVar) +
|
|
124
|
+
'\n');
|
|
125
|
+
}
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
if (json) {
|
|
129
|
+
process.stdout.write(JSON.stringify({ error: 'connect_failed', detail: r.error }, null, 2) + '\n');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
process.stderr.write(daemonUnreachableNote(r.error ?? ''));
|
|
133
|
+
}
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
const url = r.data?.authorizeUrl;
|
|
137
|
+
if (!url) {
|
|
138
|
+
process.stderr.write(colors.error(`${symbols.error} Daemon did not return an authorize URL.\n`));
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
if (json) {
|
|
142
|
+
process.stdout.write(JSON.stringify({ authorizeUrl: url }, null, 2) + '\n');
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
process.stdout.write('\n' +
|
|
146
|
+
colors.bold(`Opening ${providerArg} OAuth flow`) +
|
|
147
|
+
'\n\n' +
|
|
148
|
+
colors.dim(' URL: ') +
|
|
149
|
+
url +
|
|
150
|
+
'\n\n' +
|
|
151
|
+
colors.dim(' Codehere will store the token at ~/.codehere/context-sources.json.\n') +
|
|
152
|
+
colors.dim(' You can close the browser tab when it says "connected".\n\n'));
|
|
153
|
+
if (openBrowser) {
|
|
154
|
+
openInBrowser(url);
|
|
155
|
+
}
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
function openInBrowser(url) {
|
|
159
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
160
|
+
try {
|
|
161
|
+
const child = spawn(cmd, [url], { detached: true, stdio: 'ignore' });
|
|
162
|
+
child.unref();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// best-effort · the URL is already printed
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
169
|
+
/* disconnect */
|
|
170
|
+
/* ──────────────────────────────────────────────────────────────────────── */
|
|
171
|
+
async function disconnect(providerArg, json) {
|
|
172
|
+
if (!providerArg) {
|
|
173
|
+
process.stderr.write(colors.error(`${symbols.error} Need a provider id: codehere context-source disconnect <id>\n`));
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
const r = await fetchJson(`/api/context-sources/${encodeURIComponent(providerArg)}/disconnect`, { method: 'POST' });
|
|
177
|
+
if (!r.ok) {
|
|
178
|
+
if (json) {
|
|
179
|
+
process.stdout.write(JSON.stringify({ error: 'disconnect_failed', detail: r.error }, null, 2) + '\n');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
process.stderr.write(daemonUnreachableNote(r.error ?? ''));
|
|
183
|
+
}
|
|
184
|
+
return 2;
|
|
185
|
+
}
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(JSON.stringify(r.data) + '\n');
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
if (r.data?.ok) {
|
|
191
|
+
process.stdout.write(colors.green(`${symbols.success} disconnected ${providerArg}\n`));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
process.stdout.write(colors.gray(`${symbols.dot} ${providerArg} was not connected\n`));
|
|
195
|
+
}
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=context-source-command.js.map
|