codehere 0.8.0 → 1.0.2

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 (119) hide show
  1. package/README.md +37 -184
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +329 -0
  5. package/dist/index.js +74 -1004
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -265
  7. package/dist/presentation/cli/commands/check-command.js +251 -0
  8. package/dist/presentation/cli/commands/help-command.js +22 -286
  9. package/dist/ui.js +19 -438
  10. package/dist/utils/version.js +0 -12
  11. package/package.json +18 -27
  12. package/dist/application/use-cases/domain-trust.js +0 -110
  13. package/dist/application/use-cases/generate-final-report.js +0 -134
  14. package/dist/application/use-cases/generate-narration.js +0 -128
  15. package/dist/application/use-cases/generate-thread-title.js +0 -178
  16. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  17. package/dist/application/use-cases/thread-export.js +0 -155
  18. package/dist/application/use-cases/verify-claim.js +0 -274
  19. package/dist/application/use-cases/write-pass.js +0 -277
  20. package/dist/domain/entities/task.js +0 -61
  21. package/dist/domain/services/codebase-detector.js +0 -91
  22. package/dist/error-handler.js +0 -251
  23. package/dist/infrastructure/completion/bash-completion.js +0 -25
  24. package/dist/infrastructure/completion/fish-completion.js +0 -22
  25. package/dist/infrastructure/completion/zsh-completion.js +0 -30
  26. package/dist/infrastructure/config/config-manager.js +0 -173
  27. package/dist/infrastructure/config/product-context.js +0 -67
  28. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  29. package/dist/infrastructure/context/adapters/figma-source.js +0 -163
  30. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -159
  31. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  32. package/dist/infrastructure/context/context-resolver.js +0 -144
  33. package/dist/infrastructure/context/context-source.js +0 -114
  34. package/dist/infrastructure/context/oauth-flow.js +0 -207
  35. package/dist/infrastructure/context/oauth-store.js +0 -119
  36. package/dist/infrastructure/context/registry-bootstrap.js +0 -99
  37. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  38. package/dist/infrastructure/cost/budget-config.js +0 -113
  39. package/dist/infrastructure/cost/cost-router.js +0 -104
  40. package/dist/infrastructure/cost/spend-aggregator.js +0 -147
  41. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -888
  42. package/dist/infrastructure/hooks/hook-mode.js +0 -71
  43. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -43
  44. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  45. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  46. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  47. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  48. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  49. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -56
  50. package/dist/infrastructure/orchestration/aider-adapter.js +0 -447
  51. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  52. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -478
  53. package/dist/infrastructure/orchestration/codex-adapter.js +0 -538
  54. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -377
  55. package/dist/infrastructure/orchestration/context-handoff.js +0 -144
  56. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  57. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  58. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  59. package/dist/infrastructure/orchestration/failover-config.js +0 -77
  60. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  61. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -583
  62. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -558
  63. package/dist/infrastructure/orchestration/graphify-context.js +0 -254
  64. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  65. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -233
  66. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  67. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  68. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  69. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -510
  70. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -689
  71. package/dist/infrastructure/orchestration/readiness.js +0 -103
  72. package/dist/infrastructure/orchestration/session-manager.js +0 -1091
  73. package/dist/infrastructure/orchestration/session-persistence.js +0 -258
  74. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  75. package/dist/infrastructure/orchestration/write-pass-store.js +0 -46
  76. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  77. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  78. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -2853
  79. package/dist/infrastructure/protocols/probe-port.js +0 -36
  80. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  81. package/dist/infrastructure/protocols/ui-page-v2.js +0 -4418
  82. package/dist/infrastructure/protocols/ui-page.js +0 -2377
  83. package/dist/infrastructure/protocols/verify-page.js +0 -983
  84. package/dist/infrastructure/security/file-collection.js +0 -118
  85. package/dist/infrastructure/security/input-validator.js +0 -152
  86. package/dist/infrastructure/security/permissions-config.js +0 -234
  87. package/dist/infrastructure/security/trust-scorer.js +0 -657
  88. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  89. package/dist/infrastructure/storage/embeddings-path.js +0 -37
  90. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  91. package/dist/infrastructure/storage/project-thread-mirror.js +0 -524
  92. package/dist/infrastructure/storage/project-thread-reader.js +0 -172
  93. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  94. package/dist/infrastructure/storage/task-repository.js +0 -206
  95. package/dist/monitoring.js +0 -212
  96. package/dist/presentation/cli/commands/audit-command.js +0 -126
  97. package/dist/presentation/cli/commands/budget-command.js +0 -79
  98. package/dist/presentation/cli/commands/config-command.js +0 -125
  99. package/dist/presentation/cli/commands/context-source-command.js +0 -198
  100. package/dist/presentation/cli/commands/hook-command.js +0 -265
  101. package/dist/presentation/cli/commands/init-command.js +0 -578
  102. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  103. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  104. package/dist/presentation/cli/commands/permission-command.js +0 -116
  105. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  106. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  107. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  108. package/dist/presentation/cli/commands/report-command.js +0 -387
  109. package/dist/presentation/cli/commands/scan-tool-command.js +0 -94
  110. package/dist/presentation/cli/commands/setup-command.js +0 -307
  111. package/dist/presentation/cli/commands/status-command.js +0 -661
  112. package/dist/presentation/cli/commands/thread-command.js +0 -288
  113. package/dist/presentation/cli/commands/trust-command.js +0 -306
  114. package/dist/presentation/cli/commands/verify-command.js +0 -233
  115. package/dist/presentation/cli/error-display.js +0 -336
  116. package/dist/presentation/cli/repl.js +0 -797
  117. package/dist/presentation/cli/task-deliverable.js +0 -127
  118. package/dist/testing/cassette.js +0 -125
  119. package/dist/testing/replay-adapter.js +0 -166
package/dist/index.js CHANGED
@@ -1,360 +1,98 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * codehere CLI entry · v0.3 PM-for-AI-coding-agents surface.
4
- *
5
- * Command surface trimmed to the v0.3 story (see Decision 10 in memory):
6
- * setup · config · help · health · status · trust · report · hook ·
7
- * serve · start · task · kill · scan-tool · completion
8
- *
9
- * v0.2 commands (ask / fix / plan / explain / refactor / index / etc.) and
10
- * their backing code were removed in the D7 trim. The v0.2 npm releases
11
- * retain that surface for users who depended on it.
3
+ * codehere CLI entry: `check` re-runs a project's tests and reports what actually passed.
4
+ * The earlier orchestrator, daemon, web UI and the other commands are on the legacy/2026-10 branch.
12
5
  */
13
6
  import { Command } from 'commander';
14
- import { config } from 'dotenv';
15
7
  import { join, dirname } from 'path';
16
8
  import { fileURLToPath } from 'url';
