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.
- package/README.md +36 -192
- 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
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
2
|
-
/**
|
|
3
|
-
* The local-first team cost+audit ledger: the shared data layer.
|
|
4
|
-
*
|
|
5
|
-
* codehere is per-machine by design (code never leaves the box). A small team
|
|
6
|
-
* is several boxes, so each machine drops its daily spend + audit roll-up into a
|
|
7
|
-
* directory the team controls (their OWN git repo / shared drive, NOT a
|
|
8
|
-
* codehere relay), and any machine can merge them into one team view. This
|
|
9
|
-
* module is the single owner of that logic so BOTH surfaces read the same core:
|
|
10
|
-
* the CLI (`codehere team`) and the daemon (`GET /api/team`). Spend is the same
|
|
11
|
-
* ESTIMATE the rest of codehere uses (tokens x published rate), never a real
|
|
12
|
-
* invoice.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
15
|
-
import { execFileSync } from 'child_process';
|
|
16
|
-
import { sign as cryptoSign, verify as cryptoVerify, createPublicKey } from 'crypto';
|
|
17
|
-
import { join, isAbsolute, resolve } from 'path';
|
|
18
|
-
import { homedir, hostname } from 'os';
|
|
19
|
-
import { computeSpendToday } from './spend-aggregator.js';
|
|
20
|
-
import { verifyAuditChain } from '../hooks/claude-sdk-hook.js';
|
|
21
|
-
import { loadOrCreateKeypair } from '../orchestration/thread-key-store.js';
|
|
22
|
-
import { getOrCreateToken } from '../protocols/cloud-sync.js';
|
|
23
|
-
export function codehereHome() {
|
|
24
|
-
return join(process.env.CODEHERE_HOME || homedir(), '.codehere');
|
|
25
|
-
}
|
|
26
|
-
export function machineId() {
|
|
27
|
-
return `${hostname()}-${getOrCreateToken().slice(0, 8)}`;
|
|
28
|
-
}
|
|
29
|
-
export function todayStr() {
|
|
30
|
-
const d = new Date();
|
|
31
|
-
const p = (n) => String(n).padStart(2, '0');
|
|
32
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
33
|
-
}
|
|
34
|
-
export function loadTeamConfig() {
|
|
35
|
-
const cfg = { dir: null, dailyUsd: null };
|
|
36
|
-
const path = join(codehereHome(), 'team.yaml');
|
|
37
|
-
if (!existsSync(path))
|
|
38
|
-
return cfg;
|
|
39
|
-
try {
|
|
40
|
-
for (const rawLine of readFileSync(path, 'utf-8').split('\n')) {
|
|
41
|
-
const line = rawLine.replace(/#.*$/, '').trim();
|
|
42
|
-
const idx = line.indexOf(':');
|
|
43
|
-
if (idx === -1)
|
|
44
|
-
continue;
|
|
45
|
-
const key = line.slice(0, idx).trim();
|
|
46
|
-
const val = line.slice(idx + 1).trim();
|
|
47
|
-
// A team dir must be ABSOLUTE: a relative value silently resolves against
|
|
48
|
-
// whatever cwd the daemon/CLI happens to run from — the record then lives
|
|
49
|
-
// wherever you stood. Found live 2026-07-07: a client bug wrote the literal
|
|
50
|
-
// string "undefined" on Jul 1 and six days of shares landed in agent/undefined/.
|
|
51
|
-
if (key === 'dir' && val && val !== 'undefined' && val !== 'null' && isAbsolute(val))
|
|
52
|
-
cfg.dir = val;
|
|
53
|
-
if (key === 'daily_usd') {
|
|
54
|
-
const num = Number(val);
|
|
55
|
-
cfg.dailyUsd = Number.isFinite(num) && num > 0 ? num : null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
/* corrupt file -> defaults; never break the caller */
|
|
61
|
-
}
|
|
62
|
-
return cfg;
|
|
63
|
-
}
|
|
64
|
-
export function saveTeamConfig(cfg) {
|
|
65
|
-
const dir = codehereHome();
|
|
66
|
-
if (!existsSync(dir))
|
|
67
|
-
mkdirSync(dir, { recursive: true });
|
|
68
|
-
// Normalize on write: a dir persists only as an absolute path (see the
|
|
69
|
-
// loadTeamConfig guard for the live incident this closes).
|
|
70
|
-
const dirLine = cfg.dir && cfg.dir !== 'undefined' && cfg.dir !== 'null'
|
|
71
|
-
? `dir: ${resolve(cfg.dir)}`
|
|
72
|
-
: '# dir: /path/to/team-shared-dir';
|
|
73
|
-
const lines = [
|
|
74
|
-
'# codehere team spend ledger. The shared dir is yours (keep it in your own git repo).',
|
|
75
|
-
'# codehere never sends this anywhere, it only reads/writes the folder below.',
|
|
76
|
-
dirLine,
|
|
77
|
-
cfg.dailyUsd !== null ? `daily_usd: ${cfg.dailyUsd}` : '# daily_usd: 50',
|
|
78
|
-
'',
|
|
79
|
-
];
|
|
80
|
-
writeFileAtomic(join(codehereHome(), 'team.yaml'), lines.join('\n'));
|
|
81
|
-
}
|
|
82
|
-
/** Pure merge of every machine's daily spend into one team view. */
|
|
83
|
-
export function mergeTeamSpend(shares) {
|
|
84
|
-
const perAgent = {};
|
|
85
|
-
const perAgentSessions = {};
|
|
86
|
-
const perProject = {};
|
|
87
|
-
const practice = { usd: 0, sessions: 0 };
|
|
88
|
-
let teamTotal = 0;
|
|
89
|
-
for (const m of shares) {
|
|
90
|
-
teamTotal += m.totalUsd;
|
|
91
|
-
for (const [agent, usd] of Object.entries(m.byAgent))
|
|
92
|
-
perAgent[agent] = (perAgent[agent] ?? 0) + usd;
|
|
93
|
-
for (const [agent, n] of Object.entries(m.byAgentSessions ?? {}))
|
|
94
|
-
perAgentSessions[agent] = (perAgentSessions[agent] ?? 0) + n;
|
|
95
|
-
for (const [proj, usd] of Object.entries(m.byProject ?? {}))
|
|
96
|
-
perProject[proj] = (perProject[proj] ?? 0) + usd;
|
|
97
|
-
practice.usd += m.practiceUsd ?? 0;
|
|
98
|
-
practice.sessions += m.practiceSessions ?? 0;
|
|
99
|
-
}
|
|
100
|
-
return { teamTotal, perAgent, perAgentSessions, perProject, practice };
|
|
101
|
-
}
|
|
102
|
-
/** Pure merge of the audit roll-up: total recorded actions + any machine whose
|
|
103
|
-
* chain failed integrity (a broken chain is the one thing a team must see). */
|
|
104
|
-
export function mergeTeamActivity(shares) {
|
|
105
|
-
let totalActions = 0;
|
|
106
|
-
const broken = [];
|
|
107
|
-
for (const m of shares) {
|
|
108
|
-
if (!m.audit)
|
|
109
|
-
continue;
|
|
110
|
-
totalActions += m.audit.actions;
|
|
111
|
-
if (!m.audit.chainOk)
|
|
112
|
-
broken.push(m.machineId);
|
|
113
|
-
}
|
|
114
|
-
return { totalActions, broken };
|
|
115
|
-
}
|
|
116
|
-
/** This machine's current spend + audit snapshot (computed live, not read). */
|
|
117
|
-
export function myShare() {
|
|
118
|
-
const spend = computeSpendToday();
|
|
119
|
-
const byAgent = {};
|
|
120
|
-
const byAgentSessions = {};
|
|
121
|
-
for (const [agent, v] of Object.entries(spend.byAgent)) {
|
|
122
|
-
byAgent[agent] = v.usd;
|
|
123
|
-
byAgentSessions[agent] = v.sessions;
|
|
124
|
-
}
|
|
125
|
-
const byProject = {};
|
|
126
|
-
for (const [proj, v] of Object.entries(spend.byProject))
|
|
127
|
-
byProject[proj] = v.usd;
|
|
128
|
-
const chain = verifyAuditChain();
|
|
129
|
-
return {
|
|
130
|
-
machineId: machineId(),
|
|
131
|
-
date: todayStr(),
|
|
132
|
-
totalUsd: spend.totalUsd,
|
|
133
|
-
byAgent,
|
|
134
|
-
byAgentSessions,
|
|
135
|
-
byProject,
|
|
136
|
-
practiceUsd: spend.practice.usd,
|
|
137
|
-
practiceSessions: spend.practice.sessions,
|
|
138
|
-
updatedAt: new Date().toISOString(),
|
|
139
|
-
audit: { actions: chain.totalEntries, chainOk: chain.valid },
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
/** Deterministic bytes of a share for signing/verifying: everything EXCEPT the
|
|
143
|
-
* signature field, with object keys sorted at every level so sign-time and
|
|
144
|
-
* read-time produce identical input regardless of property order. */
|
|
145
|
-
function canonicalShareBytes(m) {
|
|
146
|
-
const { signature: _omit, ...rest } = m;
|
|
147
|
-
return JSON.stringify(rest, (_k, v) => v && typeof v === 'object' && !Array.isArray(v)
|
|
148
|
-
? Object.fromEntries(Object.keys(v)
|
|
149
|
-
.sort()
|
|
150
|
-
.map((k) => [k, v[k]]))
|
|
151
|
-
: v);
|
|
152
|
-
}
|
|
153
|
-
/** Verdict for one share's signature. 'unsigned' (no sig) ≠ 'tampered' (sig
|
|
154
|
-
* present but doesn't match) — BLOCKED-vs-FAIL discipline. */
|
|
155
|
-
export function verifyShareSignature(m) {
|
|
156
|
-
if (!m.signature || m.signature.alg !== 'ed25519')
|
|
157
|
-
return 'unsigned';
|
|
158
|
-
try {
|
|
159
|
-
const pubKey = createPublicKey({ key: Buffer.from(m.signature.pubkey, 'base64'), format: 'der', type: 'spki' });
|
|
160
|
-
const ok = cryptoVerify(null, Buffer.from(canonicalShareBytes(m), 'utf-8'), pubKey, Buffer.from(m.signature.sig, 'base64'));
|
|
161
|
-
return ok ? 'signed' : 'tampered';
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
return 'tampered'; // malformed sig/key reads as not-verifiable, fail-closed
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/** Trust anchor (known_hosts / TOFU model): machineId → the pubkey first seen for
|
|
168
|
-
* it. A self-signature alone is defeatable (an attacker edits a share, makes a
|
|
169
|
-
* fresh key, re-signs, swaps the embedded pubkey → self-verifies). Pinning the
|
|
170
|
-
* first-seen key per machine turns a later KEY CHANGE into the tamper signal.
|
|
171
|
-
* Stored on the VIEWER's machine, so each teammate pins what they've seen. */
|
|
172
|
-
function knownKeysPath() {
|
|
173
|
-
return join(codehereHome(), 'team-known-keys.json');
|
|
174
|
-
}
|
|
175
|
-
export function loadKnownKeys() {
|
|
176
|
-
try {
|
|
177
|
-
return JSON.parse(readFileSync(knownKeysPath(), 'utf-8'));
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return {};
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function saveKnownKeys(keys) {
|
|
184
|
-
try {
|
|
185
|
-
writeFileAtomic(knownKeysPath(), JSON.stringify(keys, null, 2));
|
|
186
|
-
}
|
|
187
|
-
catch {
|
|
188
|
-
/* best-effort pin store */
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/** Per-team signature roll-up against the trust anchor:
|
|
192
|
-
* - unsigned: no signature (legacy / signing unavailable) — BLOCKED, not FAIL.
|
|
193
|
-
* - tampered: signature doesn't self-verify, OR a KNOWN machine's key changed
|
|
194
|
-
* (re-signed by someone else) — the real forgery catch.
|
|
195
|
-
* - firstSeen: signed + first sight of this machine's key → caller should pin.
|
|
196
|
-
* Pure: knownKeys in, firstSeen out; buildTeamView does the I/O. */
|
|
197
|
-
export function mergeTeamSignatures(shares, knownKeys = {}) {
|
|
198
|
-
const tampered = [];
|
|
199
|
-
const unsigned = [];
|
|
200
|
-
const firstSeen = {};
|
|
201
|
-
for (const m of shares) {
|
|
202
|
-
const v = verifyShareSignature(m);
|
|
203
|
-
if (v === 'unsigned') {
|
|
204
|
-
unsigned.push(m.machineId);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (v === 'tampered') {
|
|
208
|
-
tampered.push(m.machineId);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
// self-verifies — now check it against the key we first pinned for this machine
|
|
212
|
-
const pubkey = m.signature.pubkey;
|
|
213
|
-
const pinned = knownKeys[m.machineId];
|
|
214
|
-
if (pinned && pinned !== pubkey)
|
|
215
|
-
tampered.push(m.machineId); // key changed = re-signed forgery
|
|
216
|
-
else if (!pinned)
|
|
217
|
-
firstSeen[m.machineId] = pubkey; // TOFU: trust on first sight
|
|
218
|
-
}
|
|
219
|
-
return { tampered, unsigned, firstSeen };
|
|
220
|
-
}
|
|
221
|
-
/** Write this machine's snapshot into the team dir's spend/ folder, ed25519-signed
|
|
222
|
-
* with the machine's persisted key so a teammate can detect post-write edits. */
|
|
223
|
-
export function writeMyShare(dir) {
|
|
224
|
-
const mine = myShare();
|
|
225
|
-
try {
|
|
226
|
-
const keys = loadOrCreateKeypair();
|
|
227
|
-
const sig = cryptoSign(null, Buffer.from(canonicalShareBytes(mine), 'utf-8'), keys.privateKey);
|
|
228
|
-
const pubRaw = keys.publicKey.export({ format: 'der', type: 'spki' });
|
|
229
|
-
mine.signature = { alg: 'ed25519', pubkey: pubRaw.toString('base64'), sig: sig.toString('base64') };
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
/* signing is best-effort — an unsigned share is flagged on read, never a crash */
|
|
233
|
-
}
|
|
234
|
-
const spendDir = join(dir, 'spend');
|
|
235
|
-
if (!existsSync(spendDir))
|
|
236
|
-
mkdirSync(spendDir, { recursive: true });
|
|
237
|
-
writeFileAtomic(join(spendDir, `${mine.machineId}.json`), JSON.stringify(mine, null, 2));
|
|
238
|
-
return mine;
|
|
239
|
-
}
|
|
240
|
-
/** Create a shareable team dir: ensure spend/, git-init it (best-effort), and a
|
|
241
|
-
* README. Shared by `codehere team init` (CLI) and POST /api/team/init (daemon)
|
|
242
|
-
* so both surfaces create a team the same way. */
|
|
243
|
-
export function initTeamDir(dir) {
|
|
244
|
-
mkdirSync(join(dir, 'spend'), { recursive: true });
|
|
245
|
-
if (!isGitRepo(dir)) {
|
|
246
|
-
try {
|
|
247
|
-
execFileSync('git', ['init', '-q', '--', dir]);
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
/* git missing — still usable as a plain shared folder */
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const readme = join(dir, 'README.md');
|
|
254
|
-
if (!existsSync(readme)) {
|
|
255
|
-
writeFileSync(readme, '# codehere team ledger\n\nShared cost + integrity record for this team. Each machine writes its own\nsnapshot under `spend/`. Keep this folder in a git repo your team controls —\ncodehere only reads/writes here; nothing leaves your machines.\n');
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/** Read every machine's TODAY snapshot from the team dir, corruption-tolerant. */
|
|
259
|
-
export function readAllShares(dir) {
|
|
260
|
-
const spendDir = join(dir, 'spend');
|
|
261
|
-
if (!existsSync(spendDir))
|
|
262
|
-
return [];
|
|
263
|
-
const out = [];
|
|
264
|
-
for (const file of readdirSync(spendDir)) {
|
|
265
|
-
if (!file.endsWith('.json'))
|
|
266
|
-
continue;
|
|
267
|
-
try {
|
|
268
|
-
const m = JSON.parse(readFileSync(join(spendDir, file), 'utf-8'));
|
|
269
|
-
if (m && m.date === todayStr() && typeof m.totalUsd === 'number')
|
|
270
|
-
out.push(m);
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
/* skip a corrupt teammate file rather than fail the whole view */
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return out;
|
|
277
|
-
}
|
|
278
|
-
/** Assemble the merged TeamView from what's currently in the dir (read-only). */
|
|
279
|
-
export function buildTeamView(dir, dailyUsd) {
|
|
280
|
-
const machines = readAllShares(dir).sort((a, b) => b.totalUsd - a.totalUsd);
|
|
281
|
-
const { teamTotal, perAgent, perAgentSessions, perProject, practice } = mergeTeamSpend(machines);
|
|
282
|
-
const { totalActions, broken } = mergeTeamActivity(machines);
|
|
283
|
-
const known = loadKnownKeys();
|
|
284
|
-
const { tampered, unsigned, firstSeen } = mergeTeamSignatures(machines, known);
|
|
285
|
-
if (Object.keys(firstSeen).length)
|
|
286
|
-
saveKnownKeys({ ...known, ...firstSeen }); // pin newly-seen teammate keys
|
|
287
|
-
return {
|
|
288
|
-
date: todayStr(),
|
|
289
|
-
myMachineId: machineId(),
|
|
290
|
-
machines,
|
|
291
|
-
teamTotal,
|
|
292
|
-
perAgent,
|
|
293
|
-
perAgentSessions,
|
|
294
|
-
perProject,
|
|
295
|
-
practice,
|
|
296
|
-
totalActions,
|
|
297
|
-
brokenChains: broken,
|
|
298
|
-
tamperedShares: tampered,
|
|
299
|
-
unsignedShares: unsigned,
|
|
300
|
-
dailyUsd,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
// ── git sync ─────────────────────────────────────────────────────────────────
|
|
304
|
-
// The team dir should be a DEDICATED git repo the team controls. Single-owner
|
|
305
|
-
// per-machine files (spend/<id>.json) never conflict. All git ops are
|
|
306
|
-
// best-effort: any failure (no remote, offline, not a repo) degrades to plain
|
|
307
|
-
// shared-dir mode and never breaks the view.
|
|
308
|
-
function git(dir, args, timeout = 20_000) {
|
|
309
|
-
execFileSync('git', args, { cwd: dir, stdio: 'ignore', timeout });
|
|
310
|
-
}
|
|
311
|
-
export function isGitRepo(dir) {
|
|
312
|
-
try {
|
|
313
|
-
git(dir, ['rev-parse', '--is-inside-work-tree'], 5_000);
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
return false;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
export function gitPull(dir) {
|
|
321
|
-
try {
|
|
322
|
-
git(dir, ['pull', '--quiet', '--no-edit']);
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
return false;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/** Stage + commit ONLY this machine's snapshot (never `git add -A`), then push. */
|
|
330
|
-
export function gitShareMine(dir, relFile) {
|
|
331
|
-
try {
|
|
332
|
-
git(dir, ['add', relFile]);
|
|
333
|
-
let hasChange = false;
|
|
334
|
-
try {
|
|
335
|
-
git(dir, ['diff', '--cached', '--quiet']); // exit 0 = nothing staged
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
hasChange = true; // exit 1 = staged change present
|
|
339
|
-
}
|
|
340
|
-
if (!hasChange)
|
|
341
|
-
return 'nochange';
|
|
342
|
-
git(dir, ['commit', '-q', '-m', `team snapshot: ${machineId()}`]);
|
|
343
|
-
try {
|
|
344
|
-
git(dir, ['push', '--quiet']);
|
|
345
|
-
return 'pushed';
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
return 'committed-local'; // no remote / offline; still recorded locally
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch {
|
|
352
|
-
return 'failed';
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
//# sourceMappingURL=team-ledger.js.map
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
-
import { homedir, hostname } from 'os';
|
|
3
|
-
import { dirname, join } from 'path';
|
|
4
|
-
// Continuous-discovery instrument (Teresa Torres / producttalk.org method).
|
|
5
|
-
// Two jobs, both local-first and best-effort:
|
|
6
|
-
// 1. record a behavioural signal, "the record was opened on this machine", deduped
|
|
7
|
-
// per local day so a polling dashboard can't inflate it. Honest scope: each member
|
|
8
|
-
// runs their own local daemon, so this is a PER-MACHINE-LOCAL signal (the operator's
|
|
9
|
-
// own opens); it is NOT yet aggregated across the team into one cross-machine number.
|
|
10
|
-
// 2. expose the in-product recruiting link (the scheduler) for the interview hook.
|
|
11
|
-
// Never throws into a request handler; state lives on disk under ~/.codehere.
|
|
12
|
-
function discoveryFile() {
|
|
13
|
-
return join(process.env.CODEHERE_HOME || homedir(), '.codehere', 'discovery.jsonl');
|
|
14
|
-
}
|
|
15
|
-
function localDate(now) {
|
|
16
|
-
const y = now.getFullYear();
|
|
17
|
-
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
18
|
-
const d = String(now.getDate()).padStart(2, '0');
|
|
19
|
-
return `${y}-${m}-${d}`;
|
|
20
|
-
}
|
|
21
|
-
function readEvents() {
|
|
22
|
-
try {
|
|
23
|
-
if (!existsSync(discoveryFile()))
|
|
24
|
-
return [];
|
|
25
|
-
return readFileSync(discoveryFile(), 'utf-8')
|
|
26
|
-
.split('\n')
|
|
27
|
-
.filter(Boolean)
|
|
28
|
-
.map((l) => {
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(l);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
.filter((e) => !!e && e.t === 'record-open' && typeof e.machine === 'string' && typeof e.date === 'string');
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Record that the shared record was opened on this machine, once per local day.
|
|
44
|
-
* A per-machine-local behavioural proxy (each member's daemon records its own opens;
|
|
45
|
-
* not yet aggregated across the team). Deduped so polling can't inflate it. Best-effort.
|
|
46
|
-
*/
|
|
47
|
-
export function recordOpenOncePerDay(machine = hostname(), now = new Date()) {
|
|
48
|
-
try {
|
|
49
|
-
const date = localDate(now);
|
|
50
|
-
for (const e of readEvents()) {
|
|
51
|
-
if (e.machine === machine && e.date === date)
|
|
52
|
-
return; // already counted today
|
|
53
|
-
}
|
|
54
|
-
mkdirSync(dirname(discoveryFile()), { recursive: true });
|
|
55
|
-
const entry = { t: 'record-open', machine, date, at: now.toISOString() };
|
|
56
|
-
appendFileSync(discoveryFile(), JSON.stringify(entry) + '\n');
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
/* best-effort: a discovery metric must never break a request */
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
export function discoverySummary(now = new Date()) {
|
|
63
|
-
const events = readEvents();
|
|
64
|
-
if (!events.length)
|
|
65
|
-
return { recordOpenDays: 0, lastOpen: null, weeklyActive: false };
|
|
66
|
-
const days = new Set(events.map((e) => e.date));
|
|
67
|
-
const times = events
|
|
68
|
-
.map((e) => Date.parse(e.at))
|
|
69
|
-
.filter((n) => !Number.isNaN(n))
|
|
70
|
-
.sort((a, b) => b - a);
|
|
71
|
-
const lastOpen = times.length ? new Date(times[0]).toISOString() : null;
|
|
72
|
-
const weeklyActive = times.length ? times[0] >= now.getTime() - 7 * 24 * 60 * 60 * 1000 : false;
|
|
73
|
-
return { recordOpenDays: days.size, lastOpen, weeklyActive };
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* The interview scheduler link for the in-product recruiting hook.
|
|
77
|
-
* Opt-in: empty string hides the hook entirely (so the dashboard is unchanged by
|
|
78
|
-
* default). Set CODEHERE_DISCOVERY_URL to a scheduler (Calendly-style) to enable it.
|
|
79
|
-
*/
|
|
80
|
-
export function recruitUrl() {
|
|
81
|
-
const url = (process.env.CODEHERE_DISCOVERY_URL || '').trim();
|
|
82
|
-
// Only http(s) — block javascript:/data: so the in-product link can never execute.
|
|
83
|
-
return /^https?:\/\//i.test(url) ? url : '';
|
|
84
|
-
}
|
|
85
|
-
//# sourceMappingURL=discovery-log.js.map
|