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.
Files changed (81) 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 +84 -5
  4. package/dist/application/use-cases/generate-narration.js +20 -3
  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 +218 -19
  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/cost/budget-config.js +1 -1
  23. package/dist/infrastructure/cost/spend-aggregator.js +5 -5
  24. package/dist/infrastructure/hooks/claude-sdk-hook.js +79 -59
  25. package/dist/infrastructure/hooks/session-analyzer.js +65 -8
  26. package/dist/infrastructure/observability/execution-tracer.js +0 -5
  27. package/dist/infrastructure/orchestration/aider-adapter.js +45 -17
  28. package/dist/infrastructure/orchestration/artifacts-extractor.js +39 -5
  29. package/dist/infrastructure/orchestration/cassette-recorder.js +120 -0
  30. package/dist/infrastructure/orchestration/claude-code-adapter.js +33 -4
  31. package/dist/infrastructure/orchestration/codex-adapter.js +157 -19
  32. package/dist/infrastructure/orchestration/context-handoff.js +4 -3
  33. package/dist/infrastructure/orchestration/control/codex-control-surface.js +99 -0
  34. package/dist/infrastructure/orchestration/control/control-surface.js +15 -0
  35. package/dist/infrastructure/orchestration/gemini-adapter.js +15 -17
  36. package/dist/infrastructure/orchestration/graphify-context.js +6 -13
  37. package/dist/infrastructure/orchestration/memory-injector.js +11 -16
  38. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +233 -0
  39. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +73 -0
  40. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +83 -0
  41. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +37 -0
  42. package/dist/infrastructure/orchestration/opencode-adapter.js +23 -3
  43. package/dist/infrastructure/orchestration/progress-aggregator.js +178 -5
  44. package/dist/infrastructure/orchestration/session-manager.js +142 -29
  45. package/dist/infrastructure/orchestration/session-persistence.js +139 -30
  46. package/dist/infrastructure/orchestration/thread-key-store.js +127 -0
  47. package/dist/infrastructure/orchestration/write-pass-store.js +46 -0
  48. package/dist/infrastructure/protocols/cloud-sync.js +10 -11
  49. package/dist/infrastructure/protocols/mcp-server-impl.js +1295 -17
  50. package/dist/infrastructure/protocols/thread-phase.js +105 -0
  51. package/dist/infrastructure/protocols/ui-page-v2.js +3768 -75
  52. package/dist/infrastructure/protocols/ui-page.js +12 -3299
  53. package/dist/infrastructure/protocols/verify-page.js +941 -0
  54. package/dist/infrastructure/security/trust-scorer.js +76 -1
  55. package/dist/infrastructure/storage/cross-tool-agents.js +20 -3
  56. package/dist/infrastructure/storage/project-thread-migration.js +14 -9
  57. package/dist/infrastructure/storage/project-thread-reader.js +11 -1
  58. package/dist/infrastructure/storage/project-thread-title-backfill.js +7 -3
  59. package/dist/monitoring.js +1 -1
  60. package/dist/presentation/cli/commands/audit-command.js +126 -0
  61. package/dist/presentation/cli/commands/budget-command.js +2 -2
  62. package/dist/presentation/cli/commands/config-command.js +2 -2
  63. package/dist/presentation/cli/commands/context-source-command.js +198 -0
  64. package/dist/presentation/cli/commands/help-command.js +51 -1
  65. package/dist/presentation/cli/commands/init-command.js +5 -4
  66. package/dist/presentation/cli/commands/memory-command.js +29 -15
  67. package/dist/presentation/cli/commands/protocols-builder.js +2 -2
  68. package/dist/presentation/cli/commands/repo-map-builder.js +4 -4
  69. package/dist/presentation/cli/commands/report-command.js +6 -4
  70. package/dist/presentation/cli/commands/setup-command.js +71 -18
  71. package/dist/presentation/cli/commands/status-command.js +0 -4
  72. package/dist/presentation/cli/commands/thread-command.js +288 -0
  73. package/dist/presentation/cli/commands/verify-command.js +233 -0
  74. package/dist/presentation/cli/error-display.js +1 -1
  75. package/dist/presentation/cli/repl.js +52 -2
  76. package/dist/presentation/cli/task-deliverable.js +127 -0
  77. package/dist/testing/cassette.js +125 -0
  78. package/dist/testing/replay-adapter.js +166 -0
  79. package/dist/ui.js +54 -29
  80. package/dist/utils/typo-guard.js +1 -1
  81. package/package.json +4 -2
