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