codiedev 0.10.0-beta.11 → 0.10.0-beta.13
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/dist/cli.js +39 -0
- package/dist/commands/scrub.js +91 -0
- package/dist/hook.js +13 -0
- package/dist/localScrub.js +689 -0
- package/dist/localScrubReport.js +249 -0
- package/dist/scrub/rules.js +389 -0
- package/package.json +3 -2
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.RUN_INTERVAL_MS = exports.MIN_AGE_MS = void 0;
|
|
37
|
+
exports.sessionStores = sessionStores;
|
|
38
|
+
exports.cursorIdeDb = cursorIdeDb;
|
|
39
|
+
exports.cleanJson = cleanJson;
|
|
40
|
+
exports.cleanJsonl = cleanJsonl;
|
|
41
|
+
exports.cleanText = cleanText;
|
|
42
|
+
exports.cleanYaml = cleanYaml;
|
|
43
|
+
exports.cleanFor = cleanFor;
|
|
44
|
+
exports.cleanFile = cleanFile;
|
|
45
|
+
exports.filesOf = filesOf;
|
|
46
|
+
exports.cleanOpenCodeDb = cleanOpenCodeDb;
|
|
47
|
+
exports.countCursorDb = countCursorDb;
|
|
48
|
+
exports.runLocalScrub = runLocalScrub;
|
|
49
|
+
exports.runAndReport = runAndReport;
|
|
50
|
+
exports.maybeSpawnLocalScrub = maybeSpawnLocalScrub;
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const child_process_1 = require("child_process");
|
|
55
|
+
const rules_1 = require("./scrub/rules");
|
|
56
|
+
const harnessDirs_1 = require("./harnessDirs");
|
|
57
|
+
const copilotHooks_1 = require("./copilotHooks");
|
|
58
|
+
const configDir_1 = require("./configDir");
|
|
59
|
+
const localScrubReport_1 = require("./localScrubReport");
|
|
60
|
+
exports.MIN_AGE_MS = 60 * 60_000;
|
|
61
|
+
exports.RUN_INTERVAL_MS = 60 * 60_000;
|
|
62
|
+
const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
63
|
+
const DEFAULT_BUDGET_MS = 2 * 60_000;
|
|
64
|
+
const STATE_VERSION = 1;
|
|
65
|
+
const isJsonl = (f) => f.endsWith(".jsonl");
|
|
66
|
+
function sessionStores(env = process.env, home = os.homedir(), stateDir = (0, configDir_1.codiedevStateDir)()) {
|
|
67
|
+
const claude = (0, harnessDirs_1.claudeConfigDir)(env, home);
|
|
68
|
+
const codex = (0, harnessDirs_1.codexHome)(env, home);
|
|
69
|
+
const dataHome = env.XDG_DATA_HOME?.trim() ? path.resolve(env.XDG_DATA_HOME.trim()) : path.join(home, ".local", "share");
|
|
70
|
+
const openCodeData = path.join(dataHome, "opencode");
|
|
71
|
+
return [
|
|
72
|
+
{ harness: "claude-code", kind: "jsonl", root: path.join(claude, "projects"), match: isJsonl },
|
|
73
|
+
{ harness: "claude-code", kind: "jsonl", root: path.join(claude, "history.jsonl") },
|
|
74
|
+
{ harness: "codex", kind: "jsonl", root: path.join(codex, "sessions"), match: isJsonl },
|
|
75
|
+
{ harness: "codex", kind: "jsonl", root: path.join(codex, "archived_sessions"), match: isJsonl },
|
|
76
|
+
{ harness: "codex", kind: "jsonl", root: path.join(codex, "history.jsonl") },
|
|
77
|
+
{
|
|
78
|
+
harness: "cursor",
|
|
79
|
+
kind: "jsonl",
|
|
80
|
+
root: path.join(home, ".cursor", "projects"),
|
|
81
|
+
match: (f) => isJsonl(f) && f.split(path.sep).includes("agent-transcripts"),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
harness: "copilot",
|
|
85
|
+
kind: "jsonl",
|
|
86
|
+
root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
|
|
87
|
+
match: (f) => path.basename(f) === "events.jsonl",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
harness: "copilot",
|
|
91
|
+
kind: "yaml",
|
|
92
|
+
root: path.join((0, copilotHooks_1.copilotHome)(env, home), "session-state"),
|
|
93
|
+
match: (f) => path.basename(f) === "workspace.yaml",
|
|
94
|
+
},
|
|
95
|
+
{ harness: "pi", kind: "jsonl", root: path.join((0, harnessDirs_1.piAgentDir)(env, home), "sessions"), match: isJsonl },
|
|
96
|
+
{ harness: "pi", kind: "jsonl", root: path.join(stateDir, "managed", "pi", "agent", "sessions"), match: isJsonl },
|
|
97
|
+
{ harness: "opencode", kind: "json", root: path.join(stateDir, "opencode-captures"), match: (f) => f.endsWith(".json") },
|
|
98
|
+
{ harness: "opencode", kind: "text", root: path.join(openCodeData, "tool-output") },
|
|
99
|
+
{ harness: "opencode", kind: "opencode-db", root: path.join(openCodeData, "opencode.db") },
|
|
100
|
+
{
|
|
101
|
+
harness: "cursor",
|
|
102
|
+
kind: "cursor-agent-db",
|
|
103
|
+
root: path.join(home, ".cursor", "chats"),
|
|
104
|
+
match: (f) => path.basename(f) === "store.db",
|
|
105
|
+
},
|
|
106
|
+
{ harness: "cursor", kind: "cursor-ide-db", root: cursorIdeDb(env, home) },
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
function cursorIdeDb(env = process.env, home = os.homedir(), platform = process.platform) {
|
|
110
|
+
const base = platform === "darwin"
|
|
111
|
+
? path.join(home, "Library", "Application Support")
|
|
112
|
+
: platform === "win32"
|
|
113
|
+
? env.APPDATA || path.join(home, "AppData", "Roaming")
|
|
114
|
+
: env.XDG_CONFIG_HOME?.trim() || path.join(home, ".config");
|
|
115
|
+
return path.join(base, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
116
|
+
}
|
|
117
|
+
function cleanValue(value, count) {
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
const r = (0, rules_1.scrubSecrets)(value);
|
|
120
|
+
count.n += r.redactions.length;
|
|
121
|
+
return r.text;
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(value))
|
|
124
|
+
return value.map((v) => cleanValue(v, count));
|
|
125
|
+
if (value && typeof value === "object") {
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const [k, v] of Object.entries(value))
|
|
128
|
+
out[k] = cleanValue(v, count);
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
function parses(text) {
|
|
134
|
+
try {
|
|
135
|
+
JSON.parse(text);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function cleanJson(text) {
|
|
143
|
+
const raw = (0, rules_1.scrubSecrets)(text);
|
|
144
|
+
if (raw.redactions.length === 0)
|
|
145
|
+
return { text, redactions: 0 };
|
|
146
|
+
if (parses(raw.text) || !parses(text))
|
|
147
|
+
return { text: raw.text, redactions: raw.redactions.length };
|
|
148
|
+
const count = { n: 0 };
|
|
149
|
+
const cleaned = cleanValue(JSON.parse(text), count);
|
|
150
|
+
return { text: JSON.stringify(cleaned), redactions: count.n };
|
|
151
|
+
}
|
|
152
|
+
function cleanJsonl(text) {
|
|
153
|
+
const lines = text.split("\n");
|
|
154
|
+
let redactions = 0;
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const line = lines[i];
|
|
157
|
+
if (!line.trim())
|
|
158
|
+
continue;
|
|
159
|
+
const cr = line.endsWith("\r");
|
|
160
|
+
const body = cr ? line.slice(0, -1) : line;
|
|
161
|
+
const r = cleanJson(body);
|
|
162
|
+
if (r.redactions === 0)
|
|
163
|
+
continue;
|
|
164
|
+
redactions += r.redactions;
|
|
165
|
+
lines[i] = cr ? `${r.text}\r` : r.text;
|
|
166
|
+
}
|
|
167
|
+
return redactions === 0 ? { text, redactions: 0 } : { text: lines.join("\n"), redactions };
|
|
168
|
+
}
|
|
169
|
+
function cleanText(text) {
|
|
170
|
+
const r = (0, rules_1.scrubSecrets)(text);
|
|
171
|
+
return { text: r.text, redactions: r.redactions.length };
|
|
172
|
+
}
|
|
173
|
+
function cleanYaml(text) {
|
|
174
|
+
let redactions = 0;
|
|
175
|
+
const lines = text.split("\n").map((line) => {
|
|
176
|
+
const r = (0, rules_1.scrubSecrets)(line);
|
|
177
|
+
if (r.redactions.length === 0)
|
|
178
|
+
return line;
|
|
179
|
+
redactions += r.redactions.length;
|
|
180
|
+
const m = r.text.match(/^(\s*[^\s:#][^:]*:\s+)(\[REDACTED:.*?)(\r?)$/);
|
|
181
|
+
if (!m)
|
|
182
|
+
return r.text;
|
|
183
|
+
return `${m[1]}'${m[2].replace(/'/g, "''")}'${m[3]}`;
|
|
184
|
+
});
|
|
185
|
+
return redactions === 0 ? { text, redactions: 0 } : { text: lines.join("\n"), redactions };
|
|
186
|
+
}
|
|
187
|
+
function cleanFor(kind, text) {
|
|
188
|
+
return kind === "jsonl" ? cleanJsonl(text) : kind === "json" ? cleanJson(text) : kind === "yaml" ? cleanYaml(text) : cleanText(text);
|
|
189
|
+
}
|
|
190
|
+
function cleanFile(file, kind, deps) {
|
|
191
|
+
let before;
|
|
192
|
+
try {
|
|
193
|
+
before = fs.statSync(file);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return { outcome: "error", redactions: 0 };
|
|
197
|
+
}
|
|
198
|
+
if (deps.now - before.mtimeMs < deps.minAgeMs)
|
|
199
|
+
return { outcome: "too-recent", redactions: 0 };
|
|
200
|
+
if (before.size > MAX_FILE_BYTES)
|
|
201
|
+
return { outcome: "too-large", redactions: 0 };
|
|
202
|
+
let original;
|
|
203
|
+
try {
|
|
204
|
+
original = fs.readFileSync(file);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return { outcome: "error", redactions: 0 };
|
|
208
|
+
}
|
|
209
|
+
const originalText = original.toString("utf8");
|
|
210
|
+
const result = cleanFor(kind, originalText);
|
|
211
|
+
if (result.redactions === 0)
|
|
212
|
+
return { outcome: "clean", redactions: 0 };
|
|
213
|
+
const detail = { keys: (0, localScrubReport_1.keysIn)(originalText), ...(0, localScrubReport_1.describeText)(kind, result.text) };
|
|
214
|
+
if (deps.dryRun)
|
|
215
|
+
return { outcome: "would-clean", redactions: result.redactions, detail };
|
|
216
|
+
const tmp = `${file}.codiedev-scrub-${process.pid}.tmp`;
|
|
217
|
+
try {
|
|
218
|
+
fs.writeFileSync(tmp, result.text, { mode: before.mode & 0o777 });
|
|
219
|
+
deps.beforeSwap?.(file);
|
|
220
|
+
const now = fs.statSync(file);
|
|
221
|
+
if (now.size !== before.size || now.mtimeMs !== before.mtimeMs || !fs.readFileSync(file).equals(original)) {
|
|
222
|
+
fs.rmSync(tmp, { force: true });
|
|
223
|
+
return { outcome: "changed-while-cleaning", redactions: 0 };
|
|
224
|
+
}
|
|
225
|
+
fs.renameSync(tmp, file);
|
|
226
|
+
fs.utimesSync(file, before.atime, before.mtime);
|
|
227
|
+
return { outcome: "cleaned", redactions: result.redactions, detail };
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
try {
|
|
231
|
+
fs.rmSync(tmp, { force: true });
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
return { outcome: "error", redactions: 0 };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function* walk(root) {
|
|
238
|
+
let entries;
|
|
239
|
+
try {
|
|
240
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const e of entries) {
|
|
246
|
+
const p = path.join(root, e.name);
|
|
247
|
+
if (e.isDirectory())
|
|
248
|
+
yield* walk(p);
|
|
249
|
+
else if (e.isFile() && !e.name.includes(".codiedev-scrub-"))
|
|
250
|
+
yield p;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function filesOf(store) {
|
|
254
|
+
let st;
|
|
255
|
+
try {
|
|
256
|
+
st = fs.statSync(store.root);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
if (st.isFile())
|
|
262
|
+
return [store.root];
|
|
263
|
+
const out = [];
|
|
264
|
+
for (const f of walk(store.root))
|
|
265
|
+
if (!store.match || store.match(f))
|
|
266
|
+
out.push(f);
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
function openSqlite(file, readOnly = false) {
|
|
270
|
+
try {
|
|
271
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
272
|
+
const db = readOnly ? new DatabaseSync(file, { readOnly: true }) : new DatabaseSync(file);
|
|
273
|
+
db.exec("PRAGMA busy_timeout = 2000");
|
|
274
|
+
return db;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const OPENCODE_TABLES = [
|
|
281
|
+
{ table: "message", session: "session_id", time: "time_updated" },
|
|
282
|
+
{ table: "part", session: "session_id", time: "time_updated" },
|
|
283
|
+
{ table: "session_message", session: "session_id", time: "time_updated" },
|
|
284
|
+
{ table: "event", session: "aggregate_id" },
|
|
285
|
+
];
|
|
286
|
+
function cleanOpenCodeDb(file, deps) {
|
|
287
|
+
if (!fs.existsSync(file))
|
|
288
|
+
return { outcome: "clean", redactions: 0, rows: 0 };
|
|
289
|
+
const db = openSqlite(file);
|
|
290
|
+
if (!db)
|
|
291
|
+
return { outcome: "no-sqlite", redactions: 0, rows: 0 };
|
|
292
|
+
const sessions = new Map();
|
|
293
|
+
let redactions = 0;
|
|
294
|
+
let rows = 0;
|
|
295
|
+
try {
|
|
296
|
+
const cutoff = deps.now - deps.minAgeMs;
|
|
297
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((t) => t.name));
|
|
298
|
+
const recent = new Set();
|
|
299
|
+
for (const t of OPENCODE_TABLES) {
|
|
300
|
+
if (!tables.has(t.table) || !t.time)
|
|
301
|
+
continue;
|
|
302
|
+
for (const r of db.prepare(`SELECT DISTINCT ${t.session} AS s FROM ${t.table} WHERE ${t.time} >= ?`).all(cutoff)) {
|
|
303
|
+
recent.add(r.s);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const updates = [];
|
|
307
|
+
for (const t of OPENCODE_TABLES) {
|
|
308
|
+
if (!tables.has(t.table))
|
|
309
|
+
continue;
|
|
310
|
+
const all = db.prepare(`SELECT id, ${t.session} AS s, data FROM ${t.table}`).all();
|
|
311
|
+
for (const row of all) {
|
|
312
|
+
if (recent.has(row.s) || typeof row.data !== "string")
|
|
313
|
+
continue;
|
|
314
|
+
if (deps.onlySessions && !deps.onlySessions.has(row.s))
|
|
315
|
+
continue;
|
|
316
|
+
const r = cleanJson(row.data);
|
|
317
|
+
if (r.redactions === 0)
|
|
318
|
+
continue;
|
|
319
|
+
redactions += r.redactions;
|
|
320
|
+
updates.push({ table: t.table, id: row.id, data: r.text, before: row.data });
|
|
321
|
+
const cur = sessions.get(row.s) ?? { keys: [] };
|
|
322
|
+
if (t.table !== "event")
|
|
323
|
+
cur.keys = (0, localScrubReport_1.mergeKeys)(cur.keys, (0, localScrubReport_1.keysIn)(row.data));
|
|
324
|
+
sessions.set(row.s, cur);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
rows = updates.length;
|
|
328
|
+
if (rows === 0)
|
|
329
|
+
return { outcome: "clean", redactions: 0, rows: 0 };
|
|
330
|
+
if (tables.has("session")) {
|
|
331
|
+
const get = db.prepare("SELECT title, directory FROM session WHERE id = ?");
|
|
332
|
+
for (const [id, d] of sessions) {
|
|
333
|
+
const row = get.all(id)[0];
|
|
334
|
+
if (typeof row?.title === "string")
|
|
335
|
+
d.title = (0, rules_1.scrubSecrets)(row.title).text.slice(0, 80);
|
|
336
|
+
if (typeof row?.directory === "string")
|
|
337
|
+
d.cwd = row.directory;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (deps.dryRun)
|
|
341
|
+
return { outcome: "would-clean", redactions, rows, sessions };
|
|
342
|
+
db.exec("BEGIN IMMEDIATE");
|
|
343
|
+
try {
|
|
344
|
+
for (const u of updates) {
|
|
345
|
+
db.prepare(`UPDATE ${u.table} SET data = ? WHERE id = ? AND data = ?`).run(u.data, u.id, u.before);
|
|
346
|
+
}
|
|
347
|
+
db.exec("COMMIT");
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
try {
|
|
351
|
+
db.exec("ROLLBACK");
|
|
352
|
+
}
|
|
353
|
+
catch { }
|
|
354
|
+
throw e;
|
|
355
|
+
}
|
|
356
|
+
return { outcome: "cleaned", redactions, rows, sessions };
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return { outcome: "error", redactions: 0, rows: 0 };
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
try {
|
|
363
|
+
db.close();
|
|
364
|
+
}
|
|
365
|
+
catch { }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function decodeMetaName(hexOrJson) {
|
|
369
|
+
try {
|
|
370
|
+
const text = /^[0-9a-f]+$/i.test(hexOrJson) ? Buffer.from(hexOrJson, "hex").toString("utf8") : hexOrJson;
|
|
371
|
+
const name = JSON.parse(text).name;
|
|
372
|
+
return typeof name === "string" ? name : undefined;
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function countCursorDb(file, kind) {
|
|
379
|
+
const db = openSqlite(file, true);
|
|
380
|
+
if (!db)
|
|
381
|
+
return null;
|
|
382
|
+
try {
|
|
383
|
+
const rows = (kind === "cursor-agent-db"
|
|
384
|
+
? db.prepare("SELECT data AS v FROM blobs").all()
|
|
385
|
+
: db.prepare("SELECT key AS k, value AS v FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' OR key LIKE 'composerData:%'").all());
|
|
386
|
+
const chats = new Map();
|
|
387
|
+
const titles = new Map();
|
|
388
|
+
const agentChat = path.basename(path.dirname(file));
|
|
389
|
+
for (const row of rows) {
|
|
390
|
+
const text = typeof row.v === "string" ? row.v : row.v instanceof Uint8Array ? Buffer.from(row.v).toString("latin1") : "";
|
|
391
|
+
if (!text)
|
|
392
|
+
continue;
|
|
393
|
+
const chat = kind === "cursor-agent-db" ? agentChat : (row.k ?? "").split(":")[1] ?? "";
|
|
394
|
+
if (kind === "cursor-ide-db" && row.k?.startsWith("composerData:")) {
|
|
395
|
+
const name = decodeMetaName(text);
|
|
396
|
+
if (name)
|
|
397
|
+
titles.set(chat, name);
|
|
398
|
+
}
|
|
399
|
+
const keys = (0, localScrubReport_1.keysIn)(text);
|
|
400
|
+
if (keys.length === 0)
|
|
401
|
+
continue;
|
|
402
|
+
chats.set(chat, (0, localScrubReport_1.mergeKeys)(chats.get(chat) ?? [], keys));
|
|
403
|
+
}
|
|
404
|
+
if (kind === "cursor-agent-db" && chats.size > 0) {
|
|
405
|
+
try {
|
|
406
|
+
const meta = db.prepare("SELECT value AS v FROM meta").all();
|
|
407
|
+
for (const m of meta) {
|
|
408
|
+
const name = decodeMetaName(String(m.v));
|
|
409
|
+
if (name)
|
|
410
|
+
titles.set(agentChat, name);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch { }
|
|
414
|
+
}
|
|
415
|
+
const details = [...chats].map(([chat, keys]) => ({
|
|
416
|
+
harness: "cursor",
|
|
417
|
+
sessionKey: chat,
|
|
418
|
+
...(titles.get(chat) ? { title: (0, rules_1.scrubSecrets)(titles.get(chat)).text.slice(0, 80) } : {}),
|
|
419
|
+
keys,
|
|
420
|
+
status: "not_removed",
|
|
421
|
+
reason: "cursor_chat_store",
|
|
422
|
+
}));
|
|
423
|
+
return { keys: details.reduce((n, d) => n + d.keys.reduce((m, k) => m + k.count, 0), 0), chats: details.length, details };
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
try {
|
|
430
|
+
db.close();
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const scrubDir = (stateDir) => path.join(stateDir, "local-scrub");
|
|
436
|
+
function rulesFingerprint() {
|
|
437
|
+
try {
|
|
438
|
+
const src = fs.readFileSync(path.join(__dirname, "scrub", "rules.js"), "utf8");
|
|
439
|
+
let h = 0;
|
|
440
|
+
for (let i = 0; i < src.length; i++)
|
|
441
|
+
h = (h * 31 + src.charCodeAt(i)) | 0;
|
|
442
|
+
return String(h);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return "unknown";
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function readState(dir, rules) {
|
|
449
|
+
try {
|
|
450
|
+
const s = JSON.parse(fs.readFileSync(path.join(dir, "state.json"), "utf8"));
|
|
451
|
+
if (s.version === STATE_VERSION && s.rules === rules && s.files)
|
|
452
|
+
return s;
|
|
453
|
+
}
|
|
454
|
+
catch { }
|
|
455
|
+
return { version: STATE_VERSION, rules, files: {} };
|
|
456
|
+
}
|
|
457
|
+
function writeAtomic(file, text) {
|
|
458
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
459
|
+
fs.writeFileSync(tmp, text, { mode: 0o600 });
|
|
460
|
+
fs.renameSync(tmp, file);
|
|
461
|
+
}
|
|
462
|
+
function runLocalScrub(opts = {}) {
|
|
463
|
+
const now = opts.now ?? Date.now();
|
|
464
|
+
const deps = {
|
|
465
|
+
now,
|
|
466
|
+
minAgeMs: opts.minAgeMs ?? exports.MIN_AGE_MS,
|
|
467
|
+
dryRun: Boolean(opts.dryRun),
|
|
468
|
+
...(opts.onlySessions ? { onlySessions: new Set(opts.onlySessions) } : {}),
|
|
469
|
+
};
|
|
470
|
+
const started = Date.now();
|
|
471
|
+
const budget = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
472
|
+
const stateDir = opts.stateDir ?? (0, configDir_1.codiedevStateDir)();
|
|
473
|
+
const dir = scrubDir(stateDir);
|
|
474
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
475
|
+
const rules = rulesFingerprint();
|
|
476
|
+
const state = readState(dir, rules);
|
|
477
|
+
const only = opts.only ? new Set(opts.only.map((p) => path.resolve(p))) : null;
|
|
478
|
+
const summary = {
|
|
479
|
+
dryRun: deps.dryRun, scanned: 0, cleaned: 0, redactions: 0, skippedRecent: 0, skippedLarge: 0,
|
|
480
|
+
changedWhileCleaning: 0, errors: 0, unfinished: false, byHarness: {}, leftInCursor: { keys: 0, chats: 0 }, sessions: [],
|
|
481
|
+
};
|
|
482
|
+
const found = new Map();
|
|
483
|
+
const detect = (harness, sessionKey, d) => {
|
|
484
|
+
const id = `${harness}\u0000${sessionKey}`;
|
|
485
|
+
const cur = found.get(id);
|
|
486
|
+
if (cur) {
|
|
487
|
+
cur.keys = (0, localScrubReport_1.mergeKeys)(cur.keys, d.keys);
|
|
488
|
+
cur.title ??= d.title;
|
|
489
|
+
cur.cwd ??= d.cwd;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
found.set(id, { harness, sessionKey, ...(d.title ? { title: d.title } : {}), ...(d.cwd ? { cwd: d.cwd } : {}), keys: d.keys, status: "removed" });
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const counted = (state.counted ??= {});
|
|
496
|
+
const tally = (h, n) => {
|
|
497
|
+
const t = (summary.byHarness[h] ??= { files: 0, redactions: 0 });
|
|
498
|
+
t.files += 1;
|
|
499
|
+
t.redactions += n;
|
|
500
|
+
};
|
|
501
|
+
outer: for (const store of opts.stores ?? sessionStores(process.env, os.homedir(), stateDir)) {
|
|
502
|
+
if (store.kind === "cursor-agent-db" || store.kind === "cursor-ide-db") {
|
|
503
|
+
if (only || opts.onlySessions)
|
|
504
|
+
continue;
|
|
505
|
+
for (const file of filesOf(store)) {
|
|
506
|
+
let sig;
|
|
507
|
+
try {
|
|
508
|
+
const st = fs.statSync(file);
|
|
509
|
+
let wal = "";
|
|
510
|
+
try {
|
|
511
|
+
const w = fs.statSync(`${file}-wal`);
|
|
512
|
+
wal = `:${w.size}:${w.mtimeMs}`;
|
|
513
|
+
}
|
|
514
|
+
catch { }
|
|
515
|
+
sig = `${st.size}:${st.mtimeMs}${wal}`;
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (counted[file]?.sig !== sig) {
|
|
521
|
+
const c = countCursorDb(file, store.kind);
|
|
522
|
+
if (!c)
|
|
523
|
+
continue;
|
|
524
|
+
counted[file] = { sig, ...c };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (store.kind === "opencode-db") {
|
|
530
|
+
if (only && !only.has(path.resolve(store.root)))
|
|
531
|
+
continue;
|
|
532
|
+
const r = cleanOpenCodeDb(store.root, deps);
|
|
533
|
+
if (r.outcome === "no-sqlite")
|
|
534
|
+
continue;
|
|
535
|
+
summary.scanned += 1;
|
|
536
|
+
if (r.outcome === "error")
|
|
537
|
+
summary.errors += 1;
|
|
538
|
+
if (r.redactions > 0) {
|
|
539
|
+
summary.redactions += r.redactions;
|
|
540
|
+
if (r.outcome === "cleaned")
|
|
541
|
+
summary.cleaned += 1;
|
|
542
|
+
tally(store.harness, r.redactions);
|
|
543
|
+
for (const [id, d] of r.sessions ?? [])
|
|
544
|
+
detect(store.harness, id, d);
|
|
545
|
+
}
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
for (const file of filesOf(store)) {
|
|
549
|
+
if ((only || opts.onlySessions) && !only?.has(path.resolve(file)))
|
|
550
|
+
continue;
|
|
551
|
+
if (Date.now() - started > budget) {
|
|
552
|
+
summary.unfinished = true;
|
|
553
|
+
break outer;
|
|
554
|
+
}
|
|
555
|
+
let st;
|
|
556
|
+
try {
|
|
557
|
+
st = fs.statSync(file);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const sig = `${st.size}:${st.mtimeMs}`;
|
|
563
|
+
if (!only && state.files[file] === sig)
|
|
564
|
+
continue;
|
|
565
|
+
summary.scanned += 1;
|
|
566
|
+
const r = cleanFile(file, store.kind, deps);
|
|
567
|
+
switch (r.outcome) {
|
|
568
|
+
case "too-recent":
|
|
569
|
+
summary.skippedRecent += 1;
|
|
570
|
+
break;
|
|
571
|
+
case "too-large":
|
|
572
|
+
summary.skippedLarge += 1;
|
|
573
|
+
state.files[file] = sig;
|
|
574
|
+
break;
|
|
575
|
+
case "changed-while-cleaning":
|
|
576
|
+
summary.changedWhileCleaning += 1;
|
|
577
|
+
break;
|
|
578
|
+
case "error":
|
|
579
|
+
summary.errors += 1;
|
|
580
|
+
break;
|
|
581
|
+
case "clean":
|
|
582
|
+
state.files[file] = sig;
|
|
583
|
+
break;
|
|
584
|
+
case "would-clean":
|
|
585
|
+
summary.redactions += r.redactions;
|
|
586
|
+
tally(store.harness, r.redactions);
|
|
587
|
+
if (r.detail)
|
|
588
|
+
detect(store.harness, (0, localScrubReport_1.sessionKeyOf)(store.harness, file), r.detail);
|
|
589
|
+
break;
|
|
590
|
+
case "cleaned": {
|
|
591
|
+
summary.cleaned += 1;
|
|
592
|
+
summary.redactions += r.redactions;
|
|
593
|
+
tally(store.harness, r.redactions);
|
|
594
|
+
if (r.detail)
|
|
595
|
+
detect(store.harness, (0, localScrubReport_1.sessionKeyOf)(store.harness, file), r.detail);
|
|
596
|
+
try {
|
|
597
|
+
const after = fs.statSync(file);
|
|
598
|
+
state.files[file] = `${after.size}:${after.mtimeMs}`;
|
|
599
|
+
}
|
|
600
|
+
catch { }
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
for (const [f, c] of Object.entries(counted)) {
|
|
607
|
+
if (!fs.existsSync(f)) {
|
|
608
|
+
delete counted[f];
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (only || opts.onlySessions)
|
|
612
|
+
continue;
|
|
613
|
+
summary.leftInCursor.keys += c.keys;
|
|
614
|
+
summary.leftInCursor.chats += c.chats;
|
|
615
|
+
summary.sessions.push(...(c.details ?? []));
|
|
616
|
+
}
|
|
617
|
+
summary.sessions.unshift(...found.values());
|
|
618
|
+
if (!deps.dryRun) {
|
|
619
|
+
for (const f of Object.keys(state.files))
|
|
620
|
+
if (!fs.existsSync(f))
|
|
621
|
+
delete state.files[f];
|
|
622
|
+
try {
|
|
623
|
+
writeAtomic(path.join(dir, "state.json"), JSON.stringify(state));
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
try {
|
|
627
|
+
fs.appendFileSync(path.join(dir, "log.jsonl"), JSON.stringify({ at: new Date(now).toISOString(), ...summary }) + "\n", { mode: 0o600 });
|
|
628
|
+
}
|
|
629
|
+
catch { }
|
|
630
|
+
}
|
|
631
|
+
return summary;
|
|
632
|
+
}
|
|
633
|
+
async function runAndReport(opts = {}) {
|
|
634
|
+
const at = opts.now ?? Date.now();
|
|
635
|
+
const summary = runLocalScrub(opts);
|
|
636
|
+
let reported = false;
|
|
637
|
+
if (!summary.dryRun && !opts.only && !opts.onlySessions) {
|
|
638
|
+
try {
|
|
639
|
+
const { readConfig } = require("./utils");
|
|
640
|
+
const { CLI_VERSION } = require("./version");
|
|
641
|
+
const { buildReport, machineId, sendReport } = await Promise.resolve().then(() => __importStar(require("./localScrubReport")));
|
|
642
|
+
const config = readConfig();
|
|
643
|
+
if (config?.token && config.backendUrl) {
|
|
644
|
+
const payload = buildReport({
|
|
645
|
+
machineId: machineId(scrubDir(opts.stateDir ?? (0, configDir_1.codiedevStateDir)())),
|
|
646
|
+
at,
|
|
647
|
+
complete: !summary.unfinished,
|
|
648
|
+
summary,
|
|
649
|
+
sessions: summary.sessions,
|
|
650
|
+
ignoredPaths: config.captureIgnoredPaths,
|
|
651
|
+
});
|
|
652
|
+
reported = await sendReport(config.backendUrl, config.token, payload, CLI_VERSION);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch { }
|
|
656
|
+
}
|
|
657
|
+
return { ...summary, reported };
|
|
658
|
+
}
|
|
659
|
+
function maybeSpawnLocalScrub(opts = {}) {
|
|
660
|
+
try {
|
|
661
|
+
const env = opts.env ?? process.env;
|
|
662
|
+
if (opts.connected === false || env.CODIEDEV_LOCAL_SCRUB === "off" || env.CODIEDEV_LOCAL_SCRUB_CHILD)
|
|
663
|
+
return "disabled";
|
|
664
|
+
const now = opts.now ?? Date.now();
|
|
665
|
+
const dir = scrubDir(opts.stateDir ?? (0, configDir_1.codiedevStateDir)());
|
|
666
|
+
const stamp = path.join(dir, "last-run");
|
|
667
|
+
try {
|
|
668
|
+
const last = fs.statSync(stamp).mtimeMs;
|
|
669
|
+
if (now - last < exports.RUN_INTERVAL_MS && last <= now)
|
|
670
|
+
return "recent";
|
|
671
|
+
}
|
|
672
|
+
catch { }
|
|
673
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
674
|
+
fs.writeFileSync(stamp, String(now));
|
|
675
|
+
const t = new Date(now);
|
|
676
|
+
fs.utimesSync(stamp, t, t);
|
|
677
|
+
const child = (opts.spawn ?? child_process_1.spawn)(process.execPath, [opts.hookEntry ?? path.join(__dirname, "hook.js"), "local-scrub"], {
|
|
678
|
+
detached: true,
|
|
679
|
+
stdio: "ignore",
|
|
680
|
+
env: { ...env, CODIEDEV_LOCAL_SCRUB_CHILD: "1" },
|
|
681
|
+
});
|
|
682
|
+
child.on?.("error", () => { });
|
|
683
|
+
child.unref?.();
|
|
684
|
+
return "spawned";
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return "error";
|
|
688
|
+
}
|
|
689
|
+
}
|