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.
Files changed (73) hide show
  1. package/README.md +5 -3
  2. package/dist/application/use-cases/domain-trust.js +110 -0
  3. package/dist/application/use-cases/generate-final-report.js +87 -168
  4. package/dist/application/use-cases/generate-narration.js +11 -0
  5. package/dist/application/use-cases/generate-thread-title.js +4 -0
  6. package/dist/application/use-cases/thread-export.js +155 -0
  7. package/dist/application/use-cases/verify-claim.js +274 -0
  8. package/dist/application/use-cases/write-pass.js +277 -0
  9. package/dist/index.js +211 -16
  10. package/dist/infrastructure/completion/bash-completion.js +2 -2
  11. package/dist/infrastructure/config/config-manager.js +1 -1
  12. package/dist/infrastructure/context/adapters/drawio-source.js +157 -0
  13. package/dist/infrastructure/context/adapters/figma-source.js +163 -0
  14. package/dist/infrastructure/context/adapters/github-issue-source.js +159 -0
  15. package/dist/infrastructure/context/adapters/linear-source.js +190 -0
  16. package/dist/infrastructure/context/context-resolver.js +144 -0
  17. package/dist/infrastructure/context/context-source.js +114 -0
  18. package/dist/infrastructure/context/oauth-flow.js +207 -0
  19. package/dist/infrastructure/context/oauth-store.js +119 -0
  20. package/dist/infrastructure/context/registry-bootstrap.js +99 -0
  21. package/dist/infrastructure/context/write-pass-resolver.js +106 -0
  22. package/dist/infrastructure/hooks/claude-sdk-hook.js +73 -57
  23. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  24. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  25. package/dist/infrastructure/orchestration/aider-adapter.js +49 -18
  26. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  27. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  28. package/dist/infrastructure/orchestration/claude-code-adapter.js +38 -5
  29. package/dist/infrastructure/orchestration/codex-adapter.js +150 -22
  30. package/dist/infrastructure/orchestration/conductor-adapter.js +9 -3
  31. package/dist/infrastructure/orchestration/context-handoff.js +3 -2
  32. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  33. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  34. package/dist/infrastructure/orchestration/gemini-adapter.js +12 -2
  35. package/dist/infrastructure/orchestration/graphify-context.js +18 -13
  36. package/dist/infrastructure/orchestration/memory-injector.js +46 -1
  37. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +230 -0
  38. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  40. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  41. package/dist/infrastructure/orchestration/opencode-adapter.js +22 -2
  42. package/dist/infrastructure/orchestration/progress-aggregator.js +177 -3
  43. package/dist/infrastructure/orchestration/session-manager.js +112 -6
  44. package/dist/infrastructure/orchestration/session-persistence.js +140 -31
  45. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  46. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  47. package/dist/infrastructure/protocols/cloud-sync.js +1 -6
  48. package/dist/infrastructure/protocols/mcp-server-impl.js +1283 -13
  49. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  50. package/dist/infrastructure/protocols/ui-page-v2.js +3913 -88
  51. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  52. package/dist/infrastructure/protocols/verify-page.js +941 -0
  53. package/dist/infrastructure/security/trust-scorer.js +76 -1
  54. package/dist/infrastructure/storage/cross-tool-agents.js +1 -1
  55. package/dist/infrastructure/storage/project-thread-migration.js +2 -2
  56. package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
  57. package/dist/monitoring.js +1 -1
  58. package/dist/presentation/cli/commands/audit-command.js +126 -0
  59. package/dist/presentation/cli/commands/config-command.js +2 -2
  60. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  61. package/dist/presentation/cli/commands/help-command.js +51 -1
  62. package/dist/presentation/cli/commands/report-command.js +6 -4
  63. package/dist/presentation/cli/commands/setup-command.js +71 -18
  64. package/dist/presentation/cli/commands/status-command.js +0 -4
  65. package/dist/presentation/cli/commands/thread-command.js +288 -0
  66. package/dist/presentation/cli/commands/verify-command.js +233 -0
  67. package/dist/presentation/cli/error-display.js +1 -1
  68. package/dist/presentation/cli/repl.js +32 -1
  69. package/dist/presentation/cli/task-deliverable.js +127 -0
  70. package/dist/testing/cassette.js +125 -0
  71. package/dist/testing/replay-adapter.js +166 -0
  72. package/dist/ui.js +18 -1
  73. 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
- regex: /\.exec\s*\(\s*(?!['"`])(\w+)\s*[,)]/g,
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 = null;
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
- let abandonedSessions = sessions.filter((s) => s.abandoned).length;
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
- let abandonedSessions = sessions.filter((s) => s.abandoned).length;
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)) {
@@ -145,7 +145,7 @@ export async function backfillThreadTitles(options) {
145
145
  result.noTasks++;
146
146
  continue;
147
147
  }
148
- let title = null;
148
+ let title;
149
149
  try {
150
150
  title = await generateThreadTitle(scanned.firstPrompt, scanned.firstCwd);
151
151
  }
@@ -159,7 +159,7 @@ function checkMemory() {
159
159
  usage: usagePercent,
160
160
  };
161
161
  }
162
- catch (error) {
162
+ catch {
163
163
  return {
164
164
  status: 'ok',
165
165
  message: 'Memory check unavailable',
@@ -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, } from '../../../ui.js';
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
- const runId = executionTracer.startRun('config', {
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, grade, criticalCount, highCount, mediumCount, lowCount, files } = report;
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, overallScore } = report;
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, cwd) {
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, cwd)}
150
+ ${findingsMarkdown(files)}
149
151
 
150
152
  ---
151
153