codehere 0.7.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
package/dist/index.js
CHANGED
|
@@ -15,15 +15,67 @@ import { config } from 'dotenv';
|
|
|
15
15
|
import { join, dirname } from 'path';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
17
|
import { homedir } from 'os';
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
18
19
|
// ── Global crash safety ───────────────────────────────────────────────────
|
|
19
|
-
//
|
|
20
|
-
process.
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
34
|
+
function safeVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
return JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf-8')).version ?? 'unknown';
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function crashReport(kind, err) {
|
|
44
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
45
|
+
return [
|
|
46
|
+
'',
|
|
47
|
+
`[codehere] ${kind} — this process is stopping.`,
|
|
48
|
+
` version : ${safeVersion()}`,
|
|
49
|
+
` node : ${process.version} on ${process.platform} ${process.arch}`,
|
|
50
|
+
` state : ${process.env.CODEHERE_HOME ? '$CODEHERE_HOME' : '~/.codehere'}`,
|
|
51
|
+
` pid : ${process.pid}`,
|
|
52
|
+
'',
|
|
53
|
+
(e.stack ?? e.message)
|
|
54
|
+
.split('\n')
|
|
55
|
+
.map((l) => ` ${l}`)
|
|
56
|
+
.join('\n'),
|
|
57
|
+
'',
|
|
58
|
+
' Report at https://github.com/muhammadegaa/codehere/issues with the block above.',
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
23
62
|
process.on('uncaughtException', (err) => {
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
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
|
+
console.error(crashReport('uncaughtException', err));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
process.on('unhandledRejection', (reason) => {
|
|
70
|
+
// Deliberately NOT fatal, unlike the above. The codebase has 17 explicitly
|
|
71
|
+
// `void`-marked fire-and-forget promises (re-persist, enrichment) whose
|
|
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.'));
|
|
27
79
|
});
|
|
28
80
|
import { executeSetupCommand } from './presentation/cli/commands/setup-command.js';
|
|
29
81
|
import { executeInitCommand } from './presentation/cli/commands/init-command.js';
|
|
@@ -35,11 +87,15 @@ import { executeStatusCommand } from './presentation/cli/commands/status-command
|
|
|
35
87
|
import { executeHookCommand } from './presentation/cli/commands/hook-command.js';
|
|
36
88
|
import { executePermissionCommand } from './presentation/cli/commands/permission-command.js';
|
|
37
89
|
import { executeBudgetCommand } from './presentation/cli/commands/budget-command.js';
|
|
90
|
+
import { executeTeamCommand } from './presentation/cli/commands/team-command.js';
|
|
38
91
|
import { executeNarratorCommand } from './presentation/cli/commands/narrator-command.js';
|
|
39
92
|
import { executeTrustCommand } from './presentation/cli/commands/trust-command.js';
|
|
93
|
+
import { executeCheckCommand } from './presentation/cli/commands/check-command.js';
|
|
94
|
+
import { executeScanCommand } from './presentation/cli/commands/scan-command.js';
|
|
40
95
|
import { executeReportCommand } from './presentation/cli/commands/report-command.js';
|
|
41
96
|
import { executeScanToolCommand } from './presentation/cli/commands/scan-tool-command.js';
|
|
42
97
|
import { executeVerifyCommand } from './presentation/cli/commands/verify-command.js';
|
|
98
|
+
import { executeSearchCommand } from './presentation/cli/commands/search-command.js';
|
|
43
99
|
import { executeAuditCommand } from './presentation/cli/commands/audit-command.js';
|
|
44
100
|
import { executeContextSourceCommand } from './presentation/cli/commands/context-source-command.js';
|
|
45
101
|
import { executeThreadCommand } from './presentation/cli/commands/thread-command.js';
|
|
@@ -62,7 +118,7 @@ import { checkTypo, looksLikeUnknownCommand } from './utils/typo-guard.js';
|
|
|
62
118
|
const program = new Command();
|
|
63
119
|
program
|
|
64
120
|
.name('codehere')
|
|
65
|
-
.description('
|
|
121
|
+
.description('Your AI coding agents: audited, cost-bounded, cross-vendor. Delegate a task, watch it run, and get a plain-English report with a trust score on every file it changed.')
|
|
66
122
|
.version(getVersion())
|
|
67
123
|
.configureHelp({ helpWidth: 80, sortSubcommands: true })
|
|
68
124
|
// rc.18: many users hit `codehere --help` first and get Commander's flat
|
|
@@ -272,7 +328,6 @@ program
|
|
|
272
328
|
.command('status')
|
|
273
329
|
.description('Security dashboard — hook activity, blocked calls, cost routing')
|
|
274
330
|
.option('--json', 'Machine-readable JSON')
|
|
275
|
-
.option('--tasks', 'Show recent tasks (experimental)')
|
|
276
331
|
.option('--sessions', 'Show per-session audit trail (multi-agent analysis)')
|
|
277
332
|
.option('--session <id>', 'Drill into a specific session (first 8+ chars of session ID)')
|
|
278
333
|
.option('--trust', 'Include a quick trust scan of the current directory')
|
|
@@ -330,6 +385,37 @@ program
|
|
|
330
385
|
process.exit(1);
|
|
331
386
|
}
|
|
332
387
|
});
|
|
388
|
+
// ── check ────────────────────────────────────────────────────────────────────
|
|
389
|
+
program
|
|
390
|
+
.command('check')
|
|
391
|
+
.description('Re-run your tests independently — proof of what the AI actually did, not what it claimed')
|
|
392
|
+
.argument('[target]', 'Repo or directory to check (default: current directory)')
|
|
393
|
+
.option('--json', 'Machine-readable output')
|
|
394
|
+
.option('--proof', 'Emit a paste-ready proof block for a PR description or review comment')
|
|
395
|
+
.action(async (target, options) => {
|
|
396
|
+
try {
|
|
397
|
+
process.exitCode = await executeCheckCommand(target, { json: options?.json, proof: options?.proof });
|
|
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
|
+
});
|
|
333
419
|
// ── report ───────────────────────────────────────────────────────────────────
|
|
334
420
|
program
|
|
335
421
|
.command('report')
|
|
@@ -395,6 +481,21 @@ Exit codes:
|
|
|
395
481
|
process.exitCode = 1;
|
|
396
482
|
}
|
|
397
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
|
+
});
|
|
398
499
|
// ── audit ────────────────────────────────────────────────────────────────────
|
|
399
500
|
// F-05 of the demo v12 backlog · expose the hash-chain verifier as a CLI.
|
|
400
501
|
program
|
|
@@ -564,6 +665,22 @@ program
|
|
|
564
665
|
process.exit(1);
|
|
565
666
|
}
|
|
566
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
|
+
});
|
|
567
684
|
// ── serve (MCP server) ───────────────────────────────────────────────────────
|
|
568
685
|
program
|
|
569
686
|
.command('serve')
|
|
@@ -577,25 +694,42 @@ program
|
|
|
577
694
|
const host = options?.host ?? '127.0.0.1';
|
|
578
695
|
const token = options?.token;
|
|
579
696
|
if (options?.showConfig) {
|
|
697
|
+
const url = `http://${host}:${port}/mcp`;
|
|
698
|
+
const authHeader = token ? ` --header "Authorization: Bearer ${token}"` : '';
|
|
580
699
|
const configBlock = {
|
|
581
700
|
mcpServers: {
|
|
582
701
|
codehere: {
|
|
583
|
-
url
|
|
702
|
+
url,
|
|
584
703
|
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
|
|
585
704
|
},
|
|
586
705
|
},
|
|
587
706
|
};
|
|
588
|
-
|
|
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):'));
|
|
589
714
|
console.log(JSON.stringify(configBlock, null, 2));
|
|
590
715
|
newline();
|
|
591
|
-
console.log(colors.
|
|
592
|
-
|
|
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));
|
|
593
719
|
newline();
|
|
594
720
|
return;
|
|
595
721
|
}
|
|
596
722
|
try {
|
|
597
723
|
const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
|
|
598
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
|
+
});
|
|
599
733
|
newline();
|
|
600
734
|
console.log(success(`codehere MCP server running`));
|
|
601
735
|
newline();
|
|
@@ -607,6 +741,8 @@ program
|
|
|
607
741
|
console.log(' codehere_scan — scan text for injection/threats');
|
|
608
742
|
console.log(' codehere_review — scan a directory, return riskiest files');
|
|
609
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');
|
|
610
746
|
newline();
|
|
611
747
|
console.log(colors.dim(' Run with --show-config to get the MCP client config block'));
|
|
612
748
|
console.log(colors.dim(' Press Ctrl+C to stop'));
|
|
@@ -633,11 +769,18 @@ program
|
|
|
633
769
|
.option('-p, --port <number>', 'Port to listen on', '3773')
|
|
634
770
|
.option('--host <host>', 'Host to bind to', '127.0.0.1')
|
|
635
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)')
|
|
636
773
|
.option('--cloud', 'Enable cloud sync — push session state to codehere.uk/app for remote monitoring')
|
|
637
774
|
.action(async (options) => {
|
|
638
775
|
const port = parseInt(options?.port ?? '3773', 10);
|
|
639
776
|
const host = options?.host ?? '127.0.0.1';
|
|
640
|
-
const
|
|
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;
|
|
641
784
|
const cloud = options?.cloud === true;
|
|
642
785
|
const url = `http://${host}:${port}/`;
|
|
643
786
|
const openBrowser = async () => {
|
|
@@ -661,8 +804,8 @@ program
|
|
|
661
804
|
try {
|
|
662
805
|
const { migrate } = await import('./infrastructure/storage/project-thread-migration.js');
|
|
663
806
|
const result = migrate({
|
|
664
|
-
sourceJsonl: join(homedir(), '.codehere/sessions.jsonl'),
|
|
665
|
-
targetDir: join(homedir(), '.codehere/projects'),
|
|
807
|
+
sourceJsonl: join(process.env.CODEHERE_HOME || homedir(), '.codehere/sessions.jsonl'),
|
|
808
|
+
targetDir: join(process.env.CODEHERE_HOME || homedir(), '.codehere/projects'),
|
|
666
809
|
});
|
|
667
810
|
if (result.threadsWritten > 0) {
|
|
668
811
|
console.log(colors.dim(` v0.6 migration: created ${result.threadsWritten} project thread(s) from ${result.sessionsProcessed} sessions (${result.durationMs}ms)`));
|
|
@@ -690,8 +833,12 @@ program
|
|
|
690
833
|
}
|
|
691
834
|
})();
|
|
692
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
|
+
}
|
|
693
840
|
const { startMCPServer } = await import('./infrastructure/protocols/mcp-server-impl.js');
|
|
694
|
-
const server = startMCPServer({ port, host });
|
|
841
|
+
const server = startMCPServer({ port, host, token });
|
|
695
842
|
// Wait for the server to either start listening or fail. The default
|
|
696
843
|
// Node behavior on listen() failure is an unhandled 'error' event which
|
|
697
844
|
// crashes the process with a stack trace. We catch it here so the user
|
|
@@ -704,7 +851,7 @@ program
|
|
|
704
851
|
console.log(success('codehere is running'));
|
|
705
852
|
newline();
|
|
706
853
|
console.log(colors.bold('Tasks:') + ' ' + url);
|
|
707
|
-
console.log(colors.bold('
|
|
854
|
+
console.log(colors.bold('Verify:') + ' ' + `http://${host}:${port}/verify`);
|
|
708
855
|
console.log(colors.bold('MCP:') + ' ' + `http://${host}:${port}/mcp`);
|
|
709
856
|
if (cloud) {
|
|
710
857
|
console.log(colors.bold('Cloud:') + ' ' + 'https://codehere.uk/app');
|
|
@@ -765,8 +912,12 @@ program
|
|
|
765
912
|
console.log(colors.dim(` version: ${existing.version}`));
|
|
766
913
|
}
|
|
767
914
|
newline();
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
921
|
process.exit(0);
|
|
771
922
|
}
|
|
772
923
|
console.error(error(`Port ${port} is in use by another process.`));
|
|
@@ -777,6 +928,83 @@ program
|
|
|
777
928
|
process.exit(1);
|
|
778
929
|
}
|
|
779
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
|
+
});
|
|
780
1008
|
// ── task (spawn an agent session via CLI) ────────────────────────────────────
|
|
781
1009
|
program
|
|
782
1010
|
.command('task <prompt...>')
|
|
@@ -785,6 +1013,7 @@ program
|
|
|
785
1013
|
.option('--host <host>', 'Daemon host', '127.0.0.1')
|
|
786
1014
|
.option('--agent <id>', 'Which agent to run (claude-code, codex, aider, gemini, opencode). Default: auto.')
|
|
787
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)')
|
|
788
1017
|
.addHelpText('after', `
|
|
789
1018
|
Examples:
|
|
790
1019
|
$ codehere task "fix the totalValue bug in inventory.js" # assign, get a session id
|
|
@@ -802,9 +1031,13 @@ Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`
|
|
|
802
1031
|
const host = options?.host ?? '127.0.0.1';
|
|
803
1032
|
const base = `http://${host}:${port}`;
|
|
804
1033
|
const http = await import('http');
|
|
1034
|
+
const { resolve: resolvePath } = await import('path');
|
|
805
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());
|
|
806
1039
|
const spawnResult = await new Promise((resolve) => {
|
|
807
|
-
const body = JSON.stringify({ prompt, ...(options?.agent ? { agent: options.agent } : {}) });
|
|
1040
|
+
const body = JSON.stringify({ prompt, cwd, ...(options?.agent ? { agent: options.agent } : {}) });
|
|
808
1041
|
const r = http.request(`${base}/api/task`, {
|
|
809
1042
|
method: 'POST',
|
|
810
1043
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
@@ -867,7 +1100,7 @@ Needs a running daemon: start one with \`codehere start\` (or \`codehere serve\`
|
|
|
867
1100
|
if (printed.has(s.label))
|
|
868
1101
|
continue; // one line per step, on first settle
|
|
869
1102
|
printed.add(s.label);
|
|
870
|
-
const mark = s.state === 'failed' ? colors.error('
|
|
1103
|
+
const mark = s.state === 'failed' ? colors.error('✕') : colors.success('✓');
|
|
871
1104
|
const text = s.kind === 'say' ? colors.dim(s.label) : s.label;
|
|
872
1105
|
console.log(` ${mark} ${text}`);
|
|
873
1106
|
}
|
|
@@ -931,13 +1164,22 @@ program
|
|
|
931
1164
|
res.on('end', () => {
|
|
932
1165
|
if (res.statusCode === 200)
|
|
933
1166
|
console.log(success(`Killed ${sessionId}`));
|
|
934
|
-
else
|
|
935
|
-
|
|
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
|
+
}
|
|
936
1177
|
resolve();
|
|
937
1178
|
});
|
|
938
1179
|
});
|
|
939
|
-
r.on('error', (
|
|
940
|
-
console.error(error(
|
|
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;
|
|
941
1183
|
resolve();
|
|
942
1184
|
});
|
|
943
1185
|
r.end();
|
|
@@ -947,8 +1189,8 @@ program
|
|
|
947
1189
|
program
|
|
948
1190
|
.command('migrate-threads', { hidden: true }) // hidden from --help (v0.6.0 internal — auto-runs on daemon start once wired)
|
|
949
1191
|
.description('Migrate sessions.jsonl into per-project thread.jsonl files (v0.6.0 schema)')
|
|
950
|
-
.option('--source <path>', 'Source sessions.jsonl', `${homedir()}/.codehere/sessions.jsonl`)
|
|
951
|
-
.option('--target <dir>', 'Target projects dir', `${homedir()}/.codehere/projects`)
|
|
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`)
|
|
952
1194
|
.option('--dry-run', 'Report what migrate WOULD do, write nothing', false)
|
|
953
1195
|
.option('--rollback', 'Remove the projects dir; sessions.jsonl untouched', false)
|
|
954
1196
|
.option('--keep-temp-dirs', 'Include /tmp + /var/folders cwds (default skips them)', false)
|
|
@@ -999,20 +1241,26 @@ program
|
|
|
999
1241
|
.argument('<shell>', 'Shell type: bash, zsh, or fish')
|
|
1000
1242
|
.action(async (shell) => {
|
|
1001
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] }));
|
|
1002
1250
|
switch (shell.toLowerCase()) {
|
|
1003
1251
|
case 'bash': {
|
|
1004
1252
|
const { generateBashCompletion } = await import('./infrastructure/completion/bash-completion.js');
|
|
1005
|
-
console.log(generateBashCompletion());
|
|
1253
|
+
console.log(generateBashCompletion(cmds));
|
|
1006
1254
|
break;
|
|
1007
1255
|
}
|
|
1008
1256
|
case 'zsh': {
|
|
1009
1257
|
const { generateZshCompletion } = await import('./infrastructure/completion/zsh-completion.js');
|
|
1010
|
-
console.log(generateZshCompletion());
|
|
1258
|
+
console.log(generateZshCompletion(cmds));
|
|
1011
1259
|
break;
|
|
1012
1260
|
}
|
|
1013
1261
|
case 'fish': {
|
|
1014
1262
|
const { generateFishCompletion } = await import('./infrastructure/completion/fish-completion.js');
|
|
1015
|
-
console.log(generateFishCompletion());
|
|
1263
|
+
console.log(generateFishCompletion(cmds));
|
|
1016
1264
|
break;
|
|
1017
1265
|
}
|
|
1018
1266
|
default:
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bash Completion Script Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Completions are generated from the REAL registered command list passed by
|
|
5
|
+
* the caller — the alpha-era hardcoded set (`ask plan orchestrate …`) shipped
|
|
6
|
+
* completions for commands that never existed (caught by the 2026-06-07
|
|
7
|
+
* v1-readiness audit, Lane C).
|
|
5
8
|
*/
|
|
6
|
-
export function generateBashCompletion() {
|
|
9
|
+
export function generateBashCompletion(commands) {
|
|
10
|
+
const opts = commands.map((c) => c.name).join(' ');
|
|
7
11
|
return `# Codehere bash completion
|
|
8
12
|
_complete_codehere() {
|
|
9
13
|
local cur prev opts
|
|
10
14
|
COMPREPLY=()
|
|
11
15
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
12
16
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
13
|
-
opts="
|
|
17
|
+
opts="${opts}"
|
|
14
18
|
|
|
15
19
|
if [[ \${cur} == -* ]]; then
|
|
16
20
|
COMPREPLY=($(compgen -W "--help --version" -- \${cur}))
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fish Completion Script Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Completions are generated from the REAL registered command list passed by
|
|
5
|
+
* the caller (see bash-completion.ts header for the audit history).
|
|
5
6
|
*/
|
|
6
|
-
export function generateFishCompletion() {
|
|
7
|
+
export function generateFishCompletion(commands) {
|
|
8
|
+
const lines = commands
|
|
9
|
+
.map((c) => `complete -c codehere -n __fish_use_subcommand -a ${c.name} -d "${c.description.replace(/"/g, '')}"`)
|
|
10
|
+
.join('\n');
|
|
7
11
|
return `# Codehere fish completion
|
|
8
12
|
complete -c codehere -f
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
complete -c codehere -n __fish_use_subcommand -a plan -d "Generate execution plan"
|
|
12
|
-
complete -c codehere -n __fish_use_subcommand -a orchestrate -d "Multi-agent orchestration"
|
|
13
|
-
complete -c codehere -n __fish_use_subcommand -a fix -d "Apply AI edits to file"
|
|
14
|
-
complete -c codehere -n __fish_use_subcommand -a suggest -d "Read-only assistant mode"
|
|
15
|
-
complete -c codehere -n __fish_use_subcommand -a run -d "Unified agentic execution"
|
|
16
|
-
complete -c codehere -n __fish_use_subcommand -a review -d "Task review interface"
|
|
17
|
-
complete -c codehere -n __fish_use_subcommand -a status -d "Show agent status"
|
|
18
|
-
complete -c codehere -n __fish_use_subcommand -a help -d "Show help"
|
|
19
|
-
complete -c codehere -n __fish_use_subcommand -a version -d "Show version"
|
|
14
|
+
${lines}
|
|
20
15
|
`;
|
|
21
16
|
}
|
|
22
17
|
//# sourceMappingURL=fish-completion.js.map
|
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zsh Completion Script Generator
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Completions are generated from the REAL registered command list passed by
|
|
5
|
+
* the caller (see bash-completion.ts header for the audit history).
|
|
5
6
|
*/
|
|
6
|
-
export function generateZshCompletion() {
|
|
7
|
+
export function generateZshCompletion(commands) {
|
|
8
|
+
// zsh _describe entries are 'name:description' — strip colons/quotes from
|
|
9
|
+
// descriptions so they can't break the entry syntax.
|
|
10
|
+
const entries = commands.map((c) => ` '${c.name}:${c.description.replace(/['":]/g, '')}'`).join('\n');
|
|
7
11
|
return `#compdef codehere
|
|
8
12
|
|
|
9
13
|
_codehere() {
|
|
10
14
|
local -a commands
|
|
11
15
|
commands=(
|
|
12
|
-
|
|
13
|
-
'plan:Generate execution plan'
|
|
14
|
-
'orchestrate:Multi-agent orchestration'
|
|
15
|
-
'fix:Apply AI edits to file'
|
|
16
|
-
'suggest:Read-only assistant mode'
|
|
17
|
-
'run:Unified agentic execution'
|
|
18
|
-
'review:Task review interface'
|
|
19
|
-
'status:Show agent status'
|
|
20
|
-
'help:Show help'
|
|
21
|
-
'version:Show version'
|
|
16
|
+
${entries}
|
|
22
17
|
)
|
|
23
18
|
|
|
24
19
|
_describe 'codehere' commands
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
1
2
|
/**
|
|
2
3
|
* Configuration Manager
|
|
3
4
|
* Manages user preferences and settings for Codehere
|
|
4
5
|
* Stores configuration in data/config.json
|
|
5
6
|
*/
|
|
6
|
-
import { readFileSync,
|
|
7
|
+
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
7
8
|
import { join } from 'path';
|
|
8
9
|
import { getVersion } from '../../utils/version.js';
|
|
9
10
|
function detectDefaultProvider() {
|
|
10
11
|
if (process.env.ANTHROPIC_API_KEY)
|
|
11
|
-
return { provider: 'anthropic', model: 'claude-
|
|
12
|
+
return { provider: 'anthropic', model: 'claude-fable-5' };
|
|
12
13
|
if (process.env.OPENAI_API_KEY)
|
|
13
14
|
return { provider: 'openai', model: 'gpt-4o-mini' };
|
|
14
15
|
if (process.env.COHERE_API_KEY)
|
|
@@ -100,7 +101,7 @@ export class ConfigManager {
|
|
|
100
101
|
if (!existsSync(dataDir)) {
|
|
101
102
|
mkdirSync(dataDir, { recursive: true });
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
writeFileAtomic(this.configPath, JSON.stringify(this.config, null, 2));
|
|
104
105
|
}
|
|
105
106
|
catch (error) {
|
|
106
107
|
throw new Error(`Failed to save config to ${this.configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Stored in: .codehere/product.json (per-project, gitignore-friendly)
|
|
9
9
|
*/
|
|
10
|
-
import { readFileSync,
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
const CONFIG_DIR = '.codehere';
|
|
13
13
|
const CONFIG_FILE = 'product.json';
|
|
@@ -25,24 +25,6 @@ export function loadProductContext(cwd = process.cwd()) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
export function saveProductContext(ctx, cwd = process.cwd()) {
|
|
29
|
-
const dir = join(cwd, CONFIG_DIR);
|
|
30
|
-
if (!existsSync(dir))
|
|
31
|
-
mkdirSync(dir, { recursive: true });
|
|
32
|
-
writeFileSync(getConfigPath(cwd), JSON.stringify(ctx, null, 2) + '\n');
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Returns a plain-English risk label for use in recommendations.
|
|
36
|
-
*/
|
|
37
|
-
export function riskLabel(level) {
|
|
38
|
-
return level === 'critical'
|
|
39
|
-
? 'critical-risk'
|
|
40
|
-
: level === 'high'
|
|
41
|
-
? 'high-risk'
|
|
42
|
-
: level === 'medium'
|
|
43
|
-
? 'production'
|
|
44
|
-
: 'low-risk';
|
|
45
|
-
}
|
|
46
28
|
/**
|
|
47
29
|
* Adjusts a trust recommendation based on product context.
|
|
48
30
|
* If no context is available, returns the base recommendation unchanged.
|