blackbrake 0.1.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 +202 -0
- package/NOTICE +4 -0
- package/README.md +123 -0
- package/SECURITY.md +22 -0
- package/THIRD_PARTY_NOTICES +36 -0
- package/bin/blackbrake.mjs +163 -0
- package/package.json +47 -0
- package/src/cost/analyzer.mjs +114 -0
- package/src/cost/prices.mjs +26 -0
- package/src/load/inventory.mjs +184 -0
- package/src/run.mjs +13 -0
- package/src/secrets/audit.mjs +86 -0
- package/src/secrets/context.mjs +59 -0
- package/src/secrets/engine.mjs +121 -0
- package/src/transcripts.mjs +87 -0
- package/vendor/gitleaks.rules.json +4922 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Reads Claude Code session transcripts (~/.claude/projects/**/*.jsonl) as a stream of text
|
|
2
|
+
// fragments, each labelled with where it came from. Read-only: nothing here writes to disk.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
|
|
8
|
+
export const defaultRoot = () => path.join(os.homedir(), '.claude', 'projects');
|
|
9
|
+
|
|
10
|
+
export function listTranscripts(root) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const walk = (dir) => {
|
|
13
|
+
let entries;
|
|
14
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
15
|
+
for (const e of entries) {
|
|
16
|
+
const full = path.join(dir, e.name);
|
|
17
|
+
if (e.isDirectory()) walk(full);
|
|
18
|
+
else if (e.isFile() && e.name.endsWith('.jsonl')) out.push(full);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
walk(root);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Messages that arrive with role "user" but were written by the harness, not the person.
|
|
26
|
+
// Counting them as the user's words was a measured error in this project's own research.
|
|
27
|
+
const HARNESS_TEXT = /^(Another Claude session sent a message|\[Subagent hand-back\]|Base directory for this skill:|Caveat:|<)/;
|
|
28
|
+
export const isHarnessText = (text) => HARNESS_TEXT.test(text.trimStart());
|
|
29
|
+
|
|
30
|
+
export function describeFile(root, file) {
|
|
31
|
+
const rel = path.relative(root, file).split(path.sep);
|
|
32
|
+
const isSubagent = rel.includes('subagents');
|
|
33
|
+
const session = isSubagent ? rel[rel.indexOf('subagents') - 1] : path.basename(file, '.jsonl');
|
|
34
|
+
return { project: rel[0], session, isSubagent };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Yields { kind, tool, text, ts, lineNo } for every string in a record.
|
|
38
|
+
// kind: user | harness | assistant | tool-input | tool-output | snapshot | other
|
|
39
|
+
export function* fragmentsOf(record, toolNames) {
|
|
40
|
+
const ts = record.timestamp ?? null;
|
|
41
|
+
const msg = record.message;
|
|
42
|
+
if (record.type === 'file-history-snapshot') {
|
|
43
|
+
for (const text of strings(record)) yield { kind: 'snapshot', tool: null, text, ts };
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (msg && typeof msg === 'object') {
|
|
47
|
+
const blocks = typeof msg.content === 'string' ? [{ type: 'text', text: msg.content }] : Array.isArray(msg.content) ? msg.content : [];
|
|
48
|
+
for (const b of blocks) {
|
|
49
|
+
if (!b || typeof b !== 'object') continue;
|
|
50
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
51
|
+
const kind = msg.role === 'user' ? (HARNESS_TEXT.test(b.text.trimStart()) ? 'harness' : 'user') : 'assistant';
|
|
52
|
+
yield { kind, tool: null, text: b.text, ts };
|
|
53
|
+
} else if (b.type === 'tool_use') {
|
|
54
|
+
const filePath = b.input?.file_path ?? b.input?.path ?? b.input?.notebook_path ?? null;
|
|
55
|
+
if (b.id) toolNames.set(b.id, { name: b.name ?? null, filePath });
|
|
56
|
+
for (const text of strings(b.input)) yield { kind: 'tool-input', tool: b.name ?? null, filePath, text, ts };
|
|
57
|
+
} else if (b.type === 'tool_result') {
|
|
58
|
+
const call = toolNames.get(b.tool_use_id);
|
|
59
|
+
for (const text of strings(b.content)) yield { kind: 'tool-output', tool: call?.name ?? null, filePath: call?.filePath ?? null, text, ts };
|
|
60
|
+
} else if (b.type === 'thinking' && typeof b.thinking === 'string') {
|
|
61
|
+
yield { kind: 'assistant', tool: null, text: b.thinking, ts };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Everything else in the record (e.g. toolUseResult mirrors) is still scanned, so nothing
|
|
66
|
+
// rides along unseen, but it is labelled separately.
|
|
67
|
+
const { message: _m, ...rest } = record;
|
|
68
|
+
for (const text of strings(rest)) yield { kind: 'other', tool: null, text, ts };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function* strings(value) {
|
|
72
|
+
if (typeof value === 'string') { if (value.length >= 8) yield value; return; }
|
|
73
|
+
if (Array.isArray(value)) { for (const v of value) yield* strings(v); return; }
|
|
74
|
+
if (value && typeof value === 'object') for (const v of Object.values(value)) yield* strings(v);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function* readTranscript(file) {
|
|
78
|
+
const rl = readline.createInterface({ input: fs.createReadStream(file, { encoding: 'utf8' }), crlfDelay: Infinity });
|
|
79
|
+
let lineNo = 0;
|
|
80
|
+
for await (const line of rl) {
|
|
81
|
+
lineNo++;
|
|
82
|
+
if (!line) continue;
|
|
83
|
+
let record;
|
|
84
|
+
try { record = JSON.parse(line); } catch { continue; }
|
|
85
|
+
yield { record, lineNo };
|
|
86
|
+
}
|
|
87
|
+
}
|