17
- import { homedir } from 'os';
18
- // ── Global crash safety ───────────────────────────────────────────────────
19
- // Unhandled rejections and exceptions must never silently kill the daemon.
20
- process.on('unhandledRejection', (reason) => {
21
- console.error('[codehere] unhandledRejection:', reason instanceof Error ? reason.stack : reason);
22
- });
9
+ import { readFileSync } from 'fs';
10
+ // A crash must stop the process: `process.exitCode = 1` is only honoured once the event loop drains.
11
+ function safeVersion() {
12
+ try {
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+ return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8')).version ?? 'unknown';
15
+ }
16
+ catch {
17
+ return 'unknown';
18
+ }
19
+ }
20
+ function crashReport(kind, err) {
21
+ const e = err instanceof Error ? err : new Error(String(err));
22
+ return [
23
+ '',
24
+ `[codehere] ${kind} — this process is stopping.`,
25
+ ` version : ${safeVersion()}`,
26
+ ` node : ${process.version} on ${process.platform} ${process.arch}`,
27
+ '',
28
+ (e.stack ?? e.message)
29
+ .split('\n')
30
+ .map((l) => ` ${l}`)
31
+ .join('\n'),
32
+ '',
33
+ ' Report at https://github.com/muhammadegaa/codehere/issues with the block above.',
34
+ '',
35
+ ].join('\n');
36
+ }
23
37
  process.on('uncaughtException', (err) => {
24
- console.error('[codehere] uncaughtException:', err.stack);
25
- // persist what we can before dying
26
- process.exitCode = 1;
38
+ console.error(crashReport('uncaughtException', err));
39
+ process.exit(1);
40
+ });
41
+ process.on('unhandledRejection', (reason) => {
42
+ console.error(crashReport('unhandledRejection', reason));
43
+ process.exit(1);
27
44
  });
28
- import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
29
- import { executeInitCommand } from './presentation/cli/commands/init-command.js';
30
- import { executeMemoryCommand } from './presentation/cli/commands/memory-command.js';
31
- import { executeProtocolsCommand } from './presentation/cli/commands/protocols-command.js';
32
- import { executeConfigCommand } from './presentation/cli/commands/config-command.js';
45
+ import { executeCheckCommand } from './presentation/cli/commands/check-command.js';
33
46
  import { executeHelpCommand } from './presentation/cli/commands/help-command.js';
34
- import { executeStatusCommand } from './presentation/cli/commands/status-command.js';
35
- import { executeHookCommand } from './presentation/cli/commands/hook-command.js';
36
- import { executePermissionCommand } from './presentation/cli/commands/permission-command.js';
37
- import { executeBudgetCommand } from './presentation/cli/commands/budget-command.js';
38
- import { executeNarratorCommand } from './presentation/cli/commands/narrator-command.js';
39
- import { executeTrustCommand } from './presentation/cli/commands/trust-command.js';
40
- import { executeReportCommand } from './presentation/cli/commands/report-command.js';
41
- import { executeScanToolCommand } from './presentation/cli/commands/scan-tool-command.js';
42
- import { executeVerifyCommand } from './presentation/cli/commands/verify-command.js';
43
- import { executeAuditCommand } from './presentation/cli/commands/audit-command.js';
44
- import { executeContextSourceCommand } from './presentation/cli/commands/context-source-command.js';
45
- import { executeThreadCommand } from './presentation/cli/commands/thread-command.js';
46
- import { displayError } from './presentation/cli/error-display.js';
47
- import { performHealthCheck, formatHealthStatus, getMetrics } from './monitoring.js';
48
- import { colors, createBanner, createTable, createWelcomeMessage, success, error, info, sectionHeader, formatNumber, newline, isMinimalStyle, printMinimalHeader, } from './ui.js';
49
- // Load .env — agent dir first, then cwd
50
- try {
51
- const __filename = fileURLToPath(import.meta.url);
52
- const __dirname = dirname(__filename);
53
- const agentDir = join(__dirname, '..');
54
- config({ path: join(agentDir, '.env') });
55
- }
56
- catch {
57
- // Fallback to cwd
58
- }
59
- config();
47
+ import { error } from './ui.js';
60
48
  import { getVersion } from './utils/version.js';
61
49
  import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
62
50
  const program = new Command();
63
51
  program
64
52
  .name('codehere')
65
- .description('A PM for your AI coding agents. Delegate a task, watch it run, and get a plain-English report with a trust score on every file it changed.')
53
+ .description('Re-run your project\'s tests yourself, so an AI\'s "all tests pass" is checked, not taken on trust.')
66
54
  .version(getVersion())
