codehere 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +36 -192
  2. package/dist/application/use-cases/check-claimed-tests.js +81 -0
  3. package/dist/application/use-cases/detect-test-weakening.js +237 -0
  4. package/dist/application/use-cases/rerun-tests.js +110 -7
  5. package/dist/index.js +38 -1212
  6. package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
  7. package/dist/presentation/cli/commands/check-command.js +178 -23
  8. package/dist/presentation/cli/commands/help-command.js +21 -285
  9. package/dist/ui.js +5 -236
  10. package/dist/utils/version.js +0 -6
  11. package/package.json +9 -38
  12. package/dist/application/services/verdict-service.js +0 -154
  13. package/dist/application/use-cases/assess-cross-vendor.js +0 -126
  14. package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
  15. package/dist/application/use-cases/domain-trust.js +0 -110
  16. package/dist/application/use-cases/export-aat.js +0 -107
  17. package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
  18. package/dist/application/use-cases/generate-final-report.js +0 -259
  19. package/dist/application/use-cases/generate-narration.js +0 -130
  20. package/dist/application/use-cases/generate-thread-title.js +0 -181
  21. package/dist/application/use-cases/narrator-prompt-template.js +0 -158
  22. package/dist/application/use-cases/plan-auto-review.js +0 -57
  23. package/dist/application/use-cases/reconcile-audit.js +0 -100
  24. package/dist/application/use-cases/search-sessions.js +0 -70
  25. package/dist/application/use-cases/should-auto-review.js +0 -71
  26. package/dist/application/use-cases/thread-export.js +0 -245
  27. package/dist/application/use-cases/verify-claim.js +0 -456
  28. package/dist/application/use-cases/write-pass.js +0 -277
  29. package/dist/domain/audit.js +0 -11
  30. package/dist/domain/services/codebase-detector.js +0 -85
  31. package/dist/domain/task.js +0 -91
  32. package/dist/error-handler.js +0 -139
  33. package/dist/infrastructure/completion/bash-completion.js +0 -29
  34. package/dist/infrastructure/completion/fish-completion.js +0 -17
  35. package/dist/infrastructure/completion/zsh-completion.js +0 -25
  36. package/dist/infrastructure/config/config-manager.js +0 -174
  37. package/dist/infrastructure/config/product-context.js +0 -49
  38. package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
  39. package/dist/infrastructure/context/adapters/figma-source.js +0 -198
  40. package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
  41. package/dist/infrastructure/context/adapters/linear-source.js +0 -190
  42. package/dist/infrastructure/context/context-resolver.js +0 -144
  43. package/dist/infrastructure/context/context-source.js +0 -114
  44. package/dist/infrastructure/context/github-api.js +0 -110
  45. package/dist/infrastructure/context/github-device-flow.js +0 -65
  46. package/dist/infrastructure/context/oauth-flow.js +0 -207
  47. package/dist/infrastructure/context/oauth-store.js +0 -122
  48. package/dist/infrastructure/context/registry-bootstrap.js +0 -95
  49. package/dist/infrastructure/context/write-pass-resolver.js +0 -106
  50. package/dist/infrastructure/cost/budget-config.js +0 -114
  51. package/dist/infrastructure/cost/cost-router.js +0 -97
  52. package/dist/infrastructure/cost/spend-aggregator.js +0 -222
  53. package/dist/infrastructure/cost/team-ledger.js +0 -355
  54. package/dist/infrastructure/discovery/discovery-log.js +0 -85
  55. package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
  56. package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
  57. package/dist/infrastructure/hooks/hook-mode.js +0 -87
  58. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
  59. package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
  60. package/dist/infrastructure/hooks/session-analyzer.js +0 -177
  61. package/dist/infrastructure/observability/execution-tracer.js +0 -227
  62. package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
  63. package/dist/infrastructure/orchestration/agent-installer.js +0 -101
  64. package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
  65. package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
  66. package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
  67. package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
  68. package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
  69. package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
  70. package/dist/infrastructure/orchestration/context-handoff.js +0 -162
  71. package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
  72. package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
  73. package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
  74. package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
  75. package/dist/infrastructure/orchestration/failover-config.js +0 -67
  76. package/dist/infrastructure/orchestration/failover-policy.js +0 -106
  77. package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
  78. package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
  79. package/dist/infrastructure/orchestration/graphify-context.js +0 -209
  80. package/dist/infrastructure/orchestration/memory-injector.js +0 -380
  81. package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
  82. package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
  83. package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
  84. package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
  85. package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
  86. package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
  87. package/dist/infrastructure/orchestration/readiness.js +0 -103
  88. package/dist/infrastructure/orchestration/session-manager.js +0 -1590
  89. package/dist/infrastructure/orchestration/session-persistence.js +0 -394
  90. package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
  91. package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
  92. package/dist/infrastructure/protocols/cloud-sync.js +0 -207
  93. package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
  94. package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
  95. package/dist/infrastructure/protocols/mcp-tools.js +0 -401
  96. package/dist/infrastructure/protocols/probe-port.js +0 -36
  97. package/dist/infrastructure/protocols/thread-phase.js +0 -105
  98. package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
  99. package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
  100. package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
  101. package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
  102. package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
  103. package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
  104. package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
  105. package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
  106. package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
  107. package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
  108. package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
  109. package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
  110. package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
  111. package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
  112. package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
  113. package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
  114. package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
  115. package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
  116. package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
  117. package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
  118. package/dist/infrastructure/protocols/ui/styles.js +0 -2147
  119. package/dist/infrastructure/protocols/ui/tokens.js +0 -96
  120. package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
  121. package/dist/infrastructure/protocols/verify-page.js +0 -1677
  122. package/dist/infrastructure/security/file-collection.js +0 -118
  123. package/dist/infrastructure/security/fix-code.js +0 -144
  124. package/dist/infrastructure/security/fix-secret.js +0 -89
  125. package/dist/infrastructure/security/permissions-config.js +0 -235
  126. package/dist/infrastructure/security/scan-types.js +0 -10
  127. package/dist/infrastructure/security/ship-scan.js +0 -195
  128. package/dist/infrastructure/security/trust-scorer.js +0 -724
  129. package/dist/infrastructure/security/vibe-checks.js +0 -99
  130. package/dist/infrastructure/storage/atomic-write.js +0 -86
  131. package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
  132. package/dist/infrastructure/storage/project-thread-migration.js +0 -237
  133. package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
  134. package/dist/infrastructure/storage/project-thread-reader.js +0 -409
  135. package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
  136. package/dist/monitoring.js +0 -212
  137. package/dist/presentation/cli/commands/audit-command.js +0 -201
  138. package/dist/presentation/cli/commands/budget-command.js +0 -79
  139. package/dist/presentation/cli/commands/config-command.js +0 -125
  140. package/dist/presentation/cli/commands/context-source-command.js +0 -217
  141. package/dist/presentation/cli/commands/hook-command.js +0 -265
  142. package/dist/presentation/cli/commands/init-command.js +0 -590
  143. package/dist/presentation/cli/commands/memory-command.js +0 -1031
  144. package/dist/presentation/cli/commands/narrator-command.js +0 -101
  145. package/dist/presentation/cli/commands/permission-command.js +0 -105
  146. package/dist/presentation/cli/commands/protocols-builder.js +0 -254
  147. package/dist/presentation/cli/commands/protocols-command.js +0 -84
  148. package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
  149. package/dist/presentation/cli/commands/report-command.js +0 -403
  150. package/dist/presentation/cli/commands/scan-command.js +0 -52
  151. package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
  152. package/dist/presentation/cli/commands/search-command.js +0 -51
  153. package/dist/presentation/cli/commands/setup-command.js +0 -330
  154. package/dist/presentation/cli/commands/status-command.js +0 -614
  155. package/dist/presentation/cli/commands/team-command.js +0 -256
  156. package/dist/presentation/cli/commands/thread-command.js +0 -370
  157. package/dist/presentation/cli/commands/trust-command.js +0 -328
  158. package/dist/presentation/cli/commands/verify-command.js +0 -298
  159. package/dist/presentation/cli/error-display.js +0 -328
  160. package/dist/presentation/cli/repl.js +0 -805
  161. package/dist/presentation/cli/task-deliverable.js +0 -153
  162. package/dist/testing/cassette.js +0 -125
  163. package/dist/testing/replay-adapter.js +0 -147