@@ -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',
@@ -39,7 +39,7 @@
39
39
  * Refuses to follow symlinks (lstat check) so a hostile/misplaced symlink at
40
40
  * `.codehere/CONTEXT.md` or `.gitignore` cannot redirect writes outside the repo.
41
41
  */
42
- import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, lstatSync, statSync, symlinkSync, readlinkSync, rmSync } from 'fs';
42
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync, lstatSync, statSync, symlinkSync, readlinkSync, rmSync, } from 'fs';
43
43
  import { homedir } from 'os';
44
44
  import { join, dirname } from 'path';
45
45
  import { colors, success, info, warning, error, newline } from '../../../ui.js';
@@ -316,7 +316,7 @@ function findSymlinkAncestor(codehereDir, relativePath) {
316
316
  // Defensive: reject any relative path containing .. — even though templates
317
317
  // are hard-coded, this protects against future schema additions slipping a
318
318
  // traversal through.
319
- if (segments.some(seg => seg === '..' || seg === '')) {
319
+ if (segments.some((seg) => seg === '..' || seg === '')) {
320
320
  return relativePath;
321
321
  }
322
322
  // Walk codehereDir, codehereDir/seg1, codehereDir/seg1/seg2, ... (excluding
@@ -338,12 +338,13 @@ function ensureGitignore(repoRoot) {
338
338
  return { result: 'refused-symlink', addedLines: 0 };
339
339
  const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
340
340
  // De-duplicate: only append lines that aren't already present.
341
- const missing = GITIGNORE_LINES.filter(line => !existing.includes(line));
341
+ const missing = GITIGNORE_LINES.filter((line) => !existing.includes(line));
342
342
  if (missing.length === 0)
343
343
  return { result: 'already-present', addedLines: 0 };
344
344
  const block = (existing.length > 0 && !existing.endsWith('\n') ? '\n' : '') +
345
345
  `\n${GITIGNORE_MARKER}\n` +
346
- missing.join('\n') + '\n';
346
+ missing.join('\n') +
347
+ '\n';
347
348
  appendFileSync(gitignorePath, block);
348
349
  return { result: 'updated', addedLines: missing.length };
349
350
  }
@@ -48,7 +48,7 @@ import { buildMemoryPrefix } from '../../../infrastructure/orchestration/memory-
48
48
  * Built lazily from init-command.ts's F3_TEMPLATES so the seed bodies stay
49
49
  * in one place — no duplication, no drift.
50
50
  */
51
- const DEFAULT_SEED_BODIES = new Map(F3_TEMPLATES.map(t => [t.filename, t.body]));
51
+ const DEFAULT_SEED_BODIES = new Map(F3_TEMPLATES.map((t) => [t.filename, t.body]));
52
52
  function lessonIdForText(text) {
53
53
  return createHash('sha256').update(text).digest('hex').slice(0, 16);
54
54
  }
@@ -58,8 +58,8 @@ function readLessonsFile(path) {
58
58
  return readFileSync(path, 'utf8')
59
59
  .trim()
60
60
  .split('\n')
61
- .filter(l => l.length > 0)
62
- .map(l => {
61
+ .filter((l) => l.length > 0)
62
+ .map((l) => {
63
63
  try {
64
64
  return JSON.parse(l);
65
65
  }
@@ -83,7 +83,7 @@ function readLessonsFile(path) {
83
83
  function appendLessonIfNew(path, text, source) {
84
84
  const id = lessonIdForText(text);
85
85
  const existing = readLessonsFile(path);
86
- const dup = existing.find(l => l.id === id);
86
+ const dup = existing.find((l) => l.id === id);
87
87
  if (dup)
88
88
  return { result: 'duplicate', lesson: dup };
89
89
  const lesson = {
@@ -326,7 +326,9 @@ function gatherLayerSummary(codehereDir, projectRoot) {
326
326
  const episodicLearnings = join(codehereDir, 'memory/episodic/AGENT_LEARNINGS.jsonl');
327
327
  const repoMapPath = join(codehereDir, 'memory/episodic/repo_map.json');
328
328
  const learningsLine = existsSync(episodicLearnings) || isSymlink(episodicLearnings)
329
- ? (isSymlink(episodicLearnings) ? `AGENT_LEARNINGS.jsonl → ${readlinkSync(episodicLearnings)}` : 'AGENT_LEARNINGS.jsonl (real file)')
329
+ ? isSymlink(episodicLearnings)
330
+ ? `AGENT_LEARNINGS.jsonl → ${readlinkSync(episodicLearnings)}`
331
+ : 'AGENT_LEARNINGS.jsonl (real file)'
330
332
  : '(no AGENT_LEARNINGS.jsonl)';
331
333
  const repoMapLine = existsSync(repoMapPath)
332
334
  ? `repo_map.json (${fmtSize(statSync(repoMapPath).size)})`
@@ -354,9 +356,12 @@ function gatherLayerSummary(codehereDir, projectRoot) {
354
356
  const skillsDir = join(codehereDir, 'skills');
355
357
  if (existsSync(skillsDir)) {
356
358
  const subdirs = readdirSync(skillsDir, { withFileTypes: true })
357
- .filter(e => e.isDirectory())
358
- .filter(e => existsSync(join(skillsDir, e.name, 'SKILL.md')));
359
- out.push({ layer: 'skills', line: `skills · ${subdirs.length} skill folder${subdirs.length === 1 ? '' : 's'}` });
359
+ .filter((e) => e.isDirectory())
360
+ .filter((e) => existsSync(join(skillsDir, e.name, 'SKILL.md')));
361
+ out.push({
362
+ layer: 'skills',
363
+ line: `skills · ${subdirs.length} skill folder${subdirs.length === 1 ? '' : 's'}`,
364
+ });
360
365
  }
361
366
  else {
362
367
  out.push({ layer: 'skills', line: `skills · (no skills/ dir)` });
@@ -368,7 +373,10 @@ function gatherLayerSummary(codehereDir, projectRoot) {
368
373
  const parsed = JSON.parse(readFileSync(patternsPath, 'utf8'));
369
374
  const builtin = Array.isArray(parsed.patterns_builtin) ? parsed.patterns_builtin.length : 0;
370
375
  const project = Array.isArray(parsed.patterns_project) ? parsed.patterns_project.length : 0;
371
- out.push({ layer: 'protocols', line: `protocols · hook_patterns.json (${builtin} built-in + ${project} project)` });
376
+ out.push({
377
+ layer: 'protocols',
378
+ line: `protocols · hook_patterns.json (${builtin} built-in + ${project} project)`,
379
+ });
372
380
  }
373
381
  catch {
374
382
  out.push({ layer: 'protocols', line: `protocols · hook_patterns.json (parse error)` });
@@ -389,7 +397,10 @@ function gatherLayerSummary(codehereDir, projectRoot) {
389
397
  }
390
398
  else {
391
399
  const truncNote = inj.truncated ? ' TRUNCATED' : '';
392
- out.push({ layer: 'injection', line: `injection · ${inj.tokens} tokens · ${inj.included.join(', ')}${truncNote}` });
400
+ out.push({
401
+ layer: 'injection',
402
+ line: `injection · ${inj.tokens} tokens · ${inj.included.join(', ')}${truncNote}`,
403
+ });
393
404
  }
394
405
  return out;
395
406
  }
@@ -526,7 +537,10 @@ function executeMemoryShow(layerArg) {
526
537
  console.log(` ${info('empty ')} no lessons.jsonl yet`);
527
538
  break;
528
539
  }
529
- const lines = readFileSync(path, 'utf8').trim().split('\n').filter(l => l.length > 0);
540
+ const lines = readFileSync(path, 'utf8')
541
+ .trim()
542
+ .split('\n')
543
+ .filter((l) => l.length > 0);
530
544
  if (lines.length === 0) {
531
545
  console.log(` ${info('empty ')} lessons.jsonl is empty — run ${colors.cyan('codehere memory graduate')} (coming in v0.5)`);
532
546
  break;
@@ -561,8 +575,8 @@ function executeMemoryShow(layerArg) {
561
575
  break;
562
576
  }
563
577
  const subdirs = readdirSync(skillsDir, { withFileTypes: true })
564
- .filter(e => e.isDirectory())
565
- .filter(e => existsSync(join(skillsDir, e.name, 'SKILL.md')));
578
+ .filter((e) => e.isDirectory())
579
+ .filter((e) => existsSync(join(skillsDir, e.name, 'SKILL.md')));
566
580
  if (subdirs.length === 0) {
567
581
  console.log(` ${info('empty ')} no skill folders yet (skills/<name>/SKILL.md)`);
568
582
  break;
@@ -907,8 +921,8 @@ function executeMemoryInit(options) {
907
921
  }
908
922
  // Idempotency hint when nothing was actually migrated.
909
923
  const allOutcomes = [contextOutcome, handoverOutcome, promptsResult.outcome];
910
- const anyMigrated = allOutcomes.some(o => o === 'migrated');
911
- const allAlreadyOrMissing = allOutcomes.every(o => o === 'already-migrated' || o === 'missing');
924
+ const anyMigrated = allOutcomes.some((o) => o === 'migrated');
925
+ const allAlreadyOrMissing = allOutcomes.every((o) => o === 'already-migrated' || o === 'missing');
912
926
  if (!anyMigrated && allAlreadyOrMissing) {
913
927
  newline();
914
928
  console.log(` ${info('no-op')} everything already migrated or no source files to migrate.`);
@@ -122,8 +122,8 @@ export function buildHookPatternsExport() {
122
122
  source: 'agent/src/infrastructure/security/trust-scorer.ts',
123
123
  _note: "Exported from codehere's trust-scorer pattern bank via 'codehere protocols export'. " +
124
124
  "DO NOT hand-edit `patterns_builtin[]` — it's regenerated on every export. " +
125
- "Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. " +
126
- "Pattern ids are stable slugs of category + description; reference them deterministically.",
125
+ 'Add project-specific patterns to `patterns_project[]` — those are preserved across re-exports. ' +
126
+ 'Pattern ids are stable slugs of category + description; reference them deterministically.',
127
127
  patterns_builtin: builtin,
128
128
  patterns_project: [],
129
129
  };
@@ -107,8 +107,8 @@ function gitListFiles(repoRoot) {
107
107
  return null;
108
108
  return r.stdout
109
109
  .split('\0')
110
- .filter(p => p.length > 0)
111
- .filter(p => SUPPORTED_EXTENSIONS.has(extname(p)));
110
+ .filter((p) => p.length > 0)
111
+ .filter((p) => SUPPORTED_EXTENSIONS.has(extname(p)));
112
112
  }
113
113
  /**
114
114
  * Count references to a symbol name across the repo via git grep.
@@ -191,13 +191,13 @@ export function buildRepoMap(repoRoot) {
191
191
  // acceptable for v0.5 — codehere's own 249-symbol repo runs in ~5s.
192
192
  // v0.6's tree-sitter port will swap raw grep for AST-aware reference
193
193
  // resolution + PageRank, which is faster AND more accurate.
194
- const uniqueNames = Array.from(new Set(allSymbols.map(s => s.name)));
194
+ const uniqueNames = Array.from(new Set(allSymbols.map((s) => s.name)));
195
195
  const refCounts = new Map();
196
196
  for (const name of uniqueNames) {
197
197
  refCounts.set(name, countReferences(repoRoot, name));
198
198
  }
199
199
  const topSymbols = allSymbols
200
- .map(s => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
200
+ .map((s) => ({ ...s, references: refCounts.get(s.name) ?? 0 }))
201
201
  // Stable sort: references DESC, then name ASC for tie-breaking.
202
202
  .sort((a, b) => b.references - a.references || a.name.localeCompare(b.name))
203
203
  .slice(0, 50);
@@ -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
 
@@ -6,9 +6,47 @@
6
6
  import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import { getConfigManager } from '../../../infrastructure/config/config-manager.js';
9
- import { colors, separator, newline, sectionHeader, success, error, warning, info, } from '../../../ui.js';
9
+ import { colors, separator, newline, sectionHeader, success, error, warning, info } from '../../../ui.js';
10
10
  import { executionTracer } from '../../../infrastructure/observability/execution-tracer.js';
11
11
  import { findCodebaseRoot } from '../../../domain/services/codebase-detector.js';
12
+ import { readinessForAllAgents } from '../../../infrastructure/orchestration/session-manager.js';
13
+ /**
14
+ * What codehere actually needs to run a task: at least one coding-agent CLI
15
+ * that's installed AND signed in. Most engineers have `claude` or `codex`
16
+ * logged in by subscription with no API key set, so framing setup around
17
+ * `ANTHROPIC_API_KEY` told them they were blocked when they weren't. This
18
+ * reuses the same readiness check that powers the web dashboard's squad bar.
19
+ */
20
+ function summarizeAgents() {
21
+ let all;
22
+ try {
23
+ all = readinessForAllAgents();
24
+ }
25
+ catch {
26
+ all = [];
27
+ }
28
+ return { ready: all.filter((a) => a.state === 'ready'), all };
29
+ }
30
+ /** One status line per agent, plain English. Used by both the TTY wizard and
31
+ * the non-TTY summary so they say the same thing. */
32
+ function printAgentLines(all) {
33
+ for (const a of all) {
34
+ if (a.state === 'ready') {
35
+ console.log(success(`${a.label}: ready`));
36
+ }
37
+ else if (a.state === 'needs_auth') {
38
+ const hint = a.authHint?.command ? colors.dim(` (sign in: ${a.authHint.command})`) : '';
39
+ console.log(warning(`${a.label}: installed, needs sign-in`) + hint);
40
+ }
41
+ else if (a.state === 'billed_key_required') {
42
+ console.log(colors.dim(`○ ${a.label}: needs a billed API key (paid)`));
43
+ }
44
+ else {
45
+ const hint = a.installHint?.command ? colors.dim(` (install: ${a.installHint.command})`) : '';
46
+ console.log(colors.dim(`○ ${a.label}: not installed`) + hint);
47
+ }
48
+ }
49
+ }
12
50
  /**
13
51
  * Check if any API key is configured
14
52
  */
@@ -113,19 +151,26 @@ export async function executeSetupCommand(force = false) {
113
151
  // Check for interactive terminal
114
152
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
115
153
  if (!isTTY) {
116
- console.log(colors.bold('Setup Wizard'));
154
+ const { ready, all } = summarizeAgents();
155
+ console.log(colors.bold('Setup'));
117
156
  console.log();
118
- console.log(warning('Interactive terminal required.'));
157
+ if (all.length)
158
+ printAgentLines(all);
119
159
  console.log();
120
- console.log('Set an API key in your environment:');
121
- console.log(colors.cyan(' export ANTHROPIC_API_KEY=sk-ant-...') + colors.dim(' (recommended)'));
122
- console.log(colors.cyan(' export OPENAI_API_KEY=sk-...'));
123
- console.log(colors.cyan(' export COHERE_API_KEY=...'));
160
+ if (ready.length) {
161
+ console.log(success(`You're ready. ${ready[0].label} is set up.`));
162
+ console.log(colors.dim(' Next: codehere start (open the dashboard)'));
163
+ }
164
+ else {
165
+ console.log(warning('No coding agent is set up yet. Install or sign in to one:'));
166
+ console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
167
+ console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
168
+ }
124
169
  return;
125
170
  }
126
171
  // Start execution tracing
127
172
  const codebaseInfo = findCodebaseRoot(process.cwd());
128
- const runId = executionTracer.startRun('setup', {
173
+ executionTracer.startRun('setup', {
129
174
  codebaseId: codebaseInfo?.rootPath || process.cwd(),
130
175
  });
131
176
  try {
@@ -147,21 +192,29 @@ export async function executeSetupCommand(force = false) {
147
192
  codebaseDetected: false,
148
193
  configPath: join(process.cwd(), 'data'),
149
194
  };
150
- // Step 1: API Key
151
- sectionHeader('Step 1: API Key Configuration');
195
+ // Step 1: Coding agents — codehere drives agent CLIs, not raw API keys.
196
+ sectionHeader('Step 1: Your coding agents');
197
+ const { ready, all } = summarizeAgents();
198
+ if (all.length)
199
+ printAgentLines(all);
200
+ newline();
152
201
  const apiKeyCheck = checkAPIKey();
153
- if (apiKeyCheck.configured) {
154
- console.log(success(`API Key: ${apiKeyCheck.provider} key found`));
155
- console.log(colors.dim(` Key preview: ${apiKeyCheck.key?.substring(0, 8)}...${apiKeyCheck.key?.substring(apiKeyCheck.key.length - 4)}`));
202
+ if (ready.length) {
203
+ console.log(success(`You're ready. ${ready[0].label} is set up.`));
204
+ result.apiKeyConfigured = true; // at least one agent can spawn right now
205
+ }
206
+ else if (apiKeyCheck.configured) {
207
+ // No agent CLI ready, but a key is present — some adapters (e.g. Aider) run off it.
208
+ console.log(success(`${apiKeyCheck.provider} API key found.`));
209
+ console.log(colors.dim(' No agent CLI detected yet — install one for the full experience.'));
156
210
  result.apiKeyConfigured = true;
157
211
  }
158
212
  else {
159
- console.log(warning('API Key: Not configured'));
213
+ console.log(warning('No coding agent is set up yet.'));
160
214
  newline();
161
- console.log(colors.bold('Set one of these environment variables:'));
162
- console.log(colors.cyan(' export ANTHROPIC_API_KEY=sk-ant-...') + colors.dim(' (recommended)'));
163
- console.log(colors.cyan(' export OPENAI_API_KEY=sk-...'));
164
- console.log(colors.cyan(' export COHERE_API_KEY=...'));
215
+ console.log(colors.bold('Install or sign in to at least one:'));
216
+ console.log(colors.cyan(' npm i -g @anthropic-ai/claude-code') + colors.dim(' then run: claude (sign in)'));
217
+ console.log(colors.dim(' Already use one by API key? export ANTHROPIC_API_KEY=sk-ant-...'));
165
218
  newline();
166
219
  }
167
220
  newline();
@@ -185,10 +185,6 @@ function extractFilePath(entry) {
185
185
  }
186
186
  return null;
187
187
  }
188
- function shortReason(entry, maxLen = 60) {
189
- const r = extractBlockReason(entry);
190
- return r.length <= maxLen ? r : r.substring(0, maxLen - 1) + '…';
191
- }
192
188
  const BLOCK_CATEGORY_LABEL = {
193
189
  'self-test': "codehere's own canary tests",
194
190
  'cache-cleanup': 'routine cache/tmp deletion',