codehere 0.6.2 → 0.7.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 +5 -3
- package/dist/application/use-cases/domain-trust.js +110 -0
- package/dist/application/use-cases/generate-final-report.js +87 -168
- package/dist/application/use-cases/generate-narration.js +11 -0
- 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 +211 -16
- 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/hooks/claude-sdk-hook.js +73 -57
- 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 +49 -18
- 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 +38 -5
- package/dist/infrastructure/orchestration/codex-adapter.js +150 -22
- package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
- package/dist/infrastructure/orchestration/context-handoff.js +3 -2
- 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 +12 -2
- package/dist/infrastructure/orchestration/graphify-context.js +18 -13
- package/dist/infrastructure/orchestration/memory-injector.js +46 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +230 -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 +22 -2
- package/dist/infrastructure/orchestration/progress-aggregator.js +177 -3
- package/dist/infrastructure/orchestration/session-manager.js +112 -6
- package/dist/infrastructure/orchestration/session-persistence.js +140 -31
- 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 +1 -6
- package/dist/infrastructure/protocols/mcp-server-impl.js +1283 -13
- package/dist/infrastructure/protocols/thread-phase.js +105 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +3913 -88
- 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 +1 -1
- package/dist/infrastructure/storage/project-thread-migration.js +2 -2
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +1 -1
- package/dist/presentation/cli/commands/audit-command.js +126 -0
- 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/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 +32 -1
- 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 +18 -1
- package/package.json +5 -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,
|
|
@@ -236,7 +236,7 @@ export function readCrossToolAgents(cwd, options) {
|
|
|
236
236
|
const dirs = agentDirsToScan(cwd, options?.homeDir);
|
|
237
237
|
for (const { dir, defaultFormat } of dirs) {
|
|
238
238
|
for (const filePath of listMdFilesRecursive(dir)) {
|
|
239
|
-
let agent
|
|
239
|
+
let agent;
|
|
240
240
|
try {
|
|
241
241
|
agent = parseAgentFile(filePath, defaultFormat);
|
|
242
242
|
}
|
|
@@ -158,7 +158,7 @@ export function migrate(options) {
|
|
|
158
158
|
let threadsWritten = 0;
|
|
159
159
|
let threadsSkippedExisting = 0;
|
|
160
160
|
let unknownCwdSessions = 0;
|
|
161
|
-
|
|
161
|
+
const abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
162
162
|
let tempDirSessionsSkipped = 0;
|
|
163
163
|
for (const group of groups) {
|
|
164
164
|
if (skipTempDirs && isTempDir(group.cwd)) {
|
|
@@ -198,7 +198,7 @@ export function dryRun(options) {
|
|
|
198
198
|
let threadsWritten = 0;
|
|
199
199
|
let threadsSkippedExisting = 0;
|
|
200
200
|
let unknownCwdSessions = 0;
|
|
201
|
-
|
|
201
|
+
const abandonedSessions = sessions.filter((s) => s.abandoned).length;
|
|
202
202
|
let tempDirSessionsSkipped = 0;
|
|
203
203
|
for (const group of groups) {
|
|
204
204
|
if (skipTempDirs && isTempDir(group.cwd)) {
|
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
|
|
@@ -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
|
|
@@ -91,7 +91,57 @@ const COMMAND_INFO = {
|
|
|
91
91
|
{ flag: '--json', description: 'Output as JSON' },
|
|
92
92
|
{ flag: '--export <fmt>', description: 'Export raw audit feed (jsonl, json, csv, md)' },
|
|
93
93
|
],
|
|
94
|
-
related: ['hook', 'trust'],
|
|
94
|
+
related: ['hook', 'trust', 'verify'],
|
|
95
|
+
},
|
|
96
|
+
verify: {
|
|
97
|
+
name: 'verify',
|
|
98
|
+
description: "Verify a session's Final Report against the recorded reality (catches optimistic claims)",
|
|
99
|
+
usage: 'codehere verify [session-id] [options]',
|
|
100
|
+
examples: ['codehere verify', 'codehere verify a1b2c3d4', 'codehere verify a1b2c3d4 --json'],
|
|
101
|
+
options: [{ flag: '--json', description: 'Machine-readable verdict for CI' }],
|
|
102
|
+
related: ['status', 'report', 'audit'],
|
|
103
|
+
},
|
|
104
|
+
audit: {
|
|
105
|
+
name: 'audit',
|
|
106
|
+
description: 'Inspect the hash-chained audit log · verify integrity or show one entry',
|
|
107
|
+
usage: 'codehere audit [verify|show <seq>] [options]',
|
|
108
|
+
examples: ['codehere audit', 'codehere audit verify', 'codehere audit verify --json', 'codehere audit show 42'],
|
|
109
|
+
options: [{ flag: '--json', description: 'Machine-readable output' }],
|
|
110
|
+
related: ['status', 'verify', 'hook'],
|
|
111
|
+
},
|
|
112
|
+
'context-source': {
|
|
113
|
+
name: 'context-source',
|
|
114
|
+
description: 'Connect external tools (Linear, Figma) via OAuth · list status · disconnect',
|
|
115
|
+
usage: 'codehere context-source [list|connect <provider>|disconnect <provider>]',
|
|
116
|
+
examples: [
|
|
117
|
+
'codehere context-source list',
|
|
118
|
+
'codehere context-source connect linear',
|
|
119
|
+
'codehere context-source connect figma --no-open',
|
|
120
|
+
'codehere context-source disconnect linear',
|
|
121
|
+
],
|
|
122
|
+
options: [
|
|
123
|
+
{ flag: '--json', description: 'Machine-readable output' },
|
|
124
|
+
{ flag: '--no-open', description: 'For connect: print URL but skip auto-opening the browser' },
|
|
125
|
+
],
|
|
126
|
+
related: ['serve', 'start'],
|
|
127
|
+
},
|
|
128
|
+
thread: {
|
|
129
|
+
name: 'thread',
|
|
130
|
+
description: 'Share or import a signed session bundle · local-first, no relay',
|
|
131
|
+
usage: 'codehere thread <export|import|keys|trust|untrust> [args] [options]',
|
|
132
|
+
examples: [
|
|
133
|
+
'codehere thread export <session-id> --out my-task.codehere.json',
|
|
134
|
+
'codehere thread import my-task.codehere.json',
|
|
135
|
+
'codehere thread import my-task.codehere.json --trust',
|
|
136
|
+
'codehere thread keys',
|
|
137
|
+
'codehere thread trust <pubkey-b64> "alice@laptop"',
|
|
138
|
+
],
|
|
139
|
+
options: [
|
|
140
|
+
{ flag: '--out <file>', description: 'For export: write to file (otherwise stdout)' },
|
|
141
|
+
{ flag: '--trust', description: 'For import: trust the signer key then proceed' },
|
|
142
|
+
{ flag: '--json', description: 'Machine-readable output' },
|
|
143
|
+
],
|
|
144
|
+
related: ['start', 'verify'],
|
|
95
145
|
},
|
|
96
146
|
serve: {
|
|
97
147
|
name: 'serve',
|
|
@@ -49,7 +49,7 @@ function badgeUrl(score, grade) {
|
|
|
49
49
|
return `https://img.shields.io/badge/codehere%20trust-${score}%25%20${grade}-${color}?logo=shield&logoColor=white`;
|
|
50
50
|
}
|
|
51
51
|
function narrativeSummary(report) {
|
|
52
|
-
const { overallScore,
|
|
52
|
+
const { overallScore, criticalCount, highCount, mediumCount, files } = report;
|
|
53
53
|
if (criticalCount === 0 && highCount === 0) {
|
|
54
54
|
if (overallScore >= 90) {
|
|
55
55
|
return (`Clean codebase. ${files.length} file${files.length !== 1 ? 's' : ''} scanned, nothing blocking a merge. ` +
|
|
@@ -66,7 +66,7 @@ function narrativeSummary(report) {
|
|
|
66
66
|
return `No critical issues, but ${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} worth addressing before merge.`;
|
|
67
67
|
}
|
|
68
68
|
function recommendationsMarkdown(report) {
|
|
69
|
-
const { criticalCount, highCount, mediumCount, lowCount
|
|
69
|
+
const { criticalCount, highCount, mediumCount, lowCount } = report;
|
|
70
70
|
const lines = [];
|
|
71
71
|
if (criticalCount > 0) {
|
|
72
72
|
lines.push(`1. **Resolve all ${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} before merging.** These represent active security risks (credentials in code, eval injection, SQL without parameterization, etc.). They're usually quick fixes.`);
|
|
@@ -85,7 +85,7 @@ function recommendationsMarkdown(report) {
|
|
|
85
85
|
}
|
|
86
86
|
return lines.join('\n\n');
|
|
87
87
|
}
|
|
88
|
-
function findingsMarkdown(files
|
|
88
|
+
function findingsMarkdown(files) {
|
|
89
89
|
const sections = [];
|
|
90
90
|
for (const severity of ['critical', 'high', 'medium', 'low']) {
|
|
91
91
|
const allFindings = [];
|
|
@@ -133,6 +133,8 @@ ${contextLine}
|
|
|
133
133
|
|
|
134
134
|
## Summary
|
|
135
135
|
|
|
136
|
+
${summary}
|
|
137
|
+
|
|
136
138
|
| | |
|
|
137
139
|
|---|---|
|
|
138
140
|
| Files scanned | ${files.length} |
|
|
@@ -145,7 +147,7 @@ ${contextLine}
|
|
|
145
147
|
|
|
146
148
|
## Findings
|
|
147
149
|
|
|
148
|
-
${findingsMarkdown(files
|
|
150
|
+
${findingsMarkdown(files)}
|
|
149
151
|
|
|
150
152
|
---
|
|
151
153
|
|