@@ -1,256 +0,0 @@
1
- /**
2
- * `codehere team`, the CLI face of the local-first team cost+audit ledger.
3
- *
4
- * The data layer (config, snapshots, merge, git sync) lives in
5
- * infrastructure/cost/team-ledger.ts so the CLI and the daemon (`GET /api/team`)
6
- * read the same core. This file is just the terminal rendering + subcommands.
7
- *
8
- * Subcommands:
9
- * (none) / sync write this machine's snapshot, sync the team repo, show merged view
10
- * init [path] create a team: make + git-init a shared folder and point at it
11
- * join <git-url> join a team: clone the shared repo and point at it
12
- * set-dir <path> point at an existing shared team directory
13
- * budget <usd> set the team's shared daily ceiling (off = clear)
14
- */
15
- import { existsSync, mkdirSync } from 'fs';
16
- import { execFileSync } from 'child_process';
17
- import { join as pathJoin, basename } from 'path';
18
- import { loadTeamConfig, saveTeamConfig, codehereHome, initTeamDir, isGitRepo, gitPull, gitShareMine, writeMyShare, buildTeamView, } from '../../../infrastructure/cost/team-ledger.js';
19
- import { success, error, info, heading, newline } from '../../../ui.js';
20
- function fmtUsd(n) {
21
- return n >= 0.01 || n === 0 ? `$${n.toFixed(2)}` : `$${n.toFixed(3)}`;
22
- }
23
- export async function executeTeamCommand(subcommand, value, opts) {
24
- const sub = subcommand ?? 'sync';
25
- switch (sub) {
26
- case 'sync':
27
- case 'show':
28
- return syncAndShow(Boolean(opts?.strict));
29
- case 'init':
30
- return initTeam(value);
31
- case 'join':
32
- if (!value) {
33
- console.error(error('Usage:'), 'codehere team join <git-url>');
34
- process.exit(1);
35
- }
36
- return joinTeam(value);
37
- case 'set-dir':
38
- if (!value) {
39
- console.error(error('Usage:'), 'codehere team set-dir <path-to-shared-dir>');
40
- process.exit(1);
41
- }
42
- return setDir(value);
43
- case 'budget':
44
- if (!value) {
45
- console.error(error('Usage:'), 'codehere team budget <usd> (or `off`)');
46
- process.exit(1);
47
- }
48
- return setBudget(value);
49
- default:
50
- console.error(error('Unknown subcommand:'), sub);
51
- console.error(info('Try:'), 'codehere team [sync | init [path] | join <git-url> | set-dir <path> | budget <usd>]');
52
- process.exit(1);
53
- }
54
- }
55
- function setDir(path) {
56
- const cfg = loadTeamConfig();
57
- cfg.dir = path;
58
- saveTeamConfig(cfg);
59
- console.log(success('Team directory set:'), path);
60
- console.log(info('Next:'), 'run `codehere team` on each machine. Keep that folder in your own git/shared drive.');
61
- }
62
- // Create a team: make + git-init a shared folder, point at it, write the first
63
- // snapshot — so a lead sets up a team in one command, no hand-editing config.
64
- function initTeam(dir) {
65
- const target = dir || pathJoin(codehereHome(), 'team-shared');
66
- try {
67
- initTeamDir(target); // shared core: mkdir + git-init + README (also used by POST /api/team/init)
68
- }
69
- catch {
70
- console.error(error('Could not create team dir:'), target, '— check the path and permissions.');
71
- process.exit(1);
72
- }
73
- const cfg = loadTeamConfig();
74
- cfg.dir = target;
75
- saveTeamConfig(cfg);
76
- try {
77
- writeMyShare(target);
78
- }
79
- catch {
80
- /* best-effort first snapshot */
81
- }
82
- console.log(success('Team created:'), target);
83
- console.log(info('Share it:'), 'push this folder to a git repo, then each teammate runs `codehere team join <git-url>`.');
84
- }
85
- // Join a team: clone the shared repo under ~/.codehere/teams/<name>, point at it,
86
- // write this machine's first snapshot.
87
- function joinTeam(url) {
88
- // A join URL is shared/pasted, so it's a real attack surface. git's ext::/fd::
89
- // (and file::) transports execute arbitrary commands, and a leading-dash URL is
90
- // parsed as a git option (argument injection). Allow only real remote schemes.
91
- if (/^-/.test(url) || /^(ext|fd|file)::/i.test(url) || !/^(https?:\/\/|git:\/\/|ssh:\/\/|git@[^/]+:)/.test(url)) {
92
- console.error(error('Unsupported git URL:'), url);
93
- console.error(info('Use:'), 'https:// , git:// , ssh:// , or git@host:owner/repo');
94
- process.exit(1);
95
- }
96
- const name = basename(url.replace(/\.git$/, '')) || 'team-shared';
97
- const target = pathJoin(codehereHome(), 'teams', name);
98
- if (existsSync(target)) {
99
- console.error(error('Already exists:'), target);
100
- console.error(info('Point at it instead:'), `codehere team set-dir ${target}`);
101
- process.exit(1);
102
- }
103
- try {
104
- mkdirSync(pathJoin(codehereHome(), 'teams'), { recursive: true });
105
- // Belt-and-suspenders: disable the command-executing transports at the git
106
- // level too, and `--` so the URL can never be read as an option.
107
- execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.allow=user', 'clone', '-q', '--', url, target], { stdio: 'inherit' });
108
- }
109
- catch {
110
- console.error(error('Clone failed:'), url, '— check the URL and your access.');
111
- process.exit(1);
112
- }
113
- if (!existsSync(pathJoin(target, 'spend')))
114
- mkdirSync(pathJoin(target, 'spend'), { recursive: true });
115
- const cfg = loadTeamConfig();
116
- cfg.dir = target;
117
- saveTeamConfig(cfg);
118
- try {
119
- writeMyShare(target);
120
- }
121
- catch {
122
- /* best-effort */
123
- }
124
- console.log(success('Joined team:'), target);
125
- console.log(info('Next:'), 'run `codehere team` to sync and see the merged view.');
126
- }
127
- function setBudget(value) {
128
- const cfg = loadTeamConfig();
129
- if (value.toLowerCase() === 'off') {
130
- cfg.dailyUsd = null;
131
- saveTeamConfig(cfg);
132
- console.log(success('Team daily budget cleared.'));
133
- return;
134
- }
135
- const num = Number(value.replace(/^\$/, ''));
136
- if (!Number.isFinite(num) || num <= 0) {
137
- console.error(error('Invalid amount:'), value, '(use a positive number, e.g. `codehere team budget 50`)');
138
- process.exit(1);
139
- }
140
- cfg.dailyUsd = num;
141
- saveTeamConfig(cfg);
142
- console.log(success('Team daily budget set:'), fmtUsd(num));
143
- }
144
- function syncAndShow(strict = false) {
145
- const cfg = loadTeamConfig();
146
- if (!cfg.dir) {
147
- console.error(error('No team directory set.'));
148
- console.error(info('Set one (a folder your team controls, e.g. a shared git repo):'));
149
- console.error(' codehere team set-dir /path/to/team-shared-dir');
150
- process.exit(1);
151
- }
152
- if (!existsSync(cfg.dir)) {
153
- console.error(error('Team directory does not exist:'), cfg.dir);
154
- console.error(info('Create it (or clone the team repo) first, then re-run.'));
155
- process.exit(1);
156
- }
157
- const gitMode = isGitRepo(cfg.dir);
158
- const pulled = gitMode ? gitPull(cfg.dir) : false;
159
- const mine = writeMyShare(cfg.dir);
160
- const pushed = gitMode ? gitShareMine(cfg.dir, `spend/${mine.machineId}.json`) : null;
161
- const view = buildTeamView(cfg.dir, cfg.dailyUsd);
162
- newline();
163
- console.log(heading(`Team spend today (${view.date}), estimate, not a provider invoice`));
164
- console.log(' ' + '─'.repeat(48));
165
- for (const m of view.machines) {
166
- const me = m.machineId === view.myMachineId ? ' (this machine)' : '';
167
- console.log(` ${m.machineId.padEnd(28)} ${fmtUsd(m.totalUsd).padStart(9)}${me}`);
168
- }
169
- const agents = Object.entries(view.perAgent).sort((a, b) => b[1] - a[1]);
170
- if (agents.length) {
171
- console.log(' ' + '─'.repeat(48));
172
- console.log(info(' by agent (cost · what each did)'));
173
- for (const [agent, usd] of agents) {
174
- const sess = view.perAgentSessions[agent] ?? 0;
175
- console.log(` ${agent.padEnd(20)} ${fmtUsd(usd).padStart(9)} ${String(sess).padStart(3)} session${sess === 1 ? '' : 's'}`);
176
- }
177
- }
178
- const projects = Object.entries(view.perProject).sort((a, b) => b[1] - a[1]);
179
- if (projects.length) {
180
- console.log(' ' + '─'.repeat(48));
181
- console.log(info(' by project'));
182
- for (const [proj, usd] of projects)
183
- console.log(` ${proj.padEnd(28)} ${fmtUsd(usd).padStart(9)}`);
184
- }
185
- // Practice runs sit OUTSIDE the totals (their cost is the mock's own
186
- // fabrication, never billed) — one quiet line, only when any exist.
187
- if (view.practice && view.practice.sessions > 0) {
188
- console.log(' ' + '─'.repeat(48));
189
- console.log(info(` practice runs · ${view.practice.sessions} session${view.practice.sessions === 1 ? '' : 's'} · ${fmtUsd(view.practice.usd)} fabricated · not in the totals`));
190
- }
191
- console.log(' ' + '─'.repeat(48));
192
- const totalLine = ` ${'TEAM TOTAL'.padEnd(28)} ${fmtUsd(view.teamTotal).padStart(9)}`;
193
- if (view.dailyUsd !== null) {
194
- const pct = Math.min(100, Math.round((view.teamTotal / view.dailyUsd) * 100));
195
- console.log(totalLine + ` of ${fmtUsd(view.dailyUsd)} (${pct}%)`);
196
- if (view.teamTotal >= view.dailyUsd)
197
- console.log(error(' Over the team daily budget.'));
198
- else if (pct >= 80)
199
- console.log(info(' Approaching the team daily budget.'));
200
- }
201
- else {
202
- console.log(totalLine);
203
- console.log(info(' No team budget set, `codehere team budget <usd>` to add one.'));
204
- }
205
- if (view.totalActions > 0) {
206
- newline();
207
- console.log(heading('What the agents did (recorded, integrity-checked)'));
208
- console.log(' ' + '─'.repeat(48));
209
- for (const m of view.machines.filter((x) => x.audit).sort((a, b) => b.audit.actions - a.audit.actions)) {
210
- const status = m.audit.chainOk ? success('chain ok') : error('chain BROKEN');
211
- console.log(` ${m.machineId.padEnd(28)} ${String(m.audit.actions).padStart(6)} actions ${status}`);
212
- }
213
- console.log(' ' + '─'.repeat(48));
214
- console.log(` ${'TEAM ACTIONS'.padEnd(28)} ${String(view.totalActions).padStart(6)}`);
215
- if (view.brokenChains.length)
216
- console.log(error(` Integrity broken on: ${view.brokenChains.join(', ')}. Investigate before trusting that record.`));
217
- }
218
- if (view.tamperedShares.length || view.unsignedShares.length) {
219
- newline();
220
- if (view.tamperedShares.length)
221
- console.log(error(` ✗ Signature/key mismatch on: ${view.tamperedShares.join(', ')} — altered, or re-signed with a different key than first seen. Don't trust that share.`));
222
- if (view.unsignedShares.length)
223
- console.log(info(` Unsigned, can't verify: ${view.unsignedShares.join(', ')}.`));
224
- }
225
- else if (view.machines.length) {
226
- newline();
227
- console.log(success(' ✓ All shares signed; keys match the ones first seen (TOFU) — no post-write change detected.'));
228
- }
229
- newline();
230
- if (gitMode) {
231
- const pushMsg = pushed === 'pushed'
232
- ? 'pushed yours'
233
- : pushed === 'committed-local'
234
- ? 'committed yours locally (no remote or offline)'
235
- : pushed === 'nochange'
236
- ? 'yours unchanged'
237
- : 'could not push yours';
238
- console.log(info('Synced:'), `git ${pulled ? 'pulled teammates' : 'pull skipped'}, ${pushMsg}. ${view.machines.length} machine(s) merged, all in your team's own repo, nothing sent to codehere.`);
239
- }
240
- else {
241
- console.log(info('Local-first:'), `${view.machines.length} machine(s) merged from ${cfg.dir}, codehere sent nothing to a server.`);
242
- console.log(info('Tip:'), 'make the team dir a git repo with a remote and codehere will pull/push it for you.');
243
- }
244
- // --strict: refuse to exit clean if the record can't be trusted. A tampered
245
- // signature or a broken audit chain means the team total above is not a
246
- // trustworthy record — fail closed so CI / a lead's gate catches it.
247
- if (strict) {
248
- const bad = [...view.tamperedShares, ...view.brokenChains];
249
- if (bad.length) {
250
- newline();
251
- console.error(error('--strict: record integrity failed.'), `Untrustworthy: ${[...new Set(bad)].join(', ')}.`);
252
- process.exit(1);
253
- }
254
- }
255
- }
256
- //# sourceMappingURL=team-command.js.map
@@ -1,370 +0,0 @@
1
- /**
2
- * CLI: `codehere thread` · v0.8 S4 · signed-payload share + import
3
- *
4
- * Subcommands:
5
- * export <session-id> [--out <file>] [--json]
6
- * Sign the session bundle and write it to <file> (or stdout when no --out).
7
- * import <file> [--trust] [--json]
8
- * Verify the signature. If the signer's key isn't trusted, refuse unless
9
- * --trust is set OR the user is on a TTY and confirms. On valid+trusted,
10
- * merge the session into ~/.codehere/sessions.jsonl so it appears in the
11
- * dashboard.
12
- * keys · print the local public key
13
- * trust <pubkey> [label] · add a public key to the trusted set
14
- * untrust <pubkey> · remove
15
- *
16
- * No network. Both export and import are local file operations.
17
- */
18
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'fs';
19
- import { homedir, hostname } from 'os';
20
- import { dirname, join, isAbsolute, resolve } from 'path';
21
- import { colors, symbols } from '../../../ui.js';
22
- import { signBundle, verifyBundle, shortFingerprint, collectAuditEntriesForSession, verifyEmbeddedAuditChain, } from '../../../application/use-cases/thread-export.js';
23
- import { loadOrCreateKeypair, localPublicKeyBase64, trustKey, untrustKey, trustedKeySet, loadTrustedKeys, isKeyTrusted, } from '../../../infrastructure/orchestration/thread-key-store.js';
24
- import { loadSessionById } from '../../../infrastructure/orchestration/session-persistence.js';
25
- import { getVersion } from '../../../utils/version.js';
26
- export function executeThreadCommand(sub, arg, _extra, options = {}) {
27
- switch (sub) {
28
- case 'export':
29
- return exportThread(arg, options);
30
- case 'import':
31
- return importThread(arg, options);
32
- case 'keys':
33
- return showKeys(options.json ?? false);
34
- case 'trust':
35
- return trustCmd(arg, options.label, options.json ?? false);
36
- case 'untrust':
37
- return untrustCmd(arg, options.json ?? false);
38
- default:
39
- process.stderr.write(colors.error(`${symbols.error} Unknown subcommand: ${sub ?? '(none)'}\n`) +
40
- colors.dim('Try: codehere thread export <id> | import <file> | keys | trust <key> | untrust <key>\n'));
41
- return 2;
42
- }
43
- }
44
- /* ──────────────────────────────────────────────────────────────────────── */
45
- /* export */
46
- /* ──────────────────────────────────────────────────────────────────────── */
47
- function exportThread(sessionId, options) {
48
- if (!sessionId) {
49
- process.stderr.write(colors.error(`${symbols.error} Need a session id\n`));
50
- return 2;
51
- }
52
- const session = loadSessionById(sessionId);
53
- if (!session) {
54
- process.stderr.write(colors.error(`${symbols.error} No session found matching "${sessionId}"\n`));
55
- return 2;
56
- }
57
- const keys = loadOrCreateKeypair();
58
- const body = {
59
- schema: 1,
60
- kind: 'thread',
61
- sessionId: session.sessionId,
62
- session,
63
- // Embed the session's tool-call audit so a teammate can inspect what the
64
- // agent did (signer-attested; see ThreadBundleBody.auditChain trust model).
65
- auditChain: collectAuditEntriesForSession(session.sessionId),
66
- exporter: {
67
- hostname: safeHostname(),
68
- codehereVersion: getVersion(),
69
- },
70
- exportedAt: new Date().toISOString(),
71
- };
72
- const bundle = signBundle(body, keys);
73
- const text = JSON.stringify(bundle, null, 2);
74
- if (options.out) {
75
- const outPath = isAbsolute(options.out) ? options.out : resolve(process.cwd(), options.out);
76
- mkdirSync(dirname(outPath), { recursive: true });
77
- writeFileSync(outPath, text, 'utf-8');
78
- if (options.json) {
79
- process.stdout.write(JSON.stringify({ ok: true, path: outPath, fingerprint: shortFingerprint(bundle.publicKey) }) + '\n');
80
- }
81
- else {
82
- process.stdout.write(colors.green(`${symbols.success} Exported`) +
83
- colors.dim(` ${outPath}\n`) +
84
- colors.dim(` signed by ${shortFingerprint(bundle.publicKey)}\n`) +
85
- colors.dim(` share via Slack/email/PR · recipient runs: `) +
86
- colors.bold(`codehere thread import ${outPath}\n`));
87
- }
88
- return 0;
89
- }
90
- // No --out · print to stdout (machine-pipe-friendly)
91
- process.stdout.write(text + '\n');
92
- return 0;
93
- }
94
- /* ──────────────────────────────────────────────────────────────────────── */
95
- /* import */
96
- /* ──────────────────────────────────────────────────────────────────────── */
97
- function importThread(filePath, options) {
98
- if (!filePath) {
99
- process.stderr.write(colors.error(`${symbols.error} Need a file path\n`));
100
- return 2;
101
- }
102
- const fp = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
103
- if (!existsSync(fp)) {
104
- process.stderr.write(colors.error(`${symbols.error} Not found: ${fp}\n`));
105
- return 2;
106
- }
107
- let raw;
108
- try {
109
- raw = JSON.parse(readFileSync(fp, 'utf-8'));
110
- }
111
- catch (err) {
112
- process.stderr.write(colors.error(`${symbols.error} Invalid JSON: ${err instanceof Error ? err.message : String(err)}\n`));
113
- return 2;
114
- }
115
- const verdict = verifyBundle(raw, trustedKeySet());
116
- if (verdict.status === 'malformed') {
117
- if (options.json) {
118
- process.stdout.write(JSON.stringify({ ok: false, error: 'malformed', detail: verdict.reason }) + '\n');
119
- }
120
- else {
121
- process.stderr.write(colors.error(`${symbols.error} Malformed bundle: ${verdict.reason}\n`));
122
- }
123
- return 2;
124
- }
125
- if (verdict.status === 'tampered') {
126
- if (options.json) {
127
- process.stdout.write(JSON.stringify({ ok: false, error: 'tampered', detail: verdict.reason }) + '\n');
128
- }
129
- else {
130
- process.stderr.write(colors.error(`${symbols.error} Signature INVALID · refusing to import\n`) +
131
- colors.dim(` ${verdict.reason}\n`));
132
- }
133
- return 1;
134
- }
135
- // status: 'valid' or 'unknown_key'
136
- const bundle = verdict.status === 'valid' ? verdict.bundle : verdict.bundle;
137
- const fp_key = shortFingerprint(bundle.publicKey);
138
- // Re-verify the embedded tool-call audit so the recipient SEES what the agent
139
- // did — not just the narration. Honest semantics: "verified" is gated on
140
- // verified>0 (a valid AuditChainCheck is true even for an empty set), and a
141
- // tampered entry is surfaced loudly. The bundle SIGNATURE already proved the
142
- // chain is signer-attested + unaltered-in-transit; this confirms each entry's
143
- // self-hash on top.
144
- const auditCheck = bundle.body.auditChain
145
- ? verifyEmbeddedAuditChain(bundle.body.auditChain)
146
- : undefined;
147
- if (verdict.status === 'unknown_key') {
148
- if (!options.trust) {
149
- if (options.json) {
150
- process.stdout.write(JSON.stringify({
151
- ok: false,
152
- error: 'unknown_key',
153
- publicKey: bundle.publicKey,
154
- fingerprint: fp_key,
155
- hint: 'Re-run with --trust to add this key to your trust store and import.',
156
- }) + '\n');
157
- }
158
- else {
159
- process.stderr.write(colors.warning(`${symbols.warning} Signer's key is not in your trust store\n`) +
160
- colors.dim(` fingerprint: ${fp_key}\n`) +
161
- colors.dim(` The signature is cryptographically valid but you've never trusted this key.\n`) +
162
- colors.dim(` To trust + import: `) +
163
- colors.bold(`codehere thread import ${fp} --trust\n`) +
164
- colors.dim(` To inspect first: `) +
165
- colors.bold(`codehere thread trust ${bundle.publicKey} <label>`) +
166
- '\n');
167
- }
168
- return 2;
169
- }
170
- // --trust set · add key to store and continue
171
- trustKey(bundle.publicKey, options.label);
172
- }
173
- // Persist into ~/.codehere/sessions.jsonl as 3 records (spawned, completed, finalReport)
174
- const ses = bundle.body.session;
175
- // No-clobber / idempotent import (Phase 2.5 multi-human): re-importing the same
176
- // session must not duplicate it in a shared room. Append-only never LOSES work,
177
- // but a second import would double the records (and any count-based view).
178
- if (loadSessionById(ses.sessionId)) {
179
- if (options.json) {
180
- process.stdout.write(JSON.stringify({ ok: true, sessionId: ses.sessionId, alreadyImported: true, fingerprint: fp_key }) + '\n');
181
- }
182
- else {
183
- process.stdout.write(colors.dim(`${symbols.info} session ${ses.sessionId.slice(0, 8)} is already imported · nothing to do\n`));
184
- }
185
- return 0;
186
- }
187
- const home = process.env.CODEHERE_HOME || homedir();
188
- const sessionsPath = join(home, '.codehere', 'sessions.jsonl');
189
- mkdirSync(dirname(sessionsPath), { recursive: true });
190
- const lines = [];
191
- lines.push(JSON.stringify({
192
- t: 'spawned',
193
- id: ses.sessionId,
194
- agent: ses.agent,
195
- prompt: ses.prompt,
196
- cwd: ses.cwd,
197
- startedAt: ses.startedAt,
198
- // S18 · practice classification is a property of the RUN on the exporter's
199
- // machine, carried by the bundle — never the importing daemon's env.
200
- // Absent in a legacy bundle = the work was real and audit-attested; an
201
- // import performed on a mock daemon must never demote a teammate's real
202
- // work to practice. Stamped explicitly so no spawned-record writer leaves
203
- // the field env-classified.
204
- mockRun: ses.mockRun === true,
205
- // Provenance: attribute this session to the signer it was imported from,
206
- // so a shared room shows "from teammate <fingerprint>", not as if local.
207
- importedFrom: fp_key,
208
- // Persist the audit attestation — the bundle isn't stored, so the room
209
- // needs this to show the proof badge. attested gated on verified>0 && no tamper.
210
- ...(auditCheck
211
- ? {
212
- importAudit: {
213
- total: auditCheck.total,
214
- verified: auditCheck.verified,
215
- tampered: auditCheck.tampered.length,
216
- attested: auditCheck.verified > 0 && auditCheck.tampered.length === 0,
217
- },
218
- }
219
- : {}),
220
- }));
221
- lines.push(JSON.stringify({
222
- t: 'completed',
223
- id: ses.sessionId,
224
- status: ses.status,
225
- durationMs: ses.durationMs,
226
- completedAt: ses.completedAt,
227
- steps: ses.steps,
228
- artifacts: ses.artifacts,
229
- }));
230
- if (ses.finalReport) {
231
- lines.push(JSON.stringify({
232
- t: 'finalReport',
233
- id: ses.sessionId,
234
- summary: ses.finalReport.summary,
235
- generatedAt: ses.finalReport.generatedAt,
236
- durationMs: ses.finalReport.durationMs,
237
- }));
238
- }
239
- appendFileSync(sessionsPath, lines.join('\n') + '\n', 'utf-8');
240
- if (options.json) {
241
- process.stdout.write(JSON.stringify({
242
- ok: true,
243
- sessionId: ses.sessionId,
244
- trusted: verdict.status === 'valid' ? true : 'just-added',
245
- fingerprint: fp_key,
246
- audit: auditCheck
247
- ? {
248
- total: auditCheck.total,
249
- verified: auditCheck.verified,
250
- tampered: auditCheck.tampered.length,
251
- legacy: auditCheck.legacy,
252
- // "verified" is true ONLY when entries were actually checked +
253
- // none tampered — never on a bare/empty chain.
254
- attested: auditCheck.verified > 0 && auditCheck.tampered.length === 0,
255
- }
256
- : null,
257
- }) + '\n');
258
- }
259
- else {
260
- process.stdout.write(colors.green(`${symbols.success} Imported`) +
261
- colors.dim(` session ${ses.sessionId.slice(0, 8)} · signed by ${fp_key}\n`));
262
- process.stdout.write(formatAuditLine(auditCheck));
263
- process.stdout.write(colors.dim(` view it: `) +
264
- colors.bold(`codehere start`) +
265
- colors.dim(` then click the task in the dashboard\n`));
266
- }
267
- return 0;
268
- }
269
- /**
270
- * Honest one-line summary of the embedded audit chain for the import surface.
271
- * The "✓ verified" claim is gated on verified>0 (NOT bare check.valid, which is
272
- * true for an empty/all-legacy set where nothing was checked); a tampered entry
273
- * is surfaced as a loud warning. Returns '' when there's no chain to report.
274
- */
275
- function formatAuditLine(check) {
276
- if (!check || check.total === 0) {
277
- return colors.dim(` (no tool-call audit was included in this bundle)\n`);
278
- }
279
- if (check.tampered.length > 0) {
280
- return colors.error(` ${symbols.error} audit chain INCONSISTENT · ${check.tampered.length} of ${check.total} entr${check.tampered.length === 1 ? 'y was' : 'ies were'} altered after writing · the signer's tool-call record is not internally consistent\n`);
281
- }
282
- if (check.verified > 0) {
283
- // Honest framing: this is signer ATTESTATION, not independent provenance —
284
- // the hashes match and the bundle is signed, but trust reduces to the signer's
285
- // key (a malicious signer could fabricate a consistent chain). Say exactly that
286
- // rather than a bare "verified" that implies more than self-hash + signature.
287
- return colors.green(` ${symbols.success} ${check.verified} agent tool call${check.verified === 1 ? '' : 's'} recorded` +
288
- colors.dim(` · hashes intact, as attested by this signer (your trust = the key above)\n`));
289
- }
290
- // total>0 but nothing verifiable (all legacy/no chain fields).
291
- return colors.dim(` ${check.legacy} audit entr${check.legacy === 1 ? 'y' : 'ies'} present but unverifiable (no chain fields)\n`);
292
- }
293
- /* ──────────────────────────────────────────────────────────────────────── */
294
- /* keys / trust / untrust */
295
- /* ──────────────────────────────────────────────────────────────────────── */
296
- function showKeys(json) {
297
- loadOrCreateKeypair();
298
- const localB64 = localPublicKeyBase64() ?? '';
299
- const trusted = loadTrustedKeys();
300
- if (json) {
301
- process.stdout.write(JSON.stringify({
302
- local: { publicKey: localB64, fingerprint: shortFingerprint(localB64) },
303
- trusted: Object.entries(trusted.keys).map(([k, v]) => ({
304
- publicKey: k,
305
- fingerprint: shortFingerprint(k),
306
- label: v.label,
307
- addedAt: v.addedAt,
308
- })),
309
- }, null, 2) + '\n');
310
- return 0;
311
- }
312
- process.stdout.write('\n' + colors.bold('This machine') + '\n');
313
- process.stdout.write(' ' + colors.cyan(shortFingerprint(localB64)) + colors.dim(' signs your exports\n\n'));
314
- process.stdout.write(colors.bold('Trusted keys') + colors.dim(' (imports from these accepted silently)\n'));
315
- const entries = Object.entries(trusted.keys);
316
- if (entries.length === 0) {
317
- process.stdout.write(' ' + colors.dim('(empty)\n'));
318
- }
319
- else {
320
- for (const [k, v] of entries) {
321
- const label = v.label || '(no label)';
322
- process.stdout.write(' ' + colors.cyan(shortFingerprint(k)) + ' ' + colors.dim(label) + '\n');
323
- }
324
- }
325
- process.stdout.write('\n' + colors.dim('Add a key: ') + colors.bold('codehere thread trust <pubkey> <label>') + '\n');
326
- process.stdout.write(colors.dim('Remove a key: ') + colors.bold('codehere thread untrust <pubkey>') + '\n\n');
327
- return 0;
328
- }
329
- function trustCmd(pubkey, label, json) {
330
- if (!pubkey) {
331
- process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
332
- return 2;
333
- }
334
- trustKey(pubkey, label);
335
- if (json) {
336
- process.stdout.write(JSON.stringify({ ok: true, fingerprint: shortFingerprint(pubkey) }) + '\n');
337
- }
338
- else {
339
- process.stdout.write(colors.green(`${symbols.success} trusted ${shortFingerprint(pubkey)}\n`));
340
- }
341
- return 0;
342
- }
343
- function untrustCmd(pubkey, json) {
344
- if (!pubkey) {
345
- process.stderr.write(colors.error(`${symbols.error} Need a public key (base64)\n`));
346
- return 2;
347
- }
348
- const ok = untrustKey(pubkey);
349
- if (json) {
350
- process.stdout.write(JSON.stringify({ ok }) + '\n');
351
- }
352
- else if (ok) {
353
- process.stdout.write(colors.green(`${symbols.success} untrusted ${shortFingerprint(pubkey)}\n`));
354
- }
355
- else {
356
- process.stdout.write(colors.gray(`${symbols.dot} key not in trust store (or it's this machine's own key · can't remove)\n`));
357
- }
358
- return 0;
359
- }
360
- function safeHostname() {
361
- try {
362
- return hostname();
363
- }
364
- catch {
365
- return undefined;
366
- }
367
- }
368
- // Public alias for the connection-status check used in the daemon HTTP routes.
369
- export { isKeyTrusted };
370
- //# sourceMappingURL=thread-command.js.map