whatileaked 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +12 -2
  2. package/dist/cli.js +257 -71
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -192,8 +192,18 @@ npx whatileaked scan || echo "found something"
192
192
 
193
193
  | Agent | Folder |
194
194
  | --- | --- |
195
- | Claude Code | `~/.claude/projects/` |
196
- | Codex | `~/.codex/sessions/` |
195
+ | Claude Code | `~/.claude/projects/` · `~/.claude/CLAUDE.md` |
196
+ | Codex | `~/.codex/sessions/` · `~/.codex/AGENTS.md` |
197
+ | Cursor | `~/.cursor/projects/*/agent-transcripts/` |
198
+
199
+ Cursor keeps its older Ask-mode chats in SQLite rather than jsonl, and those are
200
+ not scanned. Reading them needs `node:sqlite`, which arrived in Node 22.5, and
201
+ this runs on Node 20. Agent transcripts are covered; Ask history is not.
202
+
203
+ Transcripts record credentials that were already sent. Instruction and memory
204
+ files are worse: they are read again at the start of the next session, so a
205
+ credential in one keeps leaking until the file is edited. Both are scanned, and
206
+ `wipe` rewrites both.
197
207
 
198
208
  Nowhere else. It does not read your repos, your shell history, or your
199
209
  environment. Adding support for another agent is one small file — pull requests
package/dist/cli.js CHANGED
@@ -52,9 +52,20 @@ function home() {
52
52
  }
53
53
 
54
54
  // src/report/paths.ts
55
+ import { realpathSync } from "fs";
55
56
  function shorten(file) {
56
57
  const home2 = process.env.HOME;
57
- return home2 !== void 0 && file.startsWith(home2) ? `~${file.slice(home2.length)}` : file;
58
+ if (home2 === void 0) return file;
59
+ if (file.startsWith(home2)) return `~${file.slice(home2.length)}`;
60
+ const real = realHome(home2);
61
+ return file.startsWith(real) ? `~${file.slice(real.length)}` : file;
62
+ }
63
+ function realHome(home2) {
64
+ try {
65
+ return realpathSync(home2);
66
+ } catch {
67
+ return home2;
68
+ }
58
69
  }
