whatileaked 0.1.0 → 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/README.md +7 -2
- package/dist/cli.js +188 -64
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -192,8 +192,13 @@ 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
|
+
|
|
198
|
+
Transcripts record credentials that were already sent. Instruction and memory
|
|
199
|
+
files are worse: they are read again at the start of the next session, so a
|
|
200
|
+
credential in one keeps leaking until the file is edited. Both are scanned, and
|
|
201
|
+
`wipe` rewrites both.
|
|
197
202
|
|
|
198
203
|
Nowhere else. It does not read your repos, your shell history, or your
|
|
199
204
|
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
|
-
|
|
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(
|
|
85
|
+
advance(files, project) {
|
|
75
86
|
if (!this.#enabled) return;
|
|
76
|
-
if (
|
|
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 ${
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
153
|
+
const sent = distinct(totals, FileKind.transcript);
|
|
154
|
+
const onDisk = distinct(totals, FileKind.memory);
|
|
139
155
|
this.#write(
|
|
140
|
-
dim(
|
|
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 =
|
|
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(
|
|
175
|
+
this.#write(` ${dim(where(site))}`);
|
|
158
176
|
}
|
|
159
177
|
this.#write("");
|
|
160
178
|
}
|
|
161
|
-
this.#write(bold(red(`${plural(
|
|
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/
|
|
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 =
|
|
2390
|
+
const stream = createReadStream2(path, "utf8");
|
|
2329
2391
|
stream.on("error", () => {
|
|
2330
2392
|
});
|
|
2331
|
-
const lines =
|
|
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 = {
|
|
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
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
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
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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)) {
|
|
@@ -2460,28 +2548,44 @@ var ClaudeCodeSource = class {
|
|
|
2460
2548
|
this.#home = home2;
|
|
2461
2549
|
}
|
|
2462
2550
|
async *discover() {
|
|
2551
|
+
const instructions = await resolveFile(join2(this.#home, ".claude", "CLAUDE.md"));
|
|
2552
|
+
if (instructions !== null) {
|
|
2553
|
+
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2554
|
+
}
|
|
2463
2555
|
const root = join2(this.#home, ".claude", "projects");
|
|
2464
2556
|
for (const slug of await listDirectories(root)) {
|
|
2465
2557
|
const dir = join2(root, slug);
|
|
2466
|
-
|
|
2558
|
+
let project = slug;
|
|
2559
|
+
for (const file of await listFiles(dir, FileExtension.jsonl)) {
|
|
2467
2560
|
const path = join2(dir, file);
|
|
2468
|
-
const
|
|
2561
|
+
const cwd = await firstCwd(path);
|
|
2562
|
+
if (cwd !== null) project = basename(cwd);
|
|
2469
2563
|
yield {
|
|
2470
2564
|
source: this.name,
|
|
2565
|
+
kind: FileKind.transcript,
|
|
2471
2566
|
path,
|
|
2472
|
-
|
|
2473
|
-
project: meta === null ? slug : basename(meta.cwd)
|
|
2567
|
+
project: cwd === null ? slug : basename(cwd)
|
|
2474
2568
|
};
|
|
2475
2569
|
}
|
|
2570
|
+
for (const nested of await walkDirectories(join2(dir, "memory"))) {
|
|
2571
|
+
for (const file of await listFiles(nested, FileExtension.markdown)) {
|
|
2572
|
+
yield {
|
|
2573
|
+
source: this.name,
|
|
2574
|
+
kind: FileKind.memory,
|
|
2575
|
+
path: join2(nested, file),
|
|
2576
|
+
project
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2476
2580
|
}
|
|
2477
2581
|
}
|
|
2478
2582
|
};
|
|
2479
|
-
async function
|
|
2583
|
+
async function firstCwd(path) {
|
|
2480
2584
|
const reader = new TranscriptReader();
|
|
2481
2585
|
for await (const entry of reader.read(path)) {
|
|
2482
2586
|
const sessionId = stringField(entry.payload, "sessionId");
|
|
2483
2587
|
const cwd = stringField(entry.payload, "cwd");
|
|
2484
|
-
if (sessionId !== null && cwd !== null) return
|
|
2588
|
+
if (sessionId !== null && cwd !== null) return cwd;
|
|
2485
2589
|
}
|
|
2486
2590
|
return null;
|
|
2487
2591
|
}
|
|
@@ -2495,40 +2599,44 @@ var CodexSource = class {
|
|
|
2495
2599
|
this.#home = home2;
|
|
2496
2600
|
}
|
|
2497
2601
|
async *discover() {
|
|
2602
|
+
const instructions = await resolveFile(join3(this.#home, ".codex", "AGENTS.md"));
|
|
2603
|
+
if (instructions !== null) {
|
|
2604
|
+
yield { source: this.name, kind: FileKind.memory, path: instructions, project: this.name };
|
|
2605
|
+
}
|
|
2498
2606
|
const root = join3(this.#home, ".codex", "sessions");
|
|
2499
2607
|
for (const dir of await walkDirectories(root)) {
|
|
2500
|
-
for (const file of await
|
|
2608
|
+
for (const file of await listFiles(dir, FileExtension.jsonl)) {
|
|
2501
2609
|
const path = join3(dir, file);
|
|
2502
|
-
const
|
|
2610
|
+
const cwd = await firstCwd2(path);
|
|
2503
2611
|
yield {
|
|
2504
2612
|
source: this.name,
|
|
2613
|
+
kind: FileKind.transcript,
|
|
2505
2614
|
path,
|
|
2506
|
-
|
|
2507
|
-
project: meta === null ? basename2(dir) : basename2(meta.cwd)
|
|
2615
|
+
project: cwd === null ? basename2(dir) : basename2(cwd)
|
|
2508
2616
|
};
|
|
2509
2617
|
}
|
|
2510
2618
|
}
|
|
2511
2619
|
}
|
|
2512
2620
|
};
|
|
2513
|
-
async function
|
|
2621
|
+
async function firstCwd2(path) {
|
|
2514
2622
|
const reader = new TranscriptReader();
|
|
2515
2623
|
for await (const entry of reader.read(path)) {
|
|
2516
2624
|
if (stringField(entry.payload, "type") !== "session_meta") continue;
|
|
2517
2625
|
const payload = objectField(entry.payload, "payload");
|
|
2518
2626
|
const id = stringField(payload, "id");
|
|
2519
2627
|
const cwd = stringField(payload, "cwd");
|
|
2520
|
-
if (id !== null && cwd !== null) return
|
|
2628
|
+
if (id !== null && cwd !== null) return cwd;
|
|
2521
2629
|
}
|
|
2522
2630
|
return null;
|
|
2523
2631
|
}
|
|
2524
2632
|
|
|
2525
2633
|
// src/wipe/confirm.ts
|
|
2526
|
-
import { createInterface as
|
|
2634
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
2527
2635
|
var CONFIRMATION = "wipe";
|
|
2528
2636
|
var TerminalPrompt = class {
|
|
2529
2637
|
async ask(question) {
|
|
2530
2638
|
if (process.stdin.isTTY !== true) return "";
|
|
2531
|
-
const readline =
|
|
2639
|
+
const readline = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2532
2640
|
try {
|
|
2533
2641
|
return (await readline.question(question)).trim();
|
|
2534
2642
|
} finally {
|
|
@@ -2538,7 +2646,7 @@ var TerminalPrompt = class {
|
|
|
2538
2646
|
};
|
|
2539
2647
|
|
|
2540
2648
|
// src/wipe/wipe-run.ts
|
|
2541
|
-
import { readFile, rename, writeFile } from "fs/promises";
|
|
2649
|
+
import { readFile, rename, stat as stat2, writeFile } from "fs/promises";
|
|
2542
2650
|
|
|
2543
2651
|
// src/wipe/redact.ts
|
|
2544
2652
|
function placeholder(rule) {
|
|
@@ -2588,20 +2696,31 @@ var WipeRun = class {
|
|
|
2588
2696
|
const plans = [];
|
|
2589
2697
|
for (const source of this.#sources) {
|
|
2590
2698
|
for await (const file of source.discover()) {
|
|
2591
|
-
const reader = new TranscriptReader();
|
|
2592
2699
|
let lines = 0;
|
|
2593
2700
|
let secrets = 0;
|
|
2594
|
-
for await (const
|
|
2595
|
-
const found = this.#scanner.secretsOf(
|
|
2701
|
+
for await (const text of this.#textOf(file)) {
|
|
2702
|
+
const found = this.#scanner.secretsOf(text);
|
|
2596
2703
|
if (found.size === 0) continue;
|
|
2597
2704
|
lines++;
|
|
2598
2705
|
secrets += found.size;
|
|
2599
2706
|
}
|
|
2600
|
-
if (lines > 0)
|
|
2707
|
+
if (lines > 0) {
|
|
2708
|
+
plans.push({ file: file.path, project: file.project, kind: file.kind, lines, secrets });
|
|
2709
|
+
}
|
|
2601
2710
|
}
|
|
2602
2711
|
}
|
|
2603
2712
|
return plans;
|
|
2604
2713
|
}
|
|
2714
|
+
/** The rewrite below is textual and kind-blind; only counting needs to know
|
|
2715
|
+
* which reader gets the file. */
|
|
2716
|
+
async *#textOf(file) {
|
|
2717
|
+
if (file.kind === FileKind.memory) {
|
|
2718
|
+
for await (const line of readLines(file.path)) yield line.text;
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const reader = new TranscriptReader();
|
|
2722
|
+
for await (const entry of reader.read(file.path)) yield entry.line;
|
|
2723
|
+
}
|
|
2605
2724
|
/** Rewrites through a temporary file and a rename, so an interrupted run
|
|
2606
2725
|
* leaves the original intact rather than half a transcript. */
|
|
2607
2726
|
async #rewrite(file) {
|
|
@@ -2615,8 +2734,9 @@ var WipeRun = class {
|
|
|
2615
2734
|
redacted += secrets.size;
|
|
2616
2735
|
return redactLine(line, secrets);
|
|
2617
2736
|
});
|
|
2737
|
+
const { mode } = await stat2(file);
|
|
2618
2738
|
const temporary = `${file}.whatileaked-tmp`;
|
|
2619
|
-
await writeFile(temporary, rewritten.join("\n"), "utf8");
|
|
2739
|
+
await writeFile(temporary, rewritten.join("\n"), { encoding: "utf8", mode });
|
|
2620
2740
|
await rename(temporary, file);
|
|
2621
2741
|
return redacted;
|
|
2622
2742
|
}
|
|
@@ -2626,8 +2746,9 @@ var WipeRun = class {
|
|
|
2626
2746
|
this.#write(dim(` ${plural(secrets, "secret")} across ${plural(plans.length, "file")}`));
|
|
2627
2747
|
this.#write("");
|
|
2628
2748
|
for (const plan of plans) {
|
|
2749
|
+
const unit = plan.kind === FileKind.memory ? "line" : "message";
|
|
2629
2750
|
this.#write(
|
|
2630
|
-
` ${yellow("*")} ${bold(plan.project.padEnd(project))} ${dim(`${plural(plan.secrets, "secret")} in ${plural(plan.lines,
|
|
2751
|
+
` ${yellow("*")} ${bold(plan.project.padEnd(project))} ${dim(`${plural(plan.secrets, "secret")} in ${plural(plan.lines, unit)}`)}`
|
|
2631
2752
|
);
|
|
2632
2753
|
this.#write(` ${dim(shorten(plan.file))}`);
|
|
2633
2754
|
}
|
|
@@ -2661,6 +2782,9 @@ function colorful() {
|
|
|
2661
2782
|
return process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
|
|
2662
2783
|
}
|
|
2663
2784
|
async function scan(sources, scanner, write) {
|
|
2785
|
+
write(` ${dim("Welcome to whatileaked \u2014 built by Selan")} ${cyan("(selan.ai)")}`);
|
|
2786
|
+
write(dim(" checking what you have already sent to a model provider"));
|
|
2787
|
+
write("");
|
|
2664
2788
|
const progress = new Progress((text) => process.stderr.write(text), colorful());
|
|
2665
2789
|
return await new ScanRun(sources, scanner, new TerminalReporter(write), progress).run();
|
|
2666
2790
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whatileaked",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scan your local Claude Code and Codex transcripts for credentials you already sent to a model provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
@@ -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",
|