67
- .configureHelp({ helpWidth: 80, sortSubcommands: true })
68
- // rc.18: many users hit `codehere --help` first and get Commander's flat
69
- // list. The grouped `codehere help` is more useful for discovery. Surface
70
- // it inline so muscle-memory `--help` users find the better page.
71
- // (Pin: no raw ANSI in --help output — existing commands.test.ts asserts
72
- // this, see test/cli/commands.test.ts "no raw ANSI escape codes in --help".)
73
- .addHelpText('after', '\nTip: `codehere help` shows commands grouped by use case (Delegate · Safety & Trust · Configuration).');
74
- // Default action · show welcome, then drop into the interactive REPL on TTY.
75
- // codehere offers TWO surfaces, both first-class: the CLI REPL (here) and the
76
- // web UI (`codehere start`). Users pick whichever fits their workflow.
77
- program
78
- // Variadic so multi-token input is preserved end-to-end. Without this,
79
- // `codehere fix the bug` would silently drop "the bug" and only delegate
80
- // "fix" — and `codehere daemon start` would never see "start", letting the
81
- // typo guard miss the unknown-command shape. See typo-guard.ts.
82
- .argument('[query...]', 'Optional initial task to delegate (otherwise enter interactive REPL)')
83
- .action(async (queryParts) => {
84
- const joined = queryParts && queryParts.length > 0 ? queryParts.join(' ') : '';
85
- // One-shot delegate: `codehere "review src/auth.ts"` runs that task and exits.
86
- // Useful for scripting or quick one-offs without entering the REPL.
87
- if (joined && joined.trim()) {
88
- const trimmed = joined.trim();
89
- // Typo guard — see src/utils/typo-guard.ts for heuristics.
90
- // Returns suggestions when input looks like a typo; null falls through.
91
- const known = program.commands.map((c) => c.name());
92
- const firstToken = trimmed.split(/\s+/)[0];
93
- const suggestions = checkTypo(trimmed, known);
94
- if (suggestions) {
95
- console.error(`codehere: unknown command '${firstToken}'.`);
96
- console.error('');
97
- console.error(' Did you mean:');
98
- for (const s of suggestions)
99
- console.error(` codehere ${s}`);
100
- console.error('');
101
- console.error(` If you meant to ask a question, quote it: codehere "${trimmed}"`);
102
- process.exit(1);
103
- }
104
- // Second guard — `cmd subcmd` shape (e.g. `daemon start`) where the first
105
- // token isn't close enough for typo suggestions but is clearly a command
106
- // attempt rather than a natural-language prompt. Without this, the input
107
- // silently spawns an LLM task on the user's behalf and burns API tokens.
108
- const unknownFirst = looksLikeUnknownCommand(trimmed, known);
109
- if (unknownFirst) {
110
- console.error(`codehere: unknown command '${unknownFirst}'.`);
111
- console.error('');
112
- console.error(' Run `codehere help` to see available commands.');
113
- console.error('');
114
- console.error(' If you meant to delegate this as a task, either:');
115
- console.error(` codehere task ${trimmed}`);
116
- console.error(` codehere "${trimmed}"`);
117
- process.exit(1);
118
- }
119
- const { runOneShot } = await import('./presentation/cli/repl.js');
120
- await runOneShot(trimmed);
121
- return;
122
- }
123
- if (!process.stdin.isTTY) {
124
- console.log(createWelcomeMessage());
125
- // Non-interactive (piped / scripted): the REPL can't run, so leave the
126
- // user with ONE clear next step instead of a dead-end welcome screen.
127
- console.log(colors.dim(' Next: ') + colors.cyan('codehere start') + colors.dim(' open the dashboard'));
128
- console.log(colors.dim(' New here? ') + colors.cyan('codehere setup') + colors.dim(' one-time setup\n'));
129
- return;
130
- }
131
- const { runInteractiveRepl } = await import('./presentation/cli/repl.js');
132
- await runInteractiveRepl();
133
- });
134
- // ── setup ────────────────────────────────────────────────────────────────────
135
- program
136
- .command('setup')
137
- .description('Interactive setup wizard for first-time users')
138
- .option('-f, --force', 'Force reconfiguration even if already set up')
139
- .action(async (options) => {
140
- await executeSetupCommand(options.force || false);
141
- });
142
- // ── init ─────────────────────────────────────────────────────────────────────
143
- program
144
- .command('init')
145
- .description('Scaffold .codehere/ in this repo · CONTEXT, HANDOVER, PROMPTS')
146
- .option('-f, --force', 'Overwrite existing files in .codehere/')
147
- .option('--no-gitignore', 'Skip updating .gitignore')
148
- .action((options) => {
149
- executeInitCommand({ force: options.force, noGitignore: !options.gitignore });
150
- });
151
- // ── memory ───────────────────────────────────────────────────────────────────
152
- // F3 v0.5: project memory operations. `memory init` migrates v0.4.1 layout
153
- // (CONTEXT/HANDOVER/PROMPTS) into the four-layer F3 layout (AGENTS.md +
154
- // memory/ + skills/). `memory repo-map` builds a JSON symbol map. Future:
155
- // memory show, memory graduate.
156
- program
157
- .command('memory')
158
- .description('F3 project memory · init / repo-map / show / graduate / help')
159
- .argument('[action]', 'Action: init, repo-map, show, graduate, help')
160
- .argument('[layer]', 'Layer for `memory show` (working / episodic / semantic / personal / skills / protocols / injection)')
161
- .option('-f, --force', 'Overwrite existing target files (memory init)')
162
- .option('--no-symlinks', 'Delete old paths instead of leaving back-compat symlinks (memory init)')
163
- .option('--dry-run', 'Print planned moves without touching disk (memory init)')
164
- .option('--rebuild', 'Force a fresh build (memory repo-map)')
165
- .option('--add <text>', 'Add a free-text lesson directly (memory graduate)')
166
- .option('--accept <id>', 'Graduate a candidate by id (memory graduate)')
167
- .option('--reject <id>', 'Reject a candidate by id without graduating (memory graduate)')
168
- .action((action, layer, options) => {
169
- executeMemoryCommand(action, {
170
- force: options.force,
171
- noSymlinks: !options.symlinks,
172
- dryRun: options.dryRun,
173
- rebuild: options.rebuild,
174
- layer,
175
- add: options.add,
176
- accept: options.accept,
177
- reject: options.reject,
178
- });
179
- });
180
- // ── protocols ────────────────────────────────────────────────────────────────
181
- // F3 v0.5: protocols layer operations. `protocols export` writes the
182
- // trust-scorer pattern bank to .codehere/protocols/hook_patterns.json so
183
- // auditors / cross-tool consumers can read codehere's safety surface.
184
- program
185
- .command('protocols', { hidden: true }) // hidden from --help (May 2 cut: power-user F3 export, see docs/MEMORY.md)
186
- .description('F3 protocols layer · export (write hook_patterns.json from trust-scorer bank)')
187
- .argument('[action]', 'Action: export, help')
188
- .option('--rebuild', 'Refresh built-in patterns (project patterns are preserved)')
189
- .action((action, options) => {
190
- executeProtocolsCommand(action, { rebuild: options.rebuild });
191
- });
192
- // ── config ───────────────────────────────────────────────────────────────────
193
- program
194
- .command('config')
195
- .description('Manage configuration and preferences')
196
- .argument('[action]', 'Action: show, get, set, reset')
197
- .argument('[key]', 'Preference key (for get/set)')
198
- .argument('[value]', 'Preference value (for set)')
199
- .action(async (action, key, value) => {
200
- await executeConfigCommand(action, key, value);
201
- });
202
- // ── help ─────────────────────────────────────────────────────────────────────
203
- program
204
- .command('help')
205
- .description('Show help for commands')
206
- .argument('[command]', 'Command name to get help for')
207
- .option('--all', 'Show all commands (including advanced)')
208
- .action(async (commandName, options) => {
209
- if (options?.all && !commandName) {
210
- for (const cmd of program.commands) {
211
- cmd._hidden = false;
212
- }
55
+ .configureHelp({ helpWidth: 80, sortSubcommands: true });
56
+ // Bare `codehere` prints help. Anything else that is not a command goes through the typo guard.
57
+ program.allowExcessArguments(true).action(() => {
58
+ const input = program.args.join(' ').trim();
59
+ if (!input) {
213
60
  program.outputHelp();
214
- }
215
- else {
216
- await executeHelpCommand(commandName);
217
- }
218
- });
219
- // ── health ───────────────────────────────────────────────────────────────────
220
- program
221
- .command('health')
222
- .description('Check system health and metrics')
223
- .option('-m, --metrics', 'Show detailed metrics')
224
- .option('--json', 'Machine-readable JSON (for monitoring/CI)')
225
- .action((options) => {
226
- const health = performHealthCheck();
227
- if (options.json) {
228
- // Machine-readable: status + checks (+ metrics when --metrics).
229
- // Stdout is the contract — never mix with the human banner.
230
- // Use process.exitCode (not process.exit) so a piped consumer sees the
231
- // full JSON before the process tears down (rc.16: Codex flagged the old
232
- // process.exit call as a truncation hazard under stdout redirection).
233
- const payload = { status: health.status, checks: health.checks };
234
- if (options.metrics)
235
- payload.metrics = getMetrics();
236
- console.log(JSON.stringify(payload, null, 2));
237
- if (health.status === 'unhealthy')
238
- process.exitCode = 1;
239
61
  return;
240
62
  }
241
- if (isMinimalStyle()) {
242
- printMinimalHeader('health', '');
63
+ const known = program.commands.map((c) => c.name());
64
+ const suggestions = checkTypo(input, known);
65
+ const unknown = suggestions
66
+ ? input.split(/\s+/)[0]
67
+ : (looksLikeUnknownCommand(input, known) ?? input.split(/\s+/)[0]);
68
+ console.error(`codehere: unknown command '${unknown}'.`);
69
+ console.error('');
70
+ if (suggestions) {
71
+ console.error(' Did you mean:');
72
+ for (const s of suggestions)
73
+ console.error(` codehere ${s}`);
243
74
  }
244
75
  else {
245
- newline();
246
- console.log(createBanner('System Health', 'Quick status check'));
247
- newline();
248
- }
249
- console.log(formatHealthStatus(health));
250
- if (options.metrics) {
251
- newline();
252
- sectionHeader('Metrics');
253
- const metrics = getMetrics();
254
- const metricsTable = createTable(['Metric', 'Value'], [
255
- ['Total Operations', formatNumber(metrics.operations.total)],
256
- ['Successful', formatNumber(metrics.operations.successful)],
257
- ['Failed', formatNumber(metrics.operations.failed)],
258
- ['Retries', formatNumber(metrics.operations.retries)],
259
- ['Avg Response Time', `${metrics.performance.avgResponseTime.toFixed(2)}ms`],
260
- ['API Calls', formatNumber(metrics.performance.apiCalls)],
261
- ['Cache Hits', formatNumber(metrics.performance.cacheHits)],
262
- ]);
263
- console.log(metricsTable.toString());
264
- }
265
- newline();
266
- if (health.status === 'unhealthy') {
267
- process.exit(1);
76
+ console.error(' Run `codehere help` to see available commands.');
268
77
  }
78
+ console.error('');
79
+ process.exit(1);
269
80
  });
270
- // ── status ───────────────────────────────────────────────────────────────────
271
81
  program
272
- .command('status')
273
- .description('Security dashboard — hook activity, blocked calls, cost routing')
274
- .option('--json', 'Machine-readable JSON')
275
- .option('--tasks', 'Show recent tasks (experimental)')
276
- .option('--sessions', 'Show per-session audit trail (multi-agent analysis)')
277
- .option('--session <id>', 'Drill into a specific session (first 8+ chars of session ID)')
278
- .option('--trust', 'Include a quick trust scan of the current directory')
279
- .option('--weekly', 'Show shareable weekly agent-activity recap (last 7 days)')
280
- .option('--export <format>', 'Export raw audit events: jsonl, json, csv, or md')
281
- .option('--since <duration>', 'Limit --export window, e.g. 24h, 7d, 4w')
282
- .option('--env', 'Show environment details')
283
- .option('--track', 'Show command success rates')
284
- .addHelpText('after', `
285
- Examples:
286
- $ codehere status # full dashboard
287
- $ codehere status --weekly # shareable 7-day agent recap
288
- $ codehere status --sessions # show per-session breakdown
289
- $ codehere status --session abc12345 # drill into a specific session
290
- $ codehere status --export jsonl > audit.jsonl # raw audit log export
291
- $ codehere status --export csv --since 7d # 7-day CSV for spreadsheets
292
- $ codehere status --export md --since 24h # 24-hour markdown report
293
- `)
294
- .action(async (options) => {
295
- await executeStatusCommand({
296
- json: Boolean(options.json),
297
- tasks: Boolean(options.tasks),
298
- sessions: Boolean(options.sessions),
299
- session: options.session,
300
- trust: Boolean(options.trust),
301
- weekly: Boolean(options.weekly),
302
- export: options.export,
303
- since: options.since,
304
- env: Boolean(options.env),
305
- track: Boolean(options.track),
306
- });
307
- });
308
- // ── trust ────────────────────────────────────────────────────────────────────
309
- program
310
- .command('trust')
311
- .description('Layer 0 trust score — instant static analysis, zero API calls')
312
- .argument('[target]', 'File, directory, or omit to scan current directory')
313
- .option('--diff [ref]', 'Score a git diff (default: HEAD)')
314
- .option('--badge', 'Output shield badge markdown')
315
- .option('--json', 'Output JSON report')
316
- .option('--verbose', 'Show all findings with line numbers')
317
- .option('--ci', 'CI mode: exit 1 if score < 60')
318
- .action(async (target, options) => {
319
- try {
320
- await executeTrustCommand(target, {
321
- diff: options?.diff === true ? 'HEAD' : options?.diff,
322
- badge: options?.badge,
323
- json: options?.json,
324
- verbose: options?.verbose,
325
- ci: options?.ci,
326
- });
327
- }
328
- catch (err) {
329
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
330
- process.exit(1);
331
- }
332
- });
333
- // ── report ───────────────────────────────────────────────────────────────────
334
- program
335
- .command('report')
336
- .description('Generate a shareable security audit report — narrative, not just a score')
337
- .argument('[target]', 'File or directory to audit (default: current directory)')
338
- .option('--out <file>', 'Write report to a specific file (default: codehere-audit.md for markdown)')
339
- .option('--stdout', 'Print the report to stdout (already the default for --json)')
340
- .option('--json', 'Output machine-readable JSON to stdout (pipe to jq, etc.)')
341
- .option('--simple', 'Plain English output — no jargon, for non-technical stakeholders')
342
- .addHelpText('after', `
343
- Examples:
344
- $ codehere report # writes codehere-audit.md, prints summary
345
- $ codehere report src/ # audit a subdirectory
346
- $ codehere report --out my-audit.md # write to a specific path
347
- $ codehere report --stdout # print to terminal (pipe-friendly)
348
- $ codehere report --json # machine-readable output
349
- $ codehere report --simple # plain English for non-technical stakeholders
350
- `)
82
+ .command('check')
83
+ .description('Re-run your tests independently — proof of what the AI actually did, not what it claimed')
84
+ .argument('[target]', 'Repo or directory to check (default: current directory)')
85
+ .option('--json', 'Machine-readable output')
86
+ .option('--proof', 'Emit a paste-ready proof block for a PR description or review comment')
87
+ .option('--claim <text>', "The AI's own summary: test files it names that are not in the repo are flagged")
88
+ .option('--base <ref>', 'Look for weakened tests in everything since this branch left <ref> (e.g. origin/main)')
351
89
  .action(async (target, options) => {
352
90
  try {
353
- await executeReportCommand(target, {
354
- out: options?.out,
355
- stdout: options?.stdout,
91
+ process.exitCode = await executeCheckCommand(target, {
356
92
  json: options?.json,
357
- simple: options?.simple,
93
+ proof: options?.proof,
94
+ claim: options?.claim,
95
+ base: options?.base,
358
96
  });
359
97
  }
360
98
  catch (err) {
@@ -362,680 +100,12 @@ Examples:
362
100
  process.exit(1);
363
101
  }
364
102
  });
365
- // ── verify ───────────────────────────────────────────────────────────────────
366
- // v0.7 verifier-agent (F-03). Catches the easy lies a Final Report makes
367
- // against the structured artifacts recorded during the session.
368
- program
369
- .command('verify')
370
- .description("Verify a session's Final Report against the recorded reality (catches optimistic claims)")
371
- .argument('[session-id]', 'Session id (or short prefix) to verify. Omit to list recent sessions.')
372
- .option('--json', 'Output machine-readable JSON instead of formatted findings')
373
- .addHelpText('after', `
374
- Examples:
375
- $ codehere verify # list recent sessions to pick from
376
- $ codehere verify a1b2c3d4 # verify by short id prefix
377
- $ codehere verify a1b2c3d4 --json # machine-readable verdict for CI
378
-
379
- Exit codes:
380
- 0 = verified, partial (warnings only), or no Final Report to verify
381
- 1 = broken (a claim contradicts recorded reality)
382
- 2 = session not found
383
- `)
384
- .action(async (sessionId, options) => {
385
- try {
386
- // Per codex Round 2: set exitCode instead of calling process.exit so a
387
- // piped --json consumer sees the full payload before Node tears down.
388
- // The picker mode can write ~15 sessions worth of JSON · enough to
389
- // exceed pipe buffer + race the process exit. exitCode preserves the
390
- // intent (CI uses the exit code) without forcing an immediate teardown.
391
- process.exitCode = await executeVerifyCommand(sessionId, { json: options.json });
392
- }
393
- catch (err) {
394
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
395
- process.exitCode = 1;
396
- }
397
- });
398
- // ── audit ────────────────────────────────────────────────────────────────────
399
- // F-05 of the demo v12 backlog · expose the hash-chain verifier as a CLI.
400
103
  program
401
- .command('audit')
402
- .description('Inspect the hash-chained audit log · verify the chain or show one entry')
403
- .argument('[subcommand]', 'verify | show', 'verify')
404
- .argument('[arg]', 'For `show`: the sequence number to print.')
405
- .option('--json', 'Output machine-readable JSON instead of formatted text')
406
- .addHelpText('after', `
407
- Examples:
408
- $ codehere audit # equivalent to: codehere audit verify
409
- $ codehere audit verify # walk the chain, report intact/tampered
410
- $ codehere audit verify --json # machine-readable verdict for CI
411
- $ codehere audit show 42 # print entry with seq=42
412
- $ codehere audit show 42 --json
413
-
414
- Exit codes:
415
- 0 = chain intact OR entry found
416
- 1 = chain BROKEN (one or more break points detected)
417
- 2 = invalid input / no entry found
418
- `)
419
- .action((sub, arg, options) => {
420
- try {
421
- process.exitCode = executeAuditCommand(sub, arg, { json: options.json });
422
- }
423
- catch (err) {
424
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
425
- process.exitCode = 1;
426
- }
427
- });
428
- // ── context-source ───────────────────────────────────────────────────────────
429
- // v0.8 S2.5 · OAuth click-to-connect for Linear, Figma, etc.
430
- program
431
- .command('context-source')
432
- .alias('cs')
433
- .description('Manage context sources (Linear, Figma) · connect via OAuth, list status, disconnect')
434
- .argument('[subcommand]', 'list | connect | disconnect', 'list')
435
- .argument('[provider]', 'For connect/disconnect: linear | figma | ...')
436
- .option('--json', 'Machine-readable output')
437
- .option('--no-open', 'For connect: skip auto-opening the browser (just print the URL)')
438
- .addHelpText('after', `
439
- Examples:
440
- $ codehere context-source # equivalent to: list
441
- $ codehere context-source list
442
- $ codehere context-source connect linear
443
- $ codehere context-source connect figma --no-open
444
- $ codehere context-source disconnect linear
445
-
446
- Tokens persist at ~/.codehere/context-sources.json (0600 mode).
447
- Requires the daemon running: codehere start
448
- Provider client IDs come from env: CODEHERE_LINEAR_CLIENT_ID, CODEHERE_FIGMA_CLIENT_ID
449
- `)
450
- .action(async (sub, provider, options) => {
451
- try {
452
- process.exitCode = await executeContextSourceCommand(sub, provider, {
453
- json: options.json,
454
- open: options.open,
455
- });
456
- }
457
- catch (err) {
458
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
459
- process.exitCode = 1;
460
- }
461
- });
462
- // ── thread ───────────────────────────────────────────────────────────────────
463
- // v0.8 S4 · signed-payload "GitHub for agent sessions" · share/import threads
464
- program
465
- .command('thread')
466
- .description('Share or import a session · Ed25519-signed bundle · local-first')
467
- .argument('<subcommand>', 'export | import | keys | trust | untrust')
468
- .argument('[arg]', 'For export: session-id · For import/trust/untrust: file path or pubkey')
469
- .argument('[label]', 'For trust: an optional human label for the key')
470
- .option('--out <file>', 'For export: write the signed bundle to a file (default: stdout)')
471
- .option('--trust', 'For import: trust the signer key automatically if unknown')
472
- .option('--json', 'Machine-readable output')
473
- .addHelpText('after', `
474
- Examples:
475
- $ codehere thread export a1b2c3d4 --out demo-thread.json
476
- $ codehere thread import ./shared.json --trust
477
- $ codehere thread keys
478
- $ codehere thread trust <pubkey-base64> "Daniel"
479
- $ codehere thread untrust <pubkey-base64>
480
-
481
- Bundles include: spawned · steps · artifacts · finalReport · signature.
482
- No source code · only the synthesized record.
483
- Recipient must trust the signer key (or pass --trust) before replay.
484
- `)
485
- .action((sub, arg, label, options) => {
486
- try {
487
- process.exitCode = executeThreadCommand(sub, arg, label, {
488
- out: options.out,
489
- trust: options.trust,
490
- label,
491
- json: options.json,
492
- });
493
- }
494
- catch (err) {
495
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
496
- process.exitCode = 1;
497
- }
498
- });
499
- // ── hook ─────────────────────────────────────────────────────────────────────
500
- program
501
- .command('hook')
502
- .description('Manage Claude Agent SDK hook integration (intercepts all tool calls)')
503
- .argument('[subcommand]', 'install | uninstall | status | log')
504
- .option('--log-only', 'Log events but never block (audit mode)')
505
- .option('--block-mode <level>', 'Block threshold: critical (default), high, all')
506
- .option('--tail <n>', 'Lines to show in log mode', '20')
507
- .option('--debug', 'Verbose debug output')
508
- .action(async (subcommand, options) => {
509
- try {
510
- await executeHookCommand(subcommand, {
511
- logOnly: options?.logOnly,
512
- blockMode: options?.blockMode,
513
- tail: options?.tail ? parseInt(options.tail, 10) : undefined,
514
- debug: options?.debug,
515
- });
516
- }
517
- catch (err) {
518
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
519
- process.exit(1);
520
- }
521
- });
522
- // ── permission (P18 — per-cwd allow/deny list for the trust-scorer) ─────────
523
- program
524
- .command('permission')
525
- .description("Manage this folder's .codehere/permissions.yaml allow/deny list")
526
- .argument('[subcommand]', 'list | allow | deny | remove')
527
- .argument('[pattern]', 'the substring pattern (for allow/deny/remove)')
528
- .option('--reason <text>', 'Why this pattern is allow-listed (recorded in audit)')
529
- .action(async (subcommand, pattern, options) => {
530
- try {
531
- await executePermissionCommand(subcommand, pattern, { reason: options?.reason });
532
- }
533
- catch (err) {
534
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
535
- process.exit(1);
536
- }
537
- });
538
- // ── narrator (S6 — editable PM narrator prompt) ──────────────────────────────
539
- program
540
- .command('narrator')
541
- .description('Customize the PM narrator prompt template (~/.codehere/narrator-prompt.md)')
542
- .argument('[subcommand]', 'show | edit | reset')
543
- .action(async (subcommand) => {
544
- try {
545
- await executeNarratorCommand(subcommand);
546
- }
547
- catch (err) {
548
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
549
- process.exit(1);
550
- }
551
- });
552
- // ── budget (S3 — daily-spend ceiling enforcement) ────────────────────────────
553
- program
554
- .command('budget')
555
- .description('Set or check the daily spend budget for spawns')
556
- .argument('[subcommand]', 'show | set | off')
557
- .argument('[value]', 'amount in USD (for `set`)')
558
- .action(async (subcommand, value) => {
559
- try {
560
- await executeBudgetCommand(subcommand, value);
561
- }
562
- catch (err) {
563
- console.error(error('Error:'), err instanceof Error ? err.message : String(err));
564
- process.exit(1);
565
- }
566
- });
567
- // ── serve (MCP server) ───────────────────────────────────────────────────────
568
- program
569
- .command('serve')
570
- .description('Run codehere as a background MCP server (no web UI) — for Claude Desktop / Cursor integration')
571
- .option('-p, --port <number>', 'Port to listen on', '3773')
572
- .option('--host <host>', 'Host to bind to (default: 127.0.0.1)', '127.0.0.1')
573
- .option('--token <token>', 'Require Bearer token for authentication')
574
- .option('--show-config', 'Print the MCP config block to add to Claude Desktop / Cursor settings')
575
- .action(async (options) => {
576
- const port = parseInt(options?.port ?? '3773', 10);
577
- const host = options?.host ?? '127.0.0.1';
578
- const token = options?.token;
579
- if (options?.showConfig) {
580
- const configBlock = {
581
- mcpServers: {
582
- codehere: {
583
- url: `http://${host}:${port}/mcp`,
584
- ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
585
- },
586
- },
587
- };
588
- console.log('\nAdd to Claude Desktop or Cursor MCP settings:\n');
589
- console.log(JSON.stringify(configBlock, null, 2));
590
- newline();
591
- console.log(colors.dim('Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json'));
592
- console.log(colors.dim('Cursor: Settings > MCP Servers'));
593
- newline();
594
- return;
595
- }
596
- try {
597
- const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
598
- const server = startMCPServer({ port, host, token });
599
- newline();
600
- console.log(success(`codehere MCP server running`));
601
- newline();
602
- console.log(colors.bold('Endpoint:') + ` http://${host}:${port}/mcp`);
603
- console.log(colors.bold('Health:') + ` http://${host}:${port}/health`);
604
- newline();
605
- console.log(colors.bold('Tools available:'));
606
- console.log(' codehere_trust — score a file or snippet (0-100, no API)');
607
- console.log(' codehere_scan — scan text for injection/threats');
608
- console.log(' codehere_review — scan a directory, return riskiest files');
609
- console.log(' codehere_audit — read hook audit log');
610
- newline();
611
- console.log(colors.dim(' Run with --show-config to get the MCP client config block'));
612
- console.log(colors.dim(' Press Ctrl+C to stop'));
613
- newline();
614
- process.on('SIGINT', () => {
615
- server.close();
616
- console.log(colors.dim('\ncodehere MCP server stopped'));
617
- process.exit(0);
618
- });
619
- process.on('SIGTERM', () => process.exit(0));
620
- await new Promise((resolve) => {
621
- server.on('close', resolve);
622
- });
623
- }
624
- catch (err) {
625
- console.error(error('Failed to start server:'), err instanceof Error ? err.message : String(err));
626
- process.exit(1);
627
- }
628
- });
629
- // ── start (Mission Control: daemon + web UI) ─────────────────────────────────
630
- program
631
- .command('start')
632
- .description('Start codehere — interactive PM dashboard at localhost:3773 (delegate tasks, watch agents work)')
633
- .option('-p, --port <number>', 'Port to listen on', '3773')
634
- .option('--host <host>', 'Host to bind to', '127.0.0.1')
635
- .option('--no-open', "Don't auto-open the browser")
636
- .option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
637
- .action(async (options) => {
638
- const port = parseInt(options?.port ?? '3773', 10);
639
- const host = options?.host ?? '127.0.0.1';
640
- const shouldOpen = options?.open !== false;
641
- const cloud = options?.cloud === true;
642
- const url = `http://${host}:${port}/`;
643
- const openBrowser = async () => {
644
- if (!shouldOpen)
645
- return;
646
- const { spawn } = await import('child_process');
647
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
648
- try {
649
- spawn(opener, [url], { detached: true, stdio: 'ignore' }).unref();
650
- }
651
- catch {
652
- /* browser open is best-effort */
653
- }
654
- };
655
- try {
656
- // v0.6.0 M1+M7 — migrate sessions.jsonl → ~/.codehere/projects/<slug>/thread.jsonl
657
- // on daemon start. Idempotent: skips projects whose thread.jsonl already exists,
658
- // so this is safe to run on every boot. Opt-out via CODEHERE_DISABLE_THREAD_MIGRATION=1.
659
- // Failures are non-blocking — back-compat to v0.5 sessions.jsonl is preserved.
660
- if (process.env.CODEHERE_DISABLE_THREAD_MIGRATION !== '1') {
661
- try {
662
- const { migrate } = await import('./infrastructure/storage/project-thread-migration.js');
663
- const result = migrate({
664
- sourceJsonl: join(process.env.CODEHERE_HOME || homedir(), '.codehere/sessions.jsonl'),
665
- targetDir: join(process.env.CODEHERE_HOME || homedir(), '.codehere/projects'),
666
- });
667
- if (result.threadsWritten > 0) {
668
- console.log(colors.dim(` v0.6 migration: created ${result.threadsWritten} project thread(s) from ${result.sessionsProcessed} sessions (${result.durationMs}ms)`));
669
- }
670
- }
671
- catch (err) {
672
- console.warn(colors.dim(` v0.6 migration skipped: ${err instanceof Error ? err.message : String(err)}`));
673
- }
674
- }
675
- // v0.6.0 follow-up — backfill missing titles for projects that the
676
- // M9 Title agent never fired on (migration-seeded ones). Runs in the
677
- // background so daemon startup isn't blocked by ~5 LLM calls.
678
- // Idempotent + best-effort. Same env-var gate as M9 itself.
679
- if (process.env.CODEHERE_DISABLE_TITLE_AGENT !== '1') {
680
- void (async () => {
681
- try {
682
- const { backfillThreadTitles } = await import('./infrastructure/storage/project-thread-title-backfill.js');
683
- const r = await backfillThreadTitles();
684
- if (r.titlesGenerated > 0 && process.env.CODEHERE_DEBUG === '1') {
685
- console.log(colors.dim(` v0.6 title backfill: titled ${r.titlesGenerated}/${r.scanned} project(s) in ${r.durationMs}ms`));
686
- }
687
- }
688
- catch {
689
- /* best-effort — silent on failure */
690
- }
691
- })();
692
- }
693
- const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
694
- const server = startMCPServer({ port, host });
695
- // Wait for the server to either start listening or fail. The default
696
- // Node behavior on listen() failure is an unhandled 'error' event which
697
- // crashes the process with a stack trace. We catch it here so the user
698
- // sees a friendly message instead.
699
- await new Promise((resolve, reject) => {
700
- server.once('listening', () => resolve());
701
- server.once('error', reject);
702
- });
703
- newline();
704
- console.log(success('codehere is running'));
705
- newline();
706
- console.log(colors.bold('Tasks:') + ' ' + url);
707
- console.log(colors.bold('History:') + ' ' + `http://${host}:${port}/history`);
708
- console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
709
- if (cloud) {
710
- console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
711
- }
712
- newline();
713
- console.log(colors.dim(' Press Ctrl+C to stop'));
714
- newline();
715
- // Start cloud sync if enabled
716
- if (cloud) {
717
- const { startCloudSync } = await import('./infrastructure/protocols/cloud-sync.js');
718
- startCloudSync();
719
- }
720
- await openBrowser();
721
- const shutdown = () => {
722
- try {
723
- const { SessionManager } = require('./infrastructure/orchestration/session-manager.js');
724
- SessionManager.getInstance().shutdown();
725
- }
726
- catch {
727
- /* graceful degradation — daemon may not have started fully */
728
- }
729
- if (cloud) {
730
- try {
731
- const { stopCloudSync } = require('./infrastructure/protocols/cloud-sync.js');
732
- stopCloudSync();
733
- }
734
- catch {
735
- /* cloud sync may not be loaded */
736
- }
737
- }
738
- server.close();
739
- };
740
- process.on('SIGINT', () => {
741
- shutdown();
742
- console.log(colors.dim('\ncodehere stopped'));
743
- process.exit(0);
744
- });
745
- process.on('SIGTERM', () => {
746
- shutdown();
747
- process.exit(0);
748
- });
749
- await new Promise((resolve) => {
750
- server.on('close', resolve);
751
- });
752
- }
753
- catch (err) {
754
- const code = err?.code;
755
- if (code === 'EADDRINUSE') {
756
- // Port is taken. Probe to find out by what — if it's a running codehere,
757
- // we should hand the user the existing URL; if it's something else,
758
- // tell them to pick a different port.
759
- const { isCodehereOnPort } = await import('./infrastructure/protocols/probe-port.js');
760
- const existing = await isCodehereOnPort(host, port);
761
- newline();
762
- if (existing.isCodehere) {
763
- console.log(success(`codehere is already running at ${url}`));
764
- if (existing.version) {
765
- console.log(colors.dim(` version: ${existing.version}`));
766
- }
767
- newline();
768
- // Honor --no-open here too — found via the v1 stranger walk: a second
769
- // `start --no-open` against a running daemon popped a browser anyway.
770
- if (shouldOpen) {
771
- console.log(colors.dim(' Opening browser...'));
772
- await openBrowser();
773
- }
774
- process.exit(0);
775
- }
776
- console.error(error(`Port ${port} is in use by another process.`));
777
- console.error(colors.dim(` Try: codehere start --port ${port + 1}`));
778
- process.exit(1);
779
- }
780
- console.error(error('Failed to start:'), err instanceof Error ? err.message : String(err));
781
- process.exit(1);
782
- }
783
- });
784
- // ── task (spawn an agent session via CLI) ────────────────────────────────────
785
- program
786
- .command('task <prompt...>')
787
- .description('Assign a task — spawns an agent under codehere. Requires `codehere start` running.')
788
- .option('-p, --port <number>', 'Daemon port', '3773')
789
- .option('--host <host>', 'Daemon host', '127.0.0.1')
790
- .option('--agent <id>', 'Which agent to run (claude-code, codex, aider, gemini, opencode). Default: auto.')
791
- .option('--watch', 'Watch it work and print the report when it finishes')
792
- .addHelpText('after', `
793
- Examples:
794
- $ codehere task "fix the totalValue bug in inventory.js" # assign, get a session id
795
- $ codehere task "add a health check route" --watch # watch it work + see the report
796
- $ codehere task "summarize recent changes" --agent codex --watch # pick the agent
797
-
798
- Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`).`)
799
- .action(async (promptParts, options) => {
800
- const prompt = promptParts.join(' ').trim();
801
- if (!prompt) {
802
- console.error(error('No prompt. Usage: codehere task "fix auth bug in session.ts"'));
803
- process.exit(1);
804
- }
805
- const port = parseInt(options?.port ?? '3773', 10);
806
- const host = options?.host ?? '127.0.0.1';
807
- const base = `http://${host}:${port}`;
808
- const http = await import('http');
809
- const { waitForDeliverable, formatDeliverable } = await import('./presentation/cli/task-deliverable.js');
810
- const spawnResult = await new Promise((resolve) => {
811
- const body = JSON.stringify({ prompt, ...(options?.agent ? { agent: options.agent } : {}) });
812
- const r = http.request(`${base}/api/task`, {
813
- method: 'POST',
814
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
815
- }, (res) => {
816
- let buf = '';
817
- res.on('data', (c) => {
818
- buf += c.toString();
819
- });
820
- res.on('end', () => {
821
- try {
822
- resolve(JSON.parse(buf));
823
- }
824
- catch {
825
- resolve(null);
826
- }
827
- });
828
- });
829
- r.on('error', () => resolve(null));
830
- r.write(body);
831
- r.end();
832
- });
833
- if (!spawnResult || !spawnResult.sessionId) {
834
- if (spawnResult?.error)
835
- console.error(error(spawnResult.error));
836
- else
837
- console.error(error(`Could not reach the daemon at ${base}. Start it with: codehere start`));
838
- process.exit(1);
839
- }
840
- const sid = spawnResult.sessionId;
841
- console.log(success(`Task assigned · session ${sid.slice(0, 8)}`));
842
- if (!options?.watch) {
843
- console.log(colors.dim(` Watch in the browser: ${base}/`));
844
- console.log(colors.dim(` Or in the terminal: codehere task "${prompt}" --watch`));
845
- console.log(colors.dim(` Stop it: codehere kill ${sid.slice(0, 8)}`));
846
- return;
847
- }
848
- // --watch · render the agent's actual steps as they complete, then print
849
- // the deliverable (report + files + trust + verifier) when it's done.
850
- console.log(colors.dim(` Watching… (Ctrl-C to stop watching; the agent keeps running)`));
851
- const printed = new Set();
852
- await new Promise((resolve) => {
853
- const r = http.get(`${base}/api/task/${sid}/events`, (res) => {
854
- let buf = '';
855
- res.on('data', (c) => {
856
- buf += c.toString();
857
- let i;
858
- while ((i = buf.indexOf('\n\n')) >= 0) {
859
- const frame = buf.slice(0, i).trim();
860
- buf = buf.slice(i + 2);
861
- if (!frame.startsWith('data:'))
862
- continue;
863
- try {
864
- const event = JSON.parse(frame.slice(5).trim());
865
- const type = String(event.type || 'message');
866
- if (type === 'codehere:steps') {
867
- const steps = (event.data?.steps ?? []);
868
- for (const s of steps) {
869
- if (s.state !== 'done' && s.state !== 'failed')
870
- continue;
871
- if (printed.has(s.label))
872
- continue; // one line per step, on first settle
873
- printed.add(s.label);
874
- const mark = s.state === 'failed' ? colors.error('✗') : colors.success('✓');
875
- const text = s.kind === 'say' ? colors.dim(s.label) : s.label;
876
- console.log(` ${mark} ${text}`);
877
- }
878
- }
879
- else if (type === 'codehere:error') {
880
- const d = event.data;
881
- if (d?.message)
882
- console.log(` ${colors.error('!')} ${d.message}`);
883
- if (d?.recovery)
884
- console.log(colors.dim(` ${d.recovery}`));
885
- }
886
- else if (type === 'exit') {
887
- res.destroy();
888
- resolve();
889
- return;
890
- }
891
- }
892
- catch {
893
- /* skip malformed frame */
894
- }
895
- }
896
- });
897
- res.on('end', () => resolve());
898
- });
899
- r.on('error', () => resolve());
900
- });
901
- // The Final Report is generated after exit — poll for it, then print.
902
- // Real agents take ~30-60s to write it, so tell the user we're waiting.
903
- process.stdout.write(colors.dim(' Writing the report…'));
904
- const result = await waitForDeliverable(base, sid);
905
- process.stdout.write('\r' + ' '.repeat(24) + '\r'); // clear the waiting line
906
- if (result && (result.finalReport || result.failedReport || result.status !== 'completed')) {
907
- console.log(formatDeliverable(result));
908
- }
909
- else if (result) {
910
- // Completed, but the report is still generating past our wait window.
911
- console.log(formatDeliverable(result));
912
- console.log(colors.dim(' (the written report is still being generated — run the command below in a moment)'));
913
- }
914
- else {
915
- console.log(colors.dim('\n Task finished, but the report could not be fetched from the daemon.'));
916
- }
917
- console.log(colors.dim(`Next: codehere verify ${sid.slice(0, 8)} (see this report + the fact-check anytime)`));
918
- });
919
- // ── kill (stop a running task) ───────────────────────────────────────────────
920
- program
921
- .command('kill <sessionId>')
922
- .description('Kill a running task session.')
923
- .option('-p, --port <number>', 'Daemon port', '3773')
924
- .option('--host <host>', 'Daemon host', '127.0.0.1')
925
- .action(async (sessionId, options) => {
926
- const port = parseInt(options?.port ?? '3773', 10);
927
- const host = options?.host ?? '127.0.0.1';
928
- const http = await import('http');
929
- await new Promise((resolve) => {
930
- const r = http.request(`http://${host}:${port}/api/task/${sessionId}/kill`, { method: 'POST' }, (res) => {
931
- let buf = '';
932
- res.on('data', (c) => {
933
- buf += c.toString();
934
- });
935
- res.on('end', () => {
936
- if (res.statusCode === 200)
937
- console.log(success(`Killed ${sessionId}`));
938
- else
939
- console.error(error(`Failed (${res.statusCode}): ${buf}`));
940
- resolve();
941
- });
942
- });
943
- r.on('error', (err) => {
944
- console.error(error(err.message));
945
- resolve();
946
- });
947
- r.end();
948
- });
949
- });
950
- // ── migrate-threads — v0.6.0 schema migration sessions.jsonl → projects/<slug>/thread.jsonl
951
- program
952
- .command('migrate-threads', { hidden: true }) // hidden from --help (v0.6.0 internal — auto-runs on daemon start once wired)
953
- .description('Migrate sessions.jsonl into per-project thread.jsonl files (v0.6.0 schema)')
954
- .option('--source <path>', 'Source sessions.jsonl', `${process.env.CODEHERE_HOME || homedir()}/.codehere/sessions.jsonl`)
955
- .option('--target <dir>', 'Target projects dir', `${process.env.CODEHERE_HOME || homedir()}/.codehere/projects`)
956
- .option('--dry-run', 'Report what migrate WOULD do, write nothing', false)
957
- .option('--rollback', 'Remove the projects dir; sessions.jsonl untouched', false)
958
- .option('--keep-temp-dirs', 'Include /tmp + /var/folders cwds (default skips them)', false)
959
- .action(async (options) => {
960
- const { migrate, dryRun, rollback } = await import('./infrastructure/storage/project-thread-migration.js');
961
- if (options.rollback) {
962
- const r = rollback(options.target);
963
- console.log(`Rollback: removed ${r.removedDirs} project dir(s) from ${options.target}`);
964
- console.log(`Source ${options.source} untouched.`);
965
- return;
966
- }
967
- const fn = options.dryRun ? dryRun : migrate;
968
- const label = options.dryRun ? 'DRY RUN' : 'MIGRATION';
969
- const result = fn({
970
- sourceJsonl: options.source,
971
- targetDir: options.target,
972
- skipTempDirs: !options.keepTempDirs,
973
- });
974
- console.log(`=== ${label} ===`);
975
- console.log(` source: ${options.source}`);
976
- console.log(` target: ${options.target}`);
977
- console.log(` sessions processed: ${result.sessionsProcessed}`);
978
- console.log(` threads ${options.dryRun ? 'would write' : 'written'}: ${result.threadsWritten}`);
979
- console.log(` threads skipped (exist): ${result.threadsSkippedExisting}`);
980
- console.log(` unknown-cwd sessions: ${result.unknownCwdSessions}`);
981
- console.log(` abandoned sessions: ${result.abandonedSessions}`);
982
- console.log(` temp-dir sessions skipped: ${result.tempDirSessionsSkipped}`);
983
- console.log(` duration: ${result.durationMs}ms`);
984
- });
985
- // ── scan-tool (called by Claude Code hooks system, not users directly) ───────
986
- program
987
- .command('scan-tool', { hidden: true }) // hidden from --help (May 2 cut: internal hook handler, called by Claude Code via stdin)
988
- .description('Hook handler: reads Claude Code tool event from stdin, outputs block/allow (used by hook system)')
989
- .option('--log-only', 'Never block, log only')
990
- .option('--block-all', 'Block on any finding, not just critical')
991
- .option('--debug', 'Debug output')
992
- .action(async (options) => {
993
- await executeScanToolCommand({
994
- logOnly: options?.logOnly,
995
- blockAll: options?.blockAll,
996
- debug: options?.debug,
997
- });
998
- });
999
- // ── completion (shell completion script generator) ──────────────────────────
1000
- program
1001
- .command('completion', { hidden: true }) // hidden from --help (May 2 cut: shell-completion is npm convention, not product surface)
1002
- .description('Generate shell completion script')
1003
- .argument('<shell>', 'Shell type: bash, zsh, or fish')
1004
- .action(async (shell) => {
1005
- try {
1006
- switch (shell.toLowerCase()) {
1007
- case 'bash': {
1008
- const { generateBashCompletion } = await import('./infrastructure/completion/bash-completion.js');
1009
- console.log(generateBashCompletion());
1010
- break;
1011
- }
1012
- case 'zsh': {
1013
- const { generateZshCompletion } = await import('./infrastructure/completion/zsh-completion.js');
1014
- console.log(generateZshCompletion());
1015
- break;
1016
- }
1017
- case 'fish': {
1018
- const { generateFishCompletion } = await import('./infrastructure/completion/fish-completion.js');
1019
- console.log(generateFishCompletion());
1020
- break;
1021
- }
1022
- default:
1023
- console.error(error(`Unsupported shell: ${shell}`));
1024
- console.log(info('Supported shells: bash, zsh, fish'));
1025
- console.log(info('Usage: codehere completion <bash|zsh|fish>'));
1026
- process.exit(1);
1027
- }
1028
- }
1029
- catch (err) {
1030
- displayError(err, { operation: 'completion', showSuggestions: true });
1031
- process.exit(1);
1032
- }
104
+ .command('help')
105
+ .description('Show help for commands')
106
+ .argument('[command]', 'Command name to get help for')
107
+ .action(async (commandName) => {
108
+ await executeHelpCommand(commandName);
1033
109
  });
1034
- // ── slash-command routing: `codehere /trust .` → `codehere trust .` ─────────
1035
- const rawArgs = process.argv.slice(2);
1036
- if (rawArgs.length > 0 && rawArgs[0].startsWith('/') && rawArgs[0].length > 1) {
1037
- const stripped = rawArgs[0].slice(1);
1038
- process.argv.splice(2, 1, stripped);
1039
- }
1040
110
  program.parse();
1041
111
  //# sourceMappingURL=index.js.map