59
70
  function plural(count2, noun) {
60
71
  return `${count2.toLocaleString("en-US")} ${noun}${count2 === 1 ? "" : "s"}`;
@@ -71,12 +82,12 @@ var Progress = class {
71
82
  this.#write = write;
72
83
  this.#enabled = enabled2;
73
84
  }
74
- advance(transcripts, project) {
85
+ advance(files, project) {
75
86
  if (!this.#enabled) return;
76
- if (transcripts % 8 !== 0) return;
87
+ if (files % 8 !== 0) return;
77
88
  const spinner = SPINNER[this.#frame % SPINNER.length] ?? "|";
78
89
  this.#frame++;
79
- this.#write(`\r${CLEAR_LINE} ${spinner} reading ${transcripts} transcripts \u2014 ${clip(project)}`);
90
+ this.#write(`\r${CLEAR_LINE} ${spinner} reading ${files} files \u2014 ${clip(project)}`);
80
91
  }
81
92
  done() {
82
93
  if (this.#enabled) this.#write(`\r${CLEAR_LINE}`);
@@ -86,6 +97,9 @@ function clip(project) {
86
97
  return project.length > 40 ? `${project.slice(0, 39)}\u2026` : project;
87
98
  }
88
99
 
100
+ // src/sources/source.ts
101
+ var FileKind = { transcript: "transcript", memory: "memory" };
102
+
89
103
  // src/report/aggregate.ts
90
104
  function aggregate(findings) {
91
105
  const byRule = /* @__PURE__ */ new Map();
@@ -94,21 +108,22 @@ function aggregate(findings) {
94
108
  totals.set(finding.rule, (totals.get(finding.rule) ?? 0) + 1);
95
109
  const sites = byRule.get(finding.rule) ?? /* @__PURE__ */ new Map();
96
110
  byRule.set(finding.rule, sites);
97
- const key = `${finding.fingerprint}:${finding.project}`;
111
+ const where2 = finding.kind === FileKind.memory ? finding.file : finding.project;
112
+ const key = `${finding.fingerprint}:${where2}:${finding.kind}`;
98
113
  const site = sites.get(key);
99
114
  if (site) {
100
115
  site.occurrences++;
101
- site.firstEntryIndex = Math.min(site.firstEntryIndex, finding.entryIndex);
116
+ site.firstAt = Math.min(site.firstAt, finding.at);
102
117
  continue;
103
118
  }
104
119
  sites.set(key, {
105
120
  fingerprint: finding.fingerprint,
121
+ kind: finding.kind,
106
122
  project: finding.project,
107
- sessionId: finding.sessionId,
108
123
  file: finding.file,
109
124
  context: finding.context,
110
125
  occurrences: 1,
111
- firstEntryIndex: finding.entryIndex
126
+ firstAt: finding.at
112
127
  });
113
128
  }
114
129
  return [...byRule.entries()].map(([rule, sites]) => ({
@@ -135,9 +150,12 @@ var TerminalReporter = class {
135
150
  }
136
151
  report(findings, stats) {
137
152
  const totals = aggregate(findings);
138
- const secrets = totals.reduce((sum, total) => sum + total.distinct, 0);
153
+ const sent = distinct(totals, FileKind.transcript);
154
+ const onDisk = distinct(totals, FileKind.memory);
139
155
  this.#write(
140
- dim(` scanned ${count(stats.transcripts)} transcripts \xB7 ${count(stats.entries)} messages`)
156
+ dim(
157
+ ` scanned ${count(stats.transcripts)} transcripts \xB7 ${count(stats.entries)} messages \xB7 ${count(stats.memoryFiles)} memory files`
158
+ )
141
159
  );
142
160
  this.#write("");
143
161
  if (totals.length === 0) {
@@ -149,16 +167,24 @@ var TerminalReporter = class {
149
167
  for (const total of totals) {
150
168
  this.#write(`${red("*")} ${bold(total.rule)} ${dim(plural(total.distinct, "secret"))}`);
151
169
  for (const site of total.sites) {
152
- const times = site.occurrences === 1 ? "sent once" : `sent ${count(site.occurrences)} times`;
170
+ const times = occurrences(site);
153
171
  this.#write(
154
172
  ` ${yellow(site.fingerprint.slice(0, FINGERPRINT_CHARS))} ${site.project.padEnd(PROJECT_COLUMN)} ${dim(times)}`
155
173
  );
156
174
  if (site.context !== "") this.#write(` ${dim(`${site.context}***`)}`);
157
- this.#write(` ${dim(shorten(site.file))}`);
175
+ this.#write(` ${dim(where(site))}`);
158
176
  }
159
177
  this.#write("");
160
178
  }
161
- this.#write(bold(red(`${plural(secrets, "credential")} sent to a model provider.`)));
179
+ this.#write(bold(red(`${plural(sent, "credential")} sent to a model provider.`)));
180
+ if (onDisk > 0) {
181
+ this.#write(
182
+ bold(red(`${plural(onDisk, "credential")} still on disk, in files your agent reads.`))
183
+ );
184
+ this.#write(
185
+ red("Remove them there as well as rotating, or the next session sends them again.")
186
+ );
187
+ }
162
188
  if (stats.unscannable > 0) {
163
189
  this.#write(
164
190
  yellow(`${plural(stats.unscannable, "message")} could not be scanned and are not counted.`)
@@ -175,6 +201,22 @@ var TerminalReporter = class {
175
201
  function count(value) {
176
202
  return value.toLocaleString("en-US");
177
203
  }
204
+ function occurrences(site) {
205
+ if (site.kind === FileKind.memory) {
206
+ return site.occurrences === 1 ? "on one line" : `on ${count(site.occurrences)} lines`;
207
+ }
208
+ return site.occurrences === 1 ? "sent once" : `sent ${count(site.occurrences)} times`;
209
+ }
210
+ function where(site) {
211
+ const path = shorten(site.file);
212
+ return site.kind === FileKind.memory ? `${path}:${site.firstAt}` : path;
213
+ }
214
+ function distinct(totals, kind) {
215
+ return totals.reduce((sum, total) => {
216
+ const matching = total.sites.filter((site) => site.kind === kind);
217
+ return sum + new Set(matching.map((site) => site.fingerprint)).size;
218
+ }, 0);
219
+ }
178
220
 
179
221
  // src/scan/keyword-index.ts
180
222
  var WINDOW_CHARS = 512;
@@ -2319,16 +2361,36 @@ var SECRET_RULE_SOURCES = [
2319
2361
  }
2320
2362
  ];
2321
2363
 
2322
- // src/transcript/reader.ts
2364
+ // src/memory/reader.ts
2323
2365
  import { createReadStream } from "fs";
2324
2366
  import { createInterface } from "readline";
2367
+ async function* readLines(path) {
2368
+ const stream = createReadStream(path, "utf8");
2369
+ stream.on("error", () => {
2370
+ });
2371
+ const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2372
+ let number = 0;
2373
+ try {
2374
+ for await (const text of lines) {
2375
+ number++;
2376
+ if (text.trim().length === 0) continue;
2377
+ yield { number, text };
2378
+ }
2379
+ } catch {
2380
+ return;
2381
+ }
2382
+ }
2383
+
2384
+ // src/transcript/reader.ts
2385
+ import { createReadStream as createReadStream2 } from "fs";
2386
+ import { createInterface as createInterface2 } from "readline";
2325
2387
  var TranscriptReader = class {
2326
2388
  stats = { entries: 0, skipped: 0 };
2327
2389
  async *read(path) {
2328
- const stream = createReadStream(path, "utf8");
2390
+ const stream = createReadStream2(path, "utf8");
2329
2391
  stream.on("error", () => {
2330
2392
  });
2331
- const lines = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2393
+ const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
2332
2394
  let index = -1;
2333
2395
  try {
2334
2396
  for await (const line of lines) {
@@ -2366,48 +2428,65 @@ var ScanRun = class {
2366
2428
  * re-deriving it from the reporter's output. */
2367
2429
  async run() {
2368
2430
  const findings = [];
2369
- const stats = { transcripts: 0, entries: 0, skipped: 0, unscannable: 0 };
2431
+ const stats = {
2432
+ transcripts: 0,
2433
+ memoryFiles: 0,
2434
+ entries: 0,
2435
+ skipped: 0,
2436
+ unscannable: 0
2437
+ };
2370
2438
  for (const source of this.#sources) {
2371
2439
  for await (const file of source.discover()) {
2372
- stats.transcripts++;
2373
- this.#progress.advance(stats.transcripts, file.project);
2374
- const reader = new TranscriptReader();
2375
- for await (const entry of reader.read(file.path)) {
2376
- let matches = [];
2377
- try {
2378
- matches = this.#scanner.scan(entry.line);
2379
- } catch {
2380
- stats.unscannable++;
2381
- continue;
2440
+ if (file.kind === FileKind.transcript) {
2441
+ stats.transcripts++;
2442
+ const reader = new TranscriptReader();
2443
+ for await (const entry of reader.read(file.path)) {
2444
+ this.#collect(file, entry.line, entry.index, findings, stats);
2382
2445
  }
2383
- for (const match of matches) {
2384
- findings.push({
2385
- rule: match.rule,
2386
- fingerprint: match.fingerprint,
2387
- context: match.context,
2388
- source: file.source,
2389
- project: file.project,
2390
- sessionId: file.sessionId,
2391
- file: file.path,
2392
- entryIndex: entry.index
2393
- });
2446
+ stats.entries += reader.stats.entries;
2447
+ stats.skipped += reader.stats.skipped;
2448
+ } else {
2449
+ stats.memoryFiles++;
2450
+ for await (const line of readLines(file.path)) {
2451
+ this.#collect(file, line.text, line.number, findings, stats);
2394
2452
  }
2395
2453
  }
2396
- stats.entries += reader.stats.entries;
2397
- stats.skipped += reader.stats.skipped;
2454
+ this.#progress.advance(stats.transcripts + stats.memoryFiles, file.project);
2398
2455
  }
2399
2456
  }
2400
2457
  this.#progress.done();
2401
2458
  this.#reporter.report(findings, stats);
2402
2459
  return findings.length;
2403
2460
  }
2461
+ /** One entry the engine chokes on must not cost the other 700,000. */
2462
+ #collect(file, text, at, findings, stats) {
2463
+ let matches = [];
2464
+ try {
2465
+ matches = this.#scanner.scan(text);
2466
+ } catch {
2467
+ stats.unscannable++;
2468
+ return;
2469
+ }
2470
+ for (const match of matches) {
2471
+ findings.push({
2472
+ rule: match.rule,
2473
+ fingerprint: match.fingerprint,
2474
+ context: match.context,
2475
+ source: file.source,
2476
+ kind: file.kind,
2477
+ project: file.project,
2478
+ file: file.path,
2479
+ at
2480
+ });
2481
+ }
2482
+ }
2404
2483
  };
2405
2484
 
2406
2485
  // src/sources/claude-code-source.ts
2407
2486
  import { basename, join as join2 } from "path";
2408
2487
 
2409
2488
  // src/sources/listing.ts
2410
- import { readdir } from "fs/promises";
2489
+ import { readdir, realpath, stat } from "fs/promises";
2411
2490
  import { join } from "path";
2412
2491
  async function listDirectories(root) {
2413
2492
  try {
@@ -2417,14 +2496,23 @@ async function listDirectories(root) {
2417
2496
  return [];
2418
2497
  }
2419
2498
  }
2420
- async function listJsonl(dir) {
2499
+ var FileExtension = { jsonl: ".jsonl", markdown: ".md" };
2500
+ async function listFiles(dir, extension) {
2421
2501
  try {
2422
2502
  const entries = await readdir(dir, { withFileTypes: true });
2423
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name);
2503
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => entry.name);
2424
2504
  } catch {
2425
2505
  return [];
2426
2506
  }
2427
2507
  }
2508
+ async function resolveFile(path) {
2509
+ try {
2510
+ const real = await realpath(path);
2511
+ return (await stat(real)).isFile() ? real : null;
2512
+ } catch {
2513
+ return null;
2514
+ }
2515
+ }
2428
2516
  async function walkDirectories(root) {
2429
2517
  const found = [root];
2430
2518
  for (const name of await listDirectories(root)) {
@@ -2436,7 +2524,8 @@ async function walkDirectories(root) {
2436
2524
  // src/transcript/entry.ts
2437
2525
  var SourceName = {
2438
2526
  "claude-code": "claude-code",
2439
- codex: "codex"
2527
+ codex: "codex",
2528
+ cursor: "cursor"
2440
2529
  };
2441
2530
 
2442
2531
  // src/transcript/shape.ts
@@ -2460,28 +2549,46 @@ var ClaudeCodeSource = class {
2460
2549
  this.#home = home2;
2461
2550
  }
2462
2551
  async *discover() {
2552
+ const instructions = await resolveFile(join2(this.#home, ".claude", "CLAUDE.md"));
2553
+ if (instructions !== null) {
2554
+ yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
2555
+ }
2463
2556
  const root = join2(this.#home, ".claude", "projects");
2464
2557
  for (const slug of await listDirectories(root)) {
2465
2558
  const dir = join2(root, slug);
2466
- for (const file of await listJsonl(dir)) {
2467
- const path = join2(dir, file);
2468
- const meta = await firstMeta(path);
2469
- yield {
2470
- source: this.name,
2471
- path,
2472
- sessionId: meta === null ? basename(file, ".jsonl") : meta.sessionId,
2473
- project: meta === null ? slug : basename(meta.cwd)
2474
- };
2559
+ let project = slug;
2560
+ for (const nested of await walkDirectories(dir)) {
2561
+ for (const file of await listFiles(nested, FileExtension.jsonl)) {
2562
+ const path = join2(nested, file);
2563
+ const cwd = await firstCwd(path);
2564
+ if (cwd !== null) project = basename(cwd);
2565
+ yield {
2566
+ source: this.name,
2567
+ kind: FileKind.transcript,
2568
+ path,
2569
+ project: cwd === null ? slug : basename(cwd)
2570
+ };
2571
+ }
2572
+ }
2573
+ for (const nested of await walkDirectories(join2(dir, "memory"))) {
2574
+ for (const file of await listFiles(nested, FileExtension.markdown)) {
2575
+ yield {
2576
+ source: this.name,
2577
+ kind: FileKind.memory,
2578
+ path: join2(nested, file),
2579
+ project
2580
+ };
2581
+ }
2475
2582
  }
2476
2583
  }
2477
2584
  }
2478
2585
  };
2479
- async function firstMeta(path) {
2586
+ async function firstCwd(path) {
2480
2587
  const reader = new TranscriptReader();
2481
2588
  for await (const entry of reader.read(path)) {
2482
2589
  const sessionId = stringField(entry.payload, "sessionId");
2483
2590
  const cwd = stringField(entry.payload, "cwd");
2484
- if (sessionId !== null && cwd !== null) return { sessionId, cwd };
2591
+ if (sessionId !== null && cwd !== null) return cwd;
2485
2592
  }
2486
2593
  return null;
2487
2594
  }
@@ -2495,40 +2602,99 @@ var CodexSource = class {
2495
2602
  this.#home = home2;
2496
2603
  }
2497
2604
  async *discover() {
2605
+ const instructions = await resolveFile(join3(this.#home, ".codex", "AGENTS.md"));
2606
+ if (instructions !== null) {
2607
+ yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
2608
+ }
2498
2609
  const root = join3(this.#home, ".codex", "sessions");
2499
2610
  for (const dir of await walkDirectories(root)) {
2500
- for (const file of await listJsonl(dir)) {
2611
+ for (const file of await listFiles(dir, FileExtension.jsonl)) {
2501
2612
  const path = join3(dir, file);
2502
- const meta = await firstMeta2(path);
2613
+ const cwd = await firstCwd2(path);
2503
2614
  yield {
2504
2615
  source: this.name,
2616
+ kind: FileKind.transcript,
2505
2617
  path,
2506
- sessionId: meta === null ? basename2(file, ".jsonl") : meta.id,
2507
- project: meta === null ? basename2(dir) : basename2(meta.cwd)
2618
+ project: cwd === null ? basename2(dir) : basename2(cwd)
2508
2619
  };
2509
2620
  }
2510
2621
  }
2511
2622
  }
2512
2623
  };
2513
- async function firstMeta2(path) {
2624
+ async function firstCwd2(path) {
2514
2625
  const reader = new TranscriptReader();
2515
2626
  for await (const entry of reader.read(path)) {
2516
2627
  if (stringField(entry.payload, "type") !== "session_meta") continue;
2517
2628
  const payload = objectField(entry.payload, "payload");
2518
2629
  const id = stringField(payload, "id");
2519
2630
  const cwd = stringField(payload, "cwd");
2520
- if (id !== null && cwd !== null) return { id, cwd };
2631
+ if (id !== null && cwd !== null) return cwd;
2521
2632
  }
2522
2633
  return null;
2523
2634
  }
2524
2635
 
2636
+ // src/sources/cursor-source.ts
2637
+ import { stat as stat2 } from "fs/promises";
2638
+ import { basename as basename3, join as join4 } from "path";
2639
+ var CursorSource = class {
2640
+ name = SourceName.cursor;
2641
+ #home;
2642
+ constructor(home2) {
2643
+ this.#home = home2;
2644
+ }
2645
+ async *discover() {
2646
+ const root = join4(this.#home, ".cursor", "projects");
2647
+ for (const slug of await listDirectories(root)) {
2648
+ const project = await projectName(slug);
2649
+ for (const dir of await walkDirectories(join4(root, slug, "agent-transcripts"))) {
2650
+ for (const file of await listFiles(dir, FileExtension.jsonl)) {
2651
+ yield {
2652
+ source: this.name,
2653
+ kind: FileKind.transcript,
2654
+ path: join4(dir, file),
2655
+ project
2656
+ };
2657
+ }
2658
+ }
2659
+ }
2660
+ }
2661
+ };
2662
+ async function projectName(slug) {
2663
+ const path = await resolveSlug(slug);
2664
+ return path === null ? slug : basename3(path);
2665
+ }
2666
+ async function resolveSlug(slug) {
2667
+ const parts = slug.split("-");
2668
+ let path = "";
2669
+ let index = 0;
2670
+ while (index < parts.length) {
2671
+ let end = index;
2672
+ let candidate = `${path}/${parts[index]}`;
2673
+ while (!await isDirectory(candidate) && end + 1 < parts.length) {
2674
+ end++;
2675
+ candidate = `${candidate}-${parts[end]}`;
2676
+ }
2677
+ if (!await isDirectory(candidate)) return null;
2678
+ path = candidate;
2679
+ index = end + 1;
2680
+ }
2681
+ return path === "" ? null : path;
2682
+ }
2683
+ async function isDirectory(path) {
2684
+ try {
2685
+ return (await stat2(path)).isDirectory();
2686
+ } catch {
2687
+ return false;
2688
+ }
2689
+ }
2690
+
2525
2691
  // src/wipe/confirm.ts
2526
- import { createInterface as createInterface2 } from "readline/promises";
2692
+ import { createInterface as createInterface3 } from "readline/promises";
2527
2693
  var CONFIRMATION = "wipe";
2528
2694
  var TerminalPrompt = class {
2529
2695
  async ask(question) {
2530
2696
  if (process.stdin.isTTY !== true) return "";
2531
- const readline = createInterface2({ input: process.stdin, output: process.stdout });
2697
+ const readline = createInterface3({ input: process.stdin, output: process.stdout });
2532
2698
  try {
2533
2699
  return (await readline.question(question)).trim();
2534
2700
  } finally {
@@ -2538,7 +2704,7 @@ var TerminalPrompt = class {
2538
2704
  };
2539
2705
 
2540
2706
  // src/wipe/wipe-run.ts
2541
- import { readFile, rename, writeFile } from "fs/promises";
2707
+ import { readFile, rename, stat as stat3, writeFile } from "fs/promises";
2542
2708
 
2543
2709
  // src/wipe/redact.ts
2544
2710
  function placeholder(rule) {
@@ -2588,20 +2754,31 @@ var WipeRun = class {
2588
2754
  const plans = [];
2589
2755
  for (const source of this.#sources) {
2590
2756
  for await (const file of source.discover()) {
2591
- const reader = new TranscriptReader();
2592
2757
  let lines = 0;
2593
2758
  let secrets = 0;
2594
- for await (const entry of reader.read(file.path)) {
2595
- const found = this.#scanner.secretsOf(entry.line);
2759
+ for await (const text of this.#textOf(file)) {
2760
+ const found = this.#scanner.secretsOf(text);
2596
2761
  if (found.size === 0) continue;
2597
2762
  lines++;
2598
2763
  secrets += found.size;
2599
2764
  }
2600
- if (lines > 0) plans.push({ file: file.path, project: file.project, lines, secrets });
2765
+ if (lines > 0) {
2766
+ plans.push({ file: file.path, project: file.project, kind: file.kind, lines, secrets });
2767
+ }
2601
2768
  }
2602
2769
  }
2603
2770
  return plans;
2604
2771
  }
2772
+ /** The rewrite below is textual and kind-blind; only counting needs to know
2773
+ * which reader gets the file. */
2774
+ async *#textOf(file) {
2775
+ if (file.kind === FileKind.memory) {
2776
+ for await (const line of readLines(file.path)) yield line.text;
2777
+ return;
2778
+ }
2779
+ const reader = new TranscriptReader();
2780
+ for await (const entry of reader.read(file.path)) yield entry.line;
2781
+ }
2605
2782
  /** Rewrites through a temporary file and a rename, so an interrupted run
2606
2783
  * leaves the original intact rather than half a transcript. */
2607
2784
  async #rewrite(file) {
@@ -2615,8 +2792,9 @@ var WipeRun = class {
2615
2792
  redacted += secrets.size;
2616
2793
  return redactLine(line, secrets);
2617
2794
  });
2795
+ const { mode } = await stat3(file);
2618
2796
  const temporary = `${file}.whatileaked-tmp`;
2619
- await writeFile(temporary, rewritten.join("\n"), "utf8");
2797
+ await writeFile(temporary, rewritten.join("\n"), { encoding: "utf8", mode });
2620
2798
  await rename(temporary, file);
2621
2799
  return redacted;
2622
2800
  }
@@ -2626,8 +2804,9 @@ var WipeRun = class {
2626
2804
  this.#write(dim(` ${plural(secrets, "secret")} across ${plural(plans.length, "file")}`));
2627
2805
  this.#write("");
2628
2806
  for (const plan of plans) {
2807
+ const unit = plan.kind === FileKind.memory ? "line" : "message";
2629
2808
  this.#write(
2630
- ` ${yellow("*")} ${bold(plan.project.padEnd(project))} ${dim(`${plural(plan.secrets, "secret")} in ${plural(plan.lines, "message")}`)}`
2809
+ ` ${yellow("*")} ${bold(plan.project.padEnd(project))} ${dim(`${plural(plan.secrets, "secret")} in ${plural(plan.lines, unit)}`)}`
2631
2810
  );
2632
2811
  this.#write(` ${dim(shorten(plan.file))}`);
2633
2812
  }
@@ -2653,7 +2832,11 @@ async function main(argv) {
2653
2832
  for (const line of home()) write(line);
2654
2833
  return 0;
2655
2834
  }
2656
- const sources = [new ClaudeCodeSource(homedir()), new CodexSource(homedir())];
2835
+ const sources = [
2836
+ new ClaudeCodeSource(homedir()),
2837
+ new CodexSource(homedir()),
2838
+ new CursorSource(homedir())
2839
+ ];
2657
2840
  const scanner = new Scanner(new KeywordIndex(compileRules(SECRET_RULE_SOURCES)));
2658
2841
  return command === Command.wipe ? await wipe(sources, scanner, write) : await scan(sources, scanner, write);
2659
2842
  }
@@ -2661,6 +2844,9 @@ function colorful() {
2661
2844
  return process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
2662
2845
  }
2663
2846
  async function scan(sources, scanner, write) {
2847
+ write(` ${dim("Welcome to whatileaked \u2014 built by Selan")} ${cyan("(selan.ai)")}`);
2848
+ write(dim(" checking what you have already sent to a model provider"));
2849
+ write("");
2664
2850
  const progress = new Progress((text) => process.stderr.write(text), colorful());
2665
2851
  return await new ScanRun(sources, scanner, new TerminalReporter(write), progress).run();
2666
2852
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whatileaked",
3
- "version": "0.1.0",
4
- "description": "Scan your local Claude Code and Codex transcripts for credentials you already sent to a model provider.",
3
+ "version": "0.3.0",
4
+ "description": "Scan your local Claude Code, Codex and Cursor transcripts for credentials you already sent to a model provider.",
5
5
  "type": "module",
6
6
  "imports": {
7
7
  "#scripts/*": "./scripts/*.ts",
@@ -21,6 +21,7 @@
21
21
  "scripts": {
22
22
  "dev": "node src/cli.ts",
23
23
  "build": "tsc --noEmit && tsup",
24
+ "prepublishOnly": "npm run lint && npm test && npm run build",
24
25
  "typecheck": "tsc --noEmit",
25
26
  "test": "node --test \"src/**/*.test.ts\"",
26
27
  "format": "biome check --write",