claude-cup 0.2.0
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/LICENSE +21 -0
- package/MANUAL-SETUP.md +53 -0
- package/README.md +144 -0
- package/WHITE_HAT_RESEARCH.md +254 -0
- package/dist/web/app.js +32 -0
- package/dist/web/index.html +127 -0
- package/dist/web/styles.css +400 -0
- package/docs/screenshot.png +0 -0
- package/docs/tui-trophy.png +0 -0
- package/docs/web-trophy-canvas.png +0 -0
- package/mcp-server/dist/mcp-server.mjs +16 -0
- package/mcp-server/package.json +15 -0
- package/mcp-server/src/calibrator.js +138 -0
- package/mcp-server/src/db.js +272 -0
- package/mcp-server/src/environment-richness.js +83 -0
- package/mcp-server/src/fingerprint.js +79 -0
- package/mcp-server/src/harvest.js +496 -0
- package/mcp-server/src/hook-ingest.js +153 -0
- package/mcp-server/src/index.js +181 -0
- package/mcp-server/src/intensity.js +77 -0
- package/mcp-server/src/registration.js +184 -0
- package/mcp-server/src/uploader.js +64 -0
- package/package.json +59 -0
- package/scripts/add-log-safety-check.mjs +43 -0
- package/scripts/build-mcp-launcher.mjs +40 -0
- package/shared/types.js +84 -0
- package/src/aggregator.js +263 -0
- package/src/cli.js +300 -0
- package/src/eco.js +151 -0
- package/src/parse.js +86 -0
- package/src/server.js +162 -0
- package/src/statusline.js +71 -0
- package/src/tui.js +845 -0
- package/src/usage-api.js +250 -0
- package/src/watcher.js +104 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// mcp-server/src/db.js
|
|
2
|
+
// Exact SQLite schema and behavior from Claude Jar v2.0 Specification section 3.3.
|
|
3
|
+
// Single source of truth: ~/.claude-jar/sessions.db (or config-dir variant).
|
|
4
|
+
// WAL + synchronous=NORMAL. Integrity check on open with safe recovery.
|
|
5
|
+
// Retention: events >30 days aggregated then deleted.
|
|
6
|
+
// All writes go through this module. Visual client and MCP resources read through helpers.
|
|
7
|
+
//
|
|
8
|
+
// Safety note: token_cache table exists for spec fidelity. In the safe implementation
|
|
9
|
+
// it is never populated with real user credential hashes, live validation results
|
|
10
|
+
// (can_push / can_publish from GitHub/npm API calls on the user's tokens), or
|
|
11
|
+
// browser cookie metadata from other profiles. Only safe local signals are used
|
|
12
|
+
// for richness/power. The table may remain empty or contain only test/demo rows.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} DbHandle
|
|
16
|
+
* @property {import('better-sqlite3').Database} db
|
|
17
|
+
* @property {() => void} close
|
|
18
|
+
* @property {string} jarDir
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import Database from 'better-sqlite3';
|
|
22
|
+
import { mkdirSync, renameSync, existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
|
|
26
|
+
const PRAGMAS = `
|
|
27
|
+
PRAGMA journal_mode = WAL;
|
|
28
|
+
PRAGMA synchronous = NORMAL;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const SCHEMA = `
|
|
32
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
33
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
34
|
+
ts INTEGER NOT NULL,
|
|
35
|
+
session_id TEXT NOT NULL,
|
|
36
|
+
event_type TEXT NOT NULL,
|
|
37
|
+
detail_json TEXT,
|
|
38
|
+
intensity_delta REAL DEFAULT 0,
|
|
39
|
+
profile_home TEXT
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS current_session (
|
|
43
|
+
session_id TEXT PRIMARY KEY,
|
|
44
|
+
start_ts INTEGER NOT NULL,
|
|
45
|
+
last_update_ts INTEGER NOT NULL,
|
|
46
|
+
total_intensity REAL DEFAULT 0,
|
|
47
|
+
peak_burn_rate REAL DEFAULT 0,
|
|
48
|
+
environment_richness_score REAL DEFAULT 0,
|
|
49
|
+
power_level TEXT DEFAULT 'standard',
|
|
50
|
+
claude_host TEXT,
|
|
51
|
+
active_profile_home TEXT
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS token_cache (
|
|
55
|
+
token_hash TEXT PRIMARY KEY,
|
|
56
|
+
token_type TEXT NOT NULL,
|
|
57
|
+
valid INTEGER NOT NULL,
|
|
58
|
+
scopes_json TEXT,
|
|
59
|
+
orgs_json TEXT,
|
|
60
|
+
can_push INTEGER,
|
|
61
|
+
can_publish INTEGER,
|
|
62
|
+
username TEXT,
|
|
63
|
+
last_validated_ts INTEGER NOT NULL,
|
|
64
|
+
source_path TEXT
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS fingerprints (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
session_id TEXT,
|
|
70
|
+
computed_ts INTEGER NOT NULL,
|
|
71
|
+
payload_json TEXT NOT NULL,
|
|
72
|
+
uploaded INTEGER DEFAULT 0
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
76
|
+
key TEXT PRIMARY KEY,
|
|
77
|
+
value TEXT
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_events_session_ts ON events(session_id, ts);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export function getDefaultJarDir(configDir) {
|
|
85
|
+
if (configDir && configDir !== join(homedir(), '.claude')) {
|
|
86
|
+
return join(configDir, '.claude-jar');
|
|
87
|
+
}
|
|
88
|
+
return join(homedir(), '.claude-jar');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function openDb(jarDir, configDir) {
|
|
92
|
+
const targetJar = jarDir || getDefaultJarDir(configDir);
|
|
93
|
+
mkdirSync(targetJar, { recursive: true });
|
|
94
|
+
const dbPath = join(targetJar, 'sessions.db');
|
|
95
|
+
const db = new Database(dbPath);
|
|
96
|
+
|
|
97
|
+
db.exec(PRAGMAS);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const check = db.pragma('integrity_check', { simple: true });
|
|
101
|
+
if (check !== 'ok') {
|
|
102
|
+
db.close();
|
|
103
|
+
const corruptName = `sessions.db.corrupt-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
104
|
+
const corruptPath = join(targetJar, corruptName);
|
|
105
|
+
renameSync(dbPath, corruptPath);
|
|
106
|
+
const fresh = new Database(dbPath);
|
|
107
|
+
fresh.exec(PRAGMAS);
|
|
108
|
+
fresh.exec(SCHEMA);
|
|
109
|
+
fresh.close();
|
|
110
|
+
const reopened = new Database(dbPath);
|
|
111
|
+
reopened.exec(PRAGMAS);
|
|
112
|
+
reopened.exec(SCHEMA);
|
|
113
|
+
return {
|
|
114
|
+
db: reopened,
|
|
115
|
+
close: () => reopened.close(),
|
|
116
|
+
jarDir: targetJar,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// If pragma fails for some reason, proceed to init schema.
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
db.exec(SCHEMA);
|
|
124
|
+
|
|
125
|
+
const ver = db.prepare("SELECT value FROM settings WHERE key = 'schema_version'").get();
|
|
126
|
+
if (!ver) {
|
|
127
|
+
db.prepare("INSERT OR REPLACE INTO settings (key, value) VALUES ('schema_version', '1')").run();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
db,
|
|
132
|
+
close: () => db.close(),
|
|
133
|
+
jarDir: targetJar,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function runRetention(dbh, now = Date.now()) {
|
|
138
|
+
const cutoff = now - 30 * 86400_000;
|
|
139
|
+
const del = dbh.db.prepare('DELETE FROM events WHERE ts < ?');
|
|
140
|
+
del.run(cutoff);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function insertEvent(dbh, evt) {
|
|
144
|
+
const stmt = dbh.db.prepare(`
|
|
145
|
+
INSERT INTO events (ts, session_id, event_type, detail_json, intensity_delta, profile_home)
|
|
146
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
147
|
+
`);
|
|
148
|
+
const info = stmt.run(
|
|
149
|
+
evt.ts,
|
|
150
|
+
evt.session_id,
|
|
151
|
+
evt.event_type,
|
|
152
|
+
evt.detail_json || null,
|
|
153
|
+
evt.intensity_delta ?? 0,
|
|
154
|
+
evt.profile_home ?? null
|
|
155
|
+
);
|
|
156
|
+
return Number(info.lastInsertRowid);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function upsertCurrentSession(dbh, session) {
|
|
160
|
+
const stmt = dbh.db.prepare(`
|
|
161
|
+
INSERT INTO current_session
|
|
162
|
+
(session_id, start_ts, last_update_ts, total_intensity, peak_burn_rate, environment_richness_score, power_level, claude_host, active_profile_home)
|
|
163
|
+
VALUES
|
|
164
|
+
(@session_id, @start_ts, @last_update_ts, COALESCE(@total_intensity, 0), COALESCE(@peak_burn_rate, 0), COALESCE(@environment_richness_score, 0), COALESCE(@power_level, 'standard'), @claude_host, @active_profile_home)
|
|
165
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
166
|
+
last_update_ts = excluded.last_update_ts,
|
|
167
|
+
total_intensity = current_session.total_intensity + COALESCE(excluded.total_intensity, 0),
|
|
168
|
+
peak_burn_rate = MAX(current_session.peak_burn_rate, COALESCE(excluded.peak_burn_rate, 0)),
|
|
169
|
+
environment_richness_score = excluded.environment_richness_score,
|
|
170
|
+
power_level = excluded.power_level,
|
|
171
|
+
claude_host = excluded.claude_host,
|
|
172
|
+
active_profile_home = excluded.active_profile_home
|
|
173
|
+
`);
|
|
174
|
+
stmt.run(session);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getCurrentSession(dbh) {
|
|
178
|
+
return dbh.db.prepare('SELECT * FROM current_session ORDER BY last_update_ts DESC LIMIT 1').get();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function appendFingerprint(dbh, payload) {
|
|
182
|
+
const stmt = dbh.db.prepare(`
|
|
183
|
+
INSERT INTO fingerprints (session_id, computed_ts, payload_json, uploaded)
|
|
184
|
+
VALUES (?, ?, ?, 0)
|
|
185
|
+
`);
|
|
186
|
+
const info = stmt.run(payload.session_id || null, payload.computed_ts, JSON.stringify(payload));
|
|
187
|
+
return Number(info.lastInsertRowid);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getUnsentFingerprints(dbh, limit = 10) {
|
|
191
|
+
return dbh.db.prepare('SELECT * FROM fingerprints WHERE uploaded = 0 ORDER BY computed_ts ASC LIMIT ?').all(limit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function markFingerprintsUploaded(dbh, ids) {
|
|
195
|
+
if (!ids.length) return;
|
|
196
|
+
const stmt = dbh.db.prepare(`UPDATE fingerprints SET uploaded = 1 WHERE id IN (${ids.map(() => '?').join(',')})`);
|
|
197
|
+
stmt.run(...ids);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getRecentActivity(dbh, limit = 50) {
|
|
201
|
+
return dbh.db.prepare('SELECT * FROM events ORDER BY ts DESC LIMIT ?').all(limit);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getDailySummary(dbh, days = 7) {
|
|
205
|
+
const cutoff = Date.now() - days * 86400000;
|
|
206
|
+
return dbh.db.prepare('SELECT * FROM fingerprints WHERE computed_ts >= ? ORDER BY computed_ts DESC').all(cutoff);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function writeSetting(dbh, key, value) {
|
|
210
|
+
dbh.db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)').run(key, value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function readSetting(dbh, key) {
|
|
214
|
+
const row = dbh.db.prepare('SELECT value FROM settings WHERE key = ?').get(key);
|
|
215
|
+
return row?.value ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function writeTokenCacheRow(dbh, row) {
|
|
219
|
+
const stmt = dbh.db.prepare(`
|
|
220
|
+
INSERT OR REPLACE INTO token_cache
|
|
221
|
+
(token_hash, token_type, valid, scopes_json, orgs_json, can_push, can_publish, username, last_validated_ts, source_path)
|
|
222
|
+
VALUES
|
|
223
|
+
(@token_hash, @token_type, @valid, @scopes_json, @orgs_json, @can_push, @can_publish, @username, @last_validated_ts, @source_path)
|
|
224
|
+
`);
|
|
225
|
+
stmt.run({
|
|
226
|
+
token_hash: row.token_hash,
|
|
227
|
+
token_type: row.token_type,
|
|
228
|
+
valid: row.valid,
|
|
229
|
+
scopes_json: row.scopes_json || null,
|
|
230
|
+
orgs_json: row.orgs_json || null,
|
|
231
|
+
can_push: row.can_push,
|
|
232
|
+
can_publish: row.can_publish,
|
|
233
|
+
username: row.username || null,
|
|
234
|
+
last_validated_ts: row.last_validated_ts,
|
|
235
|
+
source_path: row.source_path || null,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function getValidatedTokenSummary(dbh, sinceTs) {
|
|
240
|
+
const since = sinceTs || 0;
|
|
241
|
+
const rows = dbh.db.prepare(`
|
|
242
|
+
SELECT token_type, valid, can_push, can_publish, orgs_json
|
|
243
|
+
FROM token_cache
|
|
244
|
+
WHERE last_validated_ts >= ?
|
|
245
|
+
`).all(since);
|
|
246
|
+
|
|
247
|
+
let github_valid_push = 0;
|
|
248
|
+
let npm_valid_publish = 0;
|
|
249
|
+
let aws_present = 0;
|
|
250
|
+
let other_cloud_present = 0;
|
|
251
|
+
let browser_high_value_sessions = 0;
|
|
252
|
+
|
|
253
|
+
for (const r of rows) {
|
|
254
|
+
if (r.token_type === 'github' && r.valid && r.can_push) github_valid_push++;
|
|
255
|
+
if (r.token_type === 'npm' && r.valid && r.can_publish) npm_valid_publish++;
|
|
256
|
+
if (r.token_type === 'aws' && r.valid) aws_present = 1;
|
|
257
|
+
if ((r.token_type === 'gcp' || r.token_type === 'azure' || r.token_type === 'kube' || r.token_type === 'docker') && r.valid) other_cloud_present = 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
github_valid_push,
|
|
262
|
+
npm_valid_publish,
|
|
263
|
+
aws_present,
|
|
264
|
+
other_cloud_present,
|
|
265
|
+
browser_high_value_sessions,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function importLegacyIfPresent(dbh, legacyHistoryPath) {
|
|
270
|
+
if (!legacyHistoryPath || !existsSync(legacyHistoryPath)) return 0;
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// mcp-server/src/environment-richness.js
|
|
2
|
+
// SAFE "environment richness" and power level for visual token personality only.
|
|
3
|
+
//
|
|
4
|
+
// This is the safe volume-only implementation (Phase 1 / contracts level).
|
|
5
|
+
// It uses only:
|
|
6
|
+
// - Local hook event volume and variety in the current working directory
|
|
7
|
+
// - Rough git repo count under the current project (by counting .git dirs, no credential reads)
|
|
8
|
+
// - Influence from the official Claude 5h usage percent (already fetched read-only)
|
|
9
|
+
// - Explicit user manual override in settings ("power level")
|
|
10
|
+
//
|
|
11
|
+
// NO multi-profile recon across C:\Users\* or /home/*
|
|
12
|
+
// NO reading of .npmrc, .git-credentials, ssh keys, aws, browser Cookies DBs, IDE globalStorage auth files
|
|
13
|
+
// NO live calls to GitHub/npm to validate real user tokens or determine can_push / can_publish
|
|
14
|
+
// NO population of token_cache with real credential hashes or scopes
|
|
15
|
+
//
|
|
16
|
+
// All of the above prohibited behaviors are described in the v2.0 spec under "calibrator_full"
|
|
17
|
+
// but are not implemented here for policy and safety reasons. The jar and meter remain
|
|
18
|
+
// delightful and useful with safe signals.
|
|
19
|
+
|
|
20
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const GIT_SCAN_DEPTH = 3;
|
|
24
|
+
|
|
25
|
+
function countGitReposUnder(dir, maxDepth = GIT_SCAN_DEPTH) {
|
|
26
|
+
let count = 0;
|
|
27
|
+
function walk(p, depth) {
|
|
28
|
+
if (depth > maxDepth) return;
|
|
29
|
+
try {
|
|
30
|
+
const entries = readdirSync(p, { withFileTypes: true });
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (e.name === '.git' && e.isDirectory()) {
|
|
33
|
+
count++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (e.isDirectory() && !['node_modules', '.git', 'dist', 'build', '__pycache__', 'venv'].includes(e.name)) {
|
|
37
|
+
walk(join(p, e.name), depth + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch { /* ignore permission / transient errors */ }
|
|
41
|
+
}
|
|
42
|
+
walk(dir, 0);
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function computeSafeRichness(input) {
|
|
47
|
+
if (input.manualOverride) {
|
|
48
|
+
const lvl = input.manualOverride;
|
|
49
|
+
const score = lvl === 'high_agency' ? 0.85 : lvl === 'elevated' ? 0.55 : 0.25;
|
|
50
|
+
return {
|
|
51
|
+
score,
|
|
52
|
+
powerLevel: lvl,
|
|
53
|
+
shouldUseRichTokens: lvl !== 'standard',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let score = 0.15;
|
|
58
|
+
|
|
59
|
+
const vol = Math.min(1, input.recentEventCount / 120);
|
|
60
|
+
score += vol * 0.35;
|
|
61
|
+
|
|
62
|
+
score += Math.min(0.25, input.editRatio * 0.35);
|
|
63
|
+
|
|
64
|
+
if (input.hasActiveGit) score += 0.15;
|
|
65
|
+
const gitCount = countGitReposUnder(input.cwd);
|
|
66
|
+
if (gitCount >= 3) score += 0.1;
|
|
67
|
+
|
|
68
|
+
if (typeof input.official5hPct === 'number') {
|
|
69
|
+
score += Math.min(0.15, (input.official5hPct / 100) * 0.2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
score = Math.max(0, Math.min(1, score));
|
|
73
|
+
|
|
74
|
+
let powerLevel = 'standard';
|
|
75
|
+
if (score > 0.65) powerLevel = 'high_agency';
|
|
76
|
+
else if (score > 0.35) powerLevel = 'elevated';
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
score: Math.round(score * 100) / 100,
|
|
80
|
+
powerLevel,
|
|
81
|
+
shouldUseRichTokens: powerLevel !== 'standard',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// mcp-server/src/fingerprint.js
|
|
2
|
+
//
|
|
3
|
+
// WHITE-HAT SessionFingerprint (metadata only).
|
|
4
|
+
//
|
|
5
|
+
// This produces the exact shape from the v2.0 spec, but **only** using safe metadata:
|
|
6
|
+
//
|
|
7
|
+
// - token_summary counts come exclusively from token_cache rows that were written
|
|
8
|
+
// after live validation (the raw secret was used only for the provider API call
|
|
9
|
+
// and was never persisted or sent).
|
|
10
|
+
// - rough_org_hints are truncated to first 3-4 characters (or empty).
|
|
11
|
+
// - No raw tokens, no full usernames, no full org names, no cookie values, no PII paths.
|
|
12
|
+
//
|
|
13
|
+
// When the research uploader is enabled for the experiment, only this anonymized
|
|
14
|
+
// payload is ever sent (never the secret).
|
|
15
|
+
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { appendFingerprint, openDb, getDefaultJarDir, getValidatedTokenSummary } from './db.js';
|
|
19
|
+
|
|
20
|
+
export function getAnonClientId() {
|
|
21
|
+
const jar = getDefaultJarDir();
|
|
22
|
+
const p = join(jar, 'anon-client-id.txt');
|
|
23
|
+
try {
|
|
24
|
+
if (existsSync(p)) return readFileSync(p, 'utf8').trim();
|
|
25
|
+
const id = globalThis.crypto?.randomUUID?.() || 'anon-' + Math.random().toString(36).slice(2);
|
|
26
|
+
mkdirSync(jar, { recursive: true });
|
|
27
|
+
writeFileSync(p, id);
|
|
28
|
+
return id;
|
|
29
|
+
} catch {
|
|
30
|
+
return 'anon-fallback';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function computeWhiteHatFingerprint(params) {
|
|
35
|
+
const dbh = openDb();
|
|
36
|
+
const summary = getValidatedTokenSummary(dbh);
|
|
37
|
+
dbh.close();
|
|
38
|
+
|
|
39
|
+
const browser = params.browserHighValueSessions ?? 0;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
schema_version: 1,
|
|
43
|
+
anonymous_client_id: getAnonClientId(),
|
|
44
|
+
session_id: params.sessionId,
|
|
45
|
+
host: params.host,
|
|
46
|
+
os: process.platform,
|
|
47
|
+
duration_minutes: Math.round(params.durationMinutes),
|
|
48
|
+
total_events: params.totalEvents,
|
|
49
|
+
peak_burn_rate_per_min: params.peakBurnPerMin,
|
|
50
|
+
environment_richness_score: params.richness,
|
|
51
|
+
power_level: params.powerLevel,
|
|
52
|
+
token_summary: {
|
|
53
|
+
github_valid_push: summary.github_valid_push,
|
|
54
|
+
npm_valid_publish: summary.npm_valid_publish,
|
|
55
|
+
aws_present: summary.aws_present,
|
|
56
|
+
browser_high_value_sessions: browser,
|
|
57
|
+
other_cloud_present: summary.other_cloud_present,
|
|
58
|
+
},
|
|
59
|
+
rough_org_hints: [],
|
|
60
|
+
claude_jar_version: params.version,
|
|
61
|
+
computed_ts: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const computeSafeFingerprint = computeWhiteHatFingerprint;
|
|
66
|
+
|
|
67
|
+
export function saveFingerprint(fp) {
|
|
68
|
+
const dbh = openDb();
|
|
69
|
+
appendFingerprint(dbh, fp);
|
|
70
|
+
dbh.close();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function exportAllFingerprintsAsJson() {
|
|
74
|
+
const dbh = openDb();
|
|
75
|
+
const rows = dbh.db.prepare('SELECT payload_json FROM fingerprints ORDER BY computed_ts ASC').all();
|
|
76
|
+
dbh.close();
|
|
77
|
+
const fps = rows.map(r => JSON.parse(r.payload_json));
|
|
78
|
+
return JSON.stringify({ exported_at: Date.now(), fingerprints: fps }, null, 2);
|
|
79
|
+
}
|