@orquentra/codebuddy 0.0.0-stage → 2.4.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/ARCHITECTURE.md +269 -0
- package/CLAUDE_CODEX.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1118 -2
- package/WORKFLOW.md +152 -0
- package/dist/chunk-3ZELDCR3.js +204 -0
- package/dist/chunk-3ZELDCR3.js.map +1 -0
- package/dist/chunk-5OWZVZSO.js +476 -0
- package/dist/chunk-5OWZVZSO.js.map +1 -0
- package/dist/chunk-DSN5CEHO.js +175 -0
- package/dist/chunk-DSN5CEHO.js.map +1 -0
- package/dist/chunk-GDJH7XJC.js +119 -0
- package/dist/chunk-GDJH7XJC.js.map +1 -0
- package/dist/chunk-IBMJYIEP.js +1578 -0
- package/dist/chunk-IBMJYIEP.js.map +1 -0
- package/dist/chunk-IE2OUO54.js +199 -0
- package/dist/chunk-IE2OUO54.js.map +1 -0
- package/dist/chunk-IHQM2ZDV.js +19 -0
- package/dist/chunk-IHQM2ZDV.js.map +1 -0
- package/dist/chunk-ITGRANYW.js +63 -0
- package/dist/chunk-ITGRANYW.js.map +1 -0
- package/dist/chunk-K63EOA4B.js +416 -0
- package/dist/chunk-K63EOA4B.js.map +1 -0
- package/dist/chunk-PQK3GKZH.js +98 -0
- package/dist/chunk-PQK3GKZH.js.map +1 -0
- package/dist/chunk-PYLH4MH5.js +34 -0
- package/dist/chunk-PYLH4MH5.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-SYHB3AMM.js +8580 -0
- package/dist/chunk-SYHB3AMM.js.map +1 -0
- package/dist/chunk-TI5TQD6D.js +324 -0
- package/dist/chunk-TI5TQD6D.js.map +1 -0
- package/dist/chunk-WHZSVY2L.js +144 -0
- package/dist/chunk-WHZSVY2L.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +15 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/codebuddy-D2xJ6hFk.d.ts +841 -0
- package/dist/config-S2TULOMK.js +11 -0
- package/dist/config-S2TULOMK.js.map +1 -0
- package/dist/index.d.ts +2527 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/langgraph/index.d.ts +53 -0
- package/dist/langgraph/index.js +143 -0
- package/dist/langgraph/index.js.map +1 -0
- package/dist/lean-command-D6TZJEFX.js +13 -0
- package/dist/lean-command-D6TZJEFX.js.map +1 -0
- package/dist/main-ZZ3EQLYA.js +4575 -0
- package/dist/main-ZZ3EQLYA.js.map +1 -0
- package/dist/memory-NIBFAMY4.js +57 -0
- package/dist/memory-NIBFAMY4.js.map +1 -0
- package/dist/pretool-command-MQQ2UNQM.js +10 -0
- package/dist/pretool-command-MQQ2UNQM.js.map +1 -0
- package/dist/rerank-Q7MIFNKA.js +60 -0
- package/dist/rerank-Q7MIFNKA.js.map +1 -0
- package/dist/summaries-TSE44KUQ.js +18 -0
- package/dist/summaries-TSE44KUQ.js.map +1 -0
- package/dist/zvec-F27AR357.js +23 -0
- package/dist/zvec-F27AR357.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/examples/claude-code/README.md +67 -0
- package/examples/claude-desktop/README.md +54 -0
- package/examples/claude-desktop/claude_desktop_config.json +13 -0
- package/examples/langgraph-agent/README.md +32 -0
- package/examples/langgraph-agent/index.ts +71 -0
- package/examples/multi-agent-handoff/README.md +35 -0
- package/examples/multi-agent-handoff/index.ts +68 -0
- package/package.json +108 -4
- package/src/db/migrations/0000_cultured_satana.sql +144 -0
- package/src/db/migrations/0001_embedding_lease.sql +3 -0
- package/src/db/migrations/0002_embedding_lease_token.sql +1 -0
- package/src/db/migrations/0003_embedding_space.sql +9 -0
- package/src/db/migrations/0004_markdown_reconcile.sql +5 -0
- package/src/db/migrations/0005_fact_propositions.sql +1 -0
- package/src/db/migrations/meta/0000_snapshot.json +1169 -0
- package/src/db/migrations/meta/0002_snapshot.json +1181 -0
- package/src/db/migrations/meta/0003_snapshot.json +1214 -0
- package/src/db/migrations/meta/0004_snapshot.json +1225 -0
- package/src/db/migrations/meta/0005_snapshot.json +1231 -0
- package/src/db/migrations/meta/_journal.json +48 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
failSoft,
|
|
3
|
+
readHookInput,
|
|
4
|
+
writeStdout
|
|
5
|
+
} from "./chunk-PYLH4MH5.js";
|
|
6
|
+
import {
|
|
7
|
+
SessionCapsuleLedger,
|
|
8
|
+
detectTestCommand,
|
|
9
|
+
estimateTokens,
|
|
10
|
+
findCoveringTest
|
|
11
|
+
} from "./chunk-5OWZVZSO.js";
|
|
12
|
+
import {
|
|
13
|
+
NON_SOURCE_PATH,
|
|
14
|
+
discoverTaskPaths,
|
|
15
|
+
loadFreshIndex,
|
|
16
|
+
loadProjectArchitectureMap,
|
|
17
|
+
readBoundedSource,
|
|
18
|
+
taskTerms
|
|
19
|
+
} from "./chunk-IBMJYIEP.js";
|
|
20
|
+
|
|
21
|
+
// src/hooks/lean.ts
|
|
22
|
+
import { createHash } from "crypto";
|
|
23
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
24
|
+
import { extname, join, resolve } from "path";
|
|
25
|
+
var LEAN_POINTER_PREFIX = "CodeBuddy likely files: ";
|
|
26
|
+
var LEAN_LIMITS = {
|
|
27
|
+
paths: 6,
|
|
28
|
+
symbolsPerFile: 3,
|
|
29
|
+
wholeFileLines: 80,
|
|
30
|
+
spanTokens: 1200
|
|
31
|
+
};
|
|
32
|
+
var LEAN_HOOK_BUDGET_MS = 1500;
|
|
33
|
+
var RENDER_RESERVE_MS = 300;
|
|
34
|
+
async function runLeanHook(input, emit, budgetMs = LEAN_HOOK_BUDGET_MS) {
|
|
35
|
+
const started = Date.now();
|
|
36
|
+
const repositoryRoot = resolve(input.cwd ?? process.cwd());
|
|
37
|
+
const prompt = input.prompt?.trim() ?? "";
|
|
38
|
+
if (prompt === "" || prompt.startsWith("/")) return "";
|
|
39
|
+
const sessionId = input.session_id ?? "default";
|
|
40
|
+
try {
|
|
41
|
+
const { manifest } = await loadFreshIndex(repositoryRoot);
|
|
42
|
+
const discovery = await discoverTaskPaths(
|
|
43
|
+
repositoryRoot,
|
|
44
|
+
prompt,
|
|
45
|
+
manifest,
|
|
46
|
+
loadProjectArchitectureMap(repositoryRoot, manifest),
|
|
47
|
+
// The rerank must answer while rendering still fits in the hook budget.
|
|
48
|
+
{ semantic: false, llmDeadlineAt: started + budgetMs - RENDER_RESERVE_MS }
|
|
49
|
+
);
|
|
50
|
+
const terms = taskTerms(prompt);
|
|
51
|
+
const indexed = new Set(Object.keys(manifest.entries));
|
|
52
|
+
const pointers = discovery.paths.filter((path) => !NON_SOURCE_PATH.test(path)).map((path) => {
|
|
53
|
+
const test = findCoveringTest(path, indexed);
|
|
54
|
+
const table = manifest.entries[path]?.symbolTable ?? [];
|
|
55
|
+
const picked = discovery.symbols?.[path] ?? [];
|
|
56
|
+
const ordered = [
|
|
57
|
+
...picked.flatMap((name) => table.filter((symbol) => symbol.name === name)),
|
|
58
|
+
...table.filter(
|
|
59
|
+
(symbol) => !picked.includes(symbol.name) && terms.some((term) => symbol.name.toLowerCase().includes(term))
|
|
60
|
+
)
|
|
61
|
+
];
|
|
62
|
+
return {
|
|
63
|
+
path,
|
|
64
|
+
symbols: ordered.map(({ name, startLine, endLine }) => ({ name, startLine, endLine })),
|
|
65
|
+
...test ? { test } : {}
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const ledger = new SessionCapsuleLedger(repositoryRoot);
|
|
69
|
+
if (input.ephemeral_context) await ledger.reset(sessionId);
|
|
70
|
+
const { text, emitted } = await renderLeanBlock({
|
|
71
|
+
repositoryRoot,
|
|
72
|
+
prompt,
|
|
73
|
+
pointers,
|
|
74
|
+
ledger,
|
|
75
|
+
sessionId,
|
|
76
|
+
testCommand: pointers.length > 0 ? await detectTestCommand(repositoryRoot) : null
|
|
77
|
+
});
|
|
78
|
+
if (Date.now() - started >= budgetMs) return "";
|
|
79
|
+
if (text) await emit?.(text);
|
|
80
|
+
await ledger.markSent(sessionId, emitted);
|
|
81
|
+
return text;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await logHookError(repositoryRoot, sessionId, error).catch(() => {
|
|
84
|
+
});
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function logHookError(root, sessionId, error) {
|
|
89
|
+
const directory = join(root, ".codebuddy", "cache", "hooks");
|
|
90
|
+
await mkdir(directory, { recursive: true });
|
|
91
|
+
const message = error?.message ?? String(error);
|
|
92
|
+
await appendFile(
|
|
93
|
+
join(directory, "errors.log"),
|
|
94
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ${message.replace(/\s+/g, " ")} session=${sessionId}
|
|
95
|
+
`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function renderLeanBlock(input) {
|
|
99
|
+
const loaded = [];
|
|
100
|
+
for (const pointer of input.pointers) {
|
|
101
|
+
if (loaded.length === LEAN_LIMITS.paths) break;
|
|
102
|
+
const source = await readBoundedSource(input.repositoryRoot, pointer.path);
|
|
103
|
+
if (source === null) continue;
|
|
104
|
+
loaded.push({
|
|
105
|
+
path: pointer.path,
|
|
106
|
+
symbols: pointer.symbols.slice(0, LEAN_LIMITS.symbolsPerFile),
|
|
107
|
+
...pointer.test ? { test: pointer.test } : {},
|
|
108
|
+
lines: source.split(/\r?\n/)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (loaded.length === 0) return { text: "", emitted: [] };
|
|
112
|
+
const capsules = [
|
|
113
|
+
{
|
|
114
|
+
key: "lean:pointers",
|
|
115
|
+
text: LEAN_POINTER_PREFIX + loaded.map(describePointer).join(", ")
|
|
116
|
+
},
|
|
117
|
+
...input.testCommand ? [{ key: "lean:test-command", text: `CodeBuddy test command: ${input.testCommand}` }] : []
|
|
118
|
+
];
|
|
119
|
+
if (isReadOnlyPrompt(input.prompt)) {
|
|
120
|
+
let tokens = 0;
|
|
121
|
+
for (const span of loaded.flatMap(spansOf)) {
|
|
122
|
+
const cost = estimateTokens(span.text);
|
|
123
|
+
if (tokens + cost > LEAN_LIMITS.spanTokens) break;
|
|
124
|
+
tokens += cost;
|
|
125
|
+
capsules.push(span);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const seen = await input.ledger.seen(input.sessionId, capsules);
|
|
129
|
+
const emitted = capsules.filter((capsule) => !seen.has(capsule.key));
|
|
130
|
+
return { text: emitted.map((capsule) => capsule.text).join("\n"), emitted };
|
|
131
|
+
}
|
|
132
|
+
function describePointer(pointer) {
|
|
133
|
+
const ranges = pointer.symbols.map((s) => `${s.name} L${s.startLine}-${s.endLine}`).join(", ");
|
|
134
|
+
const test = pointer.test ? `test: ${pointer.test}` : "";
|
|
135
|
+
const detail = [ranges, test].filter(Boolean).join("; ");
|
|
136
|
+
return detail ? `${pointer.path} (${detail})` : pointer.path;
|
|
137
|
+
}
|
|
138
|
+
function spansOf(pointer) {
|
|
139
|
+
const ranges = pointer.lines.length <= LEAN_LIMITS.wholeFileLines ? [{ start: 1, end: pointer.lines.length }] : pointer.symbols.map((s) => ({ start: s.startLine, end: s.endLine }));
|
|
140
|
+
const language = extname(pointer.path).slice(1);
|
|
141
|
+
return ranges.map(({ start, end }) => {
|
|
142
|
+
const body = pointer.lines.slice(start - 1, end).join("\n");
|
|
143
|
+
const hash = createHash("sha1").update(body).digest("hex").slice(0, 12);
|
|
144
|
+
return {
|
|
145
|
+
key: `lean:span:${pointer.path}:${start}-${end}:${hash}`,
|
|
146
|
+
text: `${pointer.path}:L${start}-${end}
|
|
147
|
+
\`\`\`${language}
|
|
148
|
+
${body}
|
|
149
|
+
\`\`\``
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
var FORCED_READ_ONLY = /\b(without editing|without changing|don't change|do not change|don't edit|do not edit)\b/i;
|
|
154
|
+
var QUESTION_START = /^(how|what|why|where|which|who|explain|describe|summarize)\b/i;
|
|
155
|
+
var EDIT_VERB = /\b(add|change|fix|implement|update|remove|delete|rename|refactor|write|create|make|edit|replace|move|migrate|support|build)\b/i;
|
|
156
|
+
function isReadOnlyPrompt(prompt) {
|
|
157
|
+
const text = prompt.trim();
|
|
158
|
+
if (text === "" || text.startsWith("/")) return false;
|
|
159
|
+
if (FORCED_READ_ONLY.test(text)) return true;
|
|
160
|
+
const asks = QUESTION_START.test(text) || text.endsWith("?");
|
|
161
|
+
return asks && !EDIT_VERB.test(text);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/hooks/lean-command.ts
|
|
165
|
+
async function runLeanHookCommand() {
|
|
166
|
+
await failSoft(async () => {
|
|
167
|
+
await runLeanHook(await readHookInput(), writeStdout);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
renderLeanBlock,
|
|
173
|
+
runLeanHookCommand
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=chunk-DSN5CEHO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/lean.ts","../src/hooks/lean-command.ts"],"sourcesContent":["/**\n * Lean mode: the prompt hook's only job is to cut the agent's token spend. It\n * injects ranked file pointers (and code spans for read-only questions) and\n * nothing else — no MCP tools, rules block, or memory capture.\n */\nimport { createHash } from \"node:crypto\";\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { extname, join, resolve } from \"node:path\";\nimport {\n discoverTaskPaths,\n loadProjectArchitectureMap,\n NON_SOURCE_PATH,\n readBoundedSource,\n taskTerms,\n} from \"../context/discovery.js\";\nimport { type ContextCapsule, SessionCapsuleLedger } from \"../context/session-ledger.js\";\nimport { detectTestCommand, findCoveringTest } from \"../context/tests.js\";\nimport { loadFreshIndex } from \"../indexer/engine.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\nimport type { HookInput } from \"./runtime.js\";\n\nexport const LEAN_POINTER_PREFIX = \"CodeBuddy likely files: \";\nexport const LEAN_LIMITS = {\n paths: 6,\n symbolsPerFile: 3,\n wholeFileLines: 80,\n spanTokens: 1200,\n} as const;\n\nexport type LeanPointer = {\n path: string;\n symbols: Array<{ name: string; startLine: number; endLine: number }>;\n /** The test file that covers this path, when one follows a known convention. */\n test?: string;\n};\n\ntype LoadedPointer = LeanPointer & { lines: string[] };\n\n/** A lean block finished later than this is dropped: the host may already have moved on. */\nexport const LEAN_HOOK_BUDGET_MS = 1500;\n/** Rendering after the BYOK rerank needs this much of the budget. */\nconst RENDER_RESERVE_MS = 300;\n\n/**\n * UserPromptSubmit in lean mode → ranked file pointers, or nothing. Never\n * throws: an error is appended to `.codebuddy/cache/hooks/errors.log` and the\n * turn proceeds as if CodeBuddy were not installed.\n */\nexport async function runLeanHook(\n input: HookInput,\n emit?: (block: string) => Promise<void>,\n budgetMs = LEAN_HOOK_BUDGET_MS,\n): Promise<string> {\n const started = Date.now();\n const repositoryRoot = resolve(input.cwd ?? process.cwd());\n const prompt = input.prompt?.trim() ?? \"\";\n if (prompt === \"\" || prompt.startsWith(\"/\")) return \"\";\n const sessionId = input.session_id ?? \"default\";\n try {\n const { manifest } = await loadFreshIndex(repositoryRoot);\n const discovery = await discoverTaskPaths(\n repositoryRoot,\n prompt,\n manifest,\n loadProjectArchitectureMap(repositoryRoot, manifest),\n // The rerank must answer while rendering still fits in the hook budget.\n { semantic: false, llmDeadlineAt: started + budgetMs - RENDER_RESERVE_MS },\n );\n const terms = taskTerms(prompt);\n const indexed = new Set(Object.keys(manifest.entries));\n const pointers: LeanPointer[] = discovery.paths\n .filter((path) => !NON_SOURCE_PATH.test(path))\n .map((path) => {\n const test = findCoveringTest(path, indexed);\n const table = manifest.entries[path]?.symbolTable ?? [];\n // Symbols the BYOK rerank named come first, then term matches.\n const picked = discovery.symbols?.[path] ?? [];\n const ordered = [\n ...picked.flatMap((name) => table.filter((symbol) => symbol.name === name)),\n ...table.filter(\n (symbol) =>\n !picked.includes(symbol.name) &&\n terms.some((term) => symbol.name.toLowerCase().includes(term)),\n ),\n ];\n return {\n path,\n symbols: ordered.map(({ name, startLine, endLine }) => ({ name, startLine, endLine })),\n ...(test ? { test } : {}),\n };\n });\n const ledger = new SessionCapsuleLedger(repositoryRoot);\n // The host keeps the block for this turn only, so nothing counts as already sent.\n if (input.ephemeral_context) await ledger.reset(sessionId);\n const { text, emitted } = await renderLeanBlock({\n repositoryRoot,\n prompt,\n pointers,\n ledger,\n sessionId,\n testCommand: pointers.length > 0 ? await detectTestCommand(repositoryRoot) : null,\n });\n if (Date.now() - started >= budgetMs) return \"\";\n if (text) await emit?.(text);\n await ledger.markSent(sessionId, emitted);\n return text;\n } catch (error) {\n await logHookError(repositoryRoot, sessionId, error).catch(() => {});\n return \"\";\n }\n}\n\nasync function logHookError(root: string, sessionId: string, error: unknown): Promise<void> {\n const directory = join(root, \".codebuddy\", \"cache\", \"hooks\");\n await mkdir(directory, { recursive: true });\n const message = (error as Error)?.message ?? String(error);\n await appendFile(\n join(directory, \"errors.log\"),\n `${new Date().toISOString()} ${message.replace(/\\s+/g, \" \")} session=${sessionId}\\n`,\n );\n}\n\n/**\n * The lean block for one prompt: a single pointer line, plus current-source\n * spans when the prompt is read-only. Capsules already delivered this session\n * are left out; the caller marks `emitted` as sent once the block is written.\n */\nexport async function renderLeanBlock(input: {\n repositoryRoot: string;\n prompt: string;\n pointers: LeanPointer[];\n ledger: SessionCapsuleLedger;\n sessionId: string;\n /** Sent once per session, after the pointers. */\n testCommand?: string | null;\n}): Promise<{ text: string; emitted: ContextCapsule[] }> {\n const loaded: LoadedPointer[] = [];\n for (const pointer of input.pointers) {\n if (loaded.length === LEAN_LIMITS.paths) break;\n const source = await readBoundedSource(input.repositoryRoot, pointer.path);\n if (source === null) continue; // deleted, oversized or binary since indexing\n loaded.push({\n path: pointer.path,\n symbols: pointer.symbols.slice(0, LEAN_LIMITS.symbolsPerFile),\n ...(pointer.test ? { test: pointer.test } : {}),\n lines: source.split(/\\r?\\n/),\n });\n }\n if (loaded.length === 0) return { text: \"\", emitted: [] };\n\n const capsules: ContextCapsule[] = [\n {\n key: \"lean:pointers\",\n text: LEAN_POINTER_PREFIX + loaded.map(describePointer).join(\", \"),\n },\n ...(input.testCommand\n ? [{ key: \"lean:test-command\", text: `CodeBuddy test command: ${input.testCommand}` }]\n : []),\n ];\n if (isReadOnlyPrompt(input.prompt)) {\n let tokens = 0;\n for (const span of loaded.flatMap(spansOf)) {\n const cost = estimateTokens(span.text);\n if (tokens + cost > LEAN_LIMITS.spanTokens) break;\n tokens += cost;\n capsules.push(span);\n }\n }\n const seen = await input.ledger.seen(input.sessionId, capsules);\n const emitted = capsules.filter((capsule) => !seen.has(capsule.key));\n return { text: emitted.map((capsule) => capsule.text).join(\"\\n\"), emitted };\n}\n\nfunction describePointer(pointer: LoadedPointer): string {\n const ranges = pointer.symbols.map((s) => `${s.name} L${s.startLine}-${s.endLine}`).join(\", \");\n const test = pointer.test ? `test: ${pointer.test}` : \"\";\n const detail = [ranges, test].filter(Boolean).join(\"; \");\n return detail ? `${pointer.path} (${detail})` : pointer.path;\n}\n\n/** Whole small files; otherwise the matched symbol spans. */\nfunction spansOf(pointer: LoadedPointer): ContextCapsule[] {\n const ranges =\n pointer.lines.length <= LEAN_LIMITS.wholeFileLines\n ? [{ start: 1, end: pointer.lines.length }]\n : pointer.symbols.map((s) => ({ start: s.startLine, end: s.endLine }));\n const language = extname(pointer.path).slice(1);\n return ranges.map(({ start, end }) => {\n const body = pointer.lines.slice(start - 1, end).join(\"\\n\");\n const hash = createHash(\"sha1\").update(body).digest(\"hex\").slice(0, 12);\n return {\n key: `lean:span:${pointer.path}:${start}-${end}:${hash}`,\n text: `${pointer.path}:L${start}-${end}\\n\\`\\`\\`${language}\\n${body}\\n\\`\\`\\``,\n };\n });\n}\n\nconst FORCED_READ_ONLY =\n /\\b(without editing|without changing|don't change|do not change|don't edit|do not edit)\\b/i;\nconst QUESTION_START = /^(how|what|why|where|which|who|explain|describe|summarize)\\b/i;\nconst EDIT_VERB =\n /\\b(add|change|fix|implement|update|remove|delete|rename|refactor|write|create|make|edit|replace|move|migrate|support|build)\\b/i;\n\n/**\n * Whether a prompt asks a question without asking for edits. Conservative: a\n * false positive costs up to the span budget, a false negative costs nothing.\n */\nexport function isReadOnlyPrompt(prompt: string): boolean {\n const text = prompt.trim();\n if (text === \"\" || text.startsWith(\"/\")) return false;\n if (FORCED_READ_ONLY.test(text)) return true;\n const asks = QUESTION_START.test(text) || text.endsWith(\"?\");\n return asks && !EDIT_VERB.test(text);\n}\n","import { runLeanHook } from \"./lean.js\";\nimport { failSoft, readHookInput, writeStdout } from \"./stdio.js\";\n\n/** `codebuddy hooks lean`: read the hook payload, print the lean block, never fail. */\nexport async function runLeanHookCommand(): Promise<void> {\n await failSoft(async () => {\n await runLeanHook(await readHookInput(), writeStdout);\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// src/memory-extract/sensitive.ts
|
|
2
|
+
var PATTERNS = [
|
|
3
|
+
{ label: "private-key", regex: /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/ },
|
|
4
|
+
{ label: "aws-access-key", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
5
|
+
{ label: "hf-token", regex: /\bhf_[A-Za-z0-9]{16,}\b/ },
|
|
6
|
+
{ label: "github-token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/ },
|
|
7
|
+
// GitHub fine-grained personal access tokens.
|
|
8
|
+
{
|
|
9
|
+
label: "github-fine-grained-pat",
|
|
10
|
+
regex: /\bgithub_pat_[A-Za-z0-9_]{22,255}_[A-Za-z0-9]{22,255}\b/
|
|
11
|
+
},
|
|
12
|
+
{ label: "npm-token", regex: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
13
|
+
{ label: "gitlab-token", regex: /\bglpat-[A-Za-z0-9_-]{20,}\b/ },
|
|
14
|
+
{ label: "slack-token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
15
|
+
{ label: "bearer-token", regex: /\bBearer\s+[A-Za-z0-9._-]{12,}\b/ },
|
|
16
|
+
// Vendor API keys with distinctive, low-false-positive prefixes.
|
|
17
|
+
{ label: "anthropic-key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
|
|
18
|
+
{ label: "openai-key", regex: /\bsk-(?!ant-)[A-Za-z0-9_-]{20,}\b/ },
|
|
19
|
+
{ label: "google-api-key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
20
|
+
{ label: "stripe-key", regex: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b/ },
|
|
21
|
+
// A JWT: three base64url segments. Common in auth-related pastes.
|
|
22
|
+
{ label: "jwt", regex: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
23
|
+
// Credentials embedded in a connection URL: scheme://user:password@host.
|
|
24
|
+
{
|
|
25
|
+
label: "connection-string-credentials",
|
|
26
|
+
regex: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]+@/i
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
// Catches `password: x`, `api_key = x`, and the prose form `password is x`.
|
|
30
|
+
// Deliberately eager — an over-flagged review item is cheaper than a leak.
|
|
31
|
+
label: "credential-assignment",
|
|
32
|
+
regex: /\b(?:api[_-]?key|secret|token|password|passwd|pwd|access[_-]?key|authorization|auth[_-]?token|credential[s]?)\b\s*(?:[:=]|\bis\b|\bwas\b|\bequals\b)\s*\S{6,}/i
|
|
33
|
+
},
|
|
34
|
+
// HTTP Basic-auth style inline credentials outside a URL scheme
|
|
35
|
+
// (`Authorization: Basic dXNlcjpwYXNz`).
|
|
36
|
+
{
|
|
37
|
+
label: "basic-auth-header",
|
|
38
|
+
regex: /\bAuthorization\s*[:=]\s*Basic\s+[A-Za-z0-9+/=]{8,}/i
|
|
39
|
+
},
|
|
40
|
+
{ label: "email", regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/ }
|
|
41
|
+
];
|
|
42
|
+
var OPENCODE_ID = /^(?:ses|msg|prt)_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
|
|
43
|
+
function hasHighEntropyToken(text) {
|
|
44
|
+
for (const token of text.split(/[^A-Za-z0-9+/_=-]+/)) {
|
|
45
|
+
if (token.length < 24 || token.length > 200 || OPENCODE_ID.test(token)) continue;
|
|
46
|
+
if (!/[a-z]/.test(token) || !/[A-Z]/.test(token) || !/[0-9]/.test(token)) continue;
|
|
47
|
+
if (shannonEntropy(token) >= 3.5) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function shannonEntropy(value) {
|
|
52
|
+
const counts = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
54
|
+
let entropy = 0;
|
|
55
|
+
for (const count of counts.values()) {
|
|
56
|
+
const p = count / value.length;
|
|
57
|
+
entropy -= p * Math.log2(p);
|
|
58
|
+
}
|
|
59
|
+
return entropy;
|
|
60
|
+
}
|
|
61
|
+
function scanSensitive(text) {
|
|
62
|
+
const reasons = [];
|
|
63
|
+
for (const { label, regex } of PATTERNS) {
|
|
64
|
+
if (regex.test(text) && !reasons.includes(label)) reasons.push(label);
|
|
65
|
+
}
|
|
66
|
+
if (hasHighEntropyToken(text)) reasons.push("high-entropy-string");
|
|
67
|
+
return { sensitive: reasons.length > 0, reasons };
|
|
68
|
+
}
|
|
69
|
+
var REDACTED = "[REDACTED SENSITIVE CONTENT]";
|
|
70
|
+
function redactSensitive(text) {
|
|
71
|
+
return scanSensitive(text).sensitive ? REDACTED : text;
|
|
72
|
+
}
|
|
73
|
+
var CLAIM_KEYS = ["subject", "predicate", "object", "content"];
|
|
74
|
+
var PROVENANCE_TEXT_KEYS = [
|
|
75
|
+
"paths",
|
|
76
|
+
"verification",
|
|
77
|
+
"reason",
|
|
78
|
+
"createdByAgent",
|
|
79
|
+
"sourcePlanId",
|
|
80
|
+
"sourceSessionId",
|
|
81
|
+
"introducedBy"
|
|
82
|
+
];
|
|
83
|
+
function claimText(record) {
|
|
84
|
+
return CLAIM_KEYS.map((key) => record[key]).filter((value) => typeof value === "string").join(" ");
|
|
85
|
+
}
|
|
86
|
+
function redactPersisted(record) {
|
|
87
|
+
const out = { ...record };
|
|
88
|
+
const present = CLAIM_KEYS.filter((key) => typeof out[key] === "string");
|
|
89
|
+
const claimSensitive = present.length > 0 && (scanSensitive(claimText(out)).sensitive || present.some((key) => scanSensitive(out[key]).sensitive));
|
|
90
|
+
if (claimSensitive) for (const key of present) out[key] = REDACTED;
|
|
91
|
+
for (const key of PROVENANCE_TEXT_KEYS) {
|
|
92
|
+
const value = out[key];
|
|
93
|
+
if (typeof value === "string") out[key] = redactSensitive(value);
|
|
94
|
+
else if (Array.isArray(value)) {
|
|
95
|
+
out[key] = value.map((item) => typeof item === "string" ? redactSensitive(item) : item);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function scanFields(fields) {
|
|
101
|
+
const reasons = [];
|
|
102
|
+
for (const field of fields) {
|
|
103
|
+
if (!field) continue;
|
|
104
|
+
for (const reason of scanSensitive(field).reasons) {
|
|
105
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { sensitive: reasons.length > 0, reasons };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
scanSensitive,
|
|
113
|
+
REDACTED,
|
|
114
|
+
redactSensitive,
|
|
115
|
+
claimText,
|
|
116
|
+
redactPersisted,
|
|
117
|
+
scanFields
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=chunk-GDJH7XJC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/memory-extract/sensitive.ts"],"sourcesContent":["/**\n * Sensitive-content scan. Secrets and personal data must never be auto-saved\n * as durable memory — a matched candidate is forced into the review queue,\n * flagged, so a human decides. This is a conservative detector: false positives\n * (an extra review item) are acceptable; false negatives (a leaked key in\n * committed Markdown) are not.\n *\n * The scan only ever *routes to review* — it never blocks a turn, deletes\n * content, or rejects on its own. Adding a reason at most moves a candidate from\n * \"auto-save\" to \"human decides\".\n */\n\nexport type SensitivityResult = {\n sensitive: boolean;\n reasons: string[];\n};\n\nconst PATTERNS: Array<{ label: string; regex: RegExp }> = [\n { label: \"private-key\", regex: /-----BEGIN(?: [A-Z0-9]+)* PRIVATE KEY-----/ },\n { label: \"aws-access-key\", regex: /\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b/ },\n { label: \"hf-token\", regex: /\\bhf_[A-Za-z0-9]{16,}\\b/ },\n { label: \"github-token\", regex: /\\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\\b/ },\n // GitHub fine-grained personal access tokens.\n {\n label: \"github-fine-grained-pat\",\n regex: /\\bgithub_pat_[A-Za-z0-9_]{22,255}_[A-Za-z0-9]{22,255}\\b/,\n },\n { label: \"npm-token\", regex: /\\bnpm_[A-Za-z0-9]{36}\\b/ },\n { label: \"gitlab-token\", regex: /\\bglpat-[A-Za-z0-9_-]{20,}\\b/ },\n { label: \"slack-token\", regex: /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/ },\n { label: \"bearer-token\", regex: /\\bBearer\\s+[A-Za-z0-9._-]{12,}\\b/ },\n // Vendor API keys with distinctive, low-false-positive prefixes.\n { label: \"anthropic-key\", regex: /\\bsk-ant-[A-Za-z0-9_-]{20,}\\b/ },\n { label: \"openai-key\", regex: /\\bsk-(?!ant-)[A-Za-z0-9_-]{20,}\\b/ },\n { label: \"google-api-key\", regex: /\\bAIza[0-9A-Za-z_-]{35}\\b/ },\n { label: \"stripe-key\", regex: /\\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\\b/ },\n // A JWT: three base64url segments. Common in auth-related pastes.\n { label: \"jwt\", regex: /\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/ },\n // Credentials embedded in a connection URL: scheme://user:password@host.\n {\n label: \"connection-string-credentials\",\n regex: /\\b[a-z][a-z0-9+.-]*:\\/\\/[^\\s:/@]+:[^\\s:/@]+@/i,\n },\n {\n // Catches `password: x`, `api_key = x`, and the prose form `password is x`.\n // Deliberately eager — an over-flagged review item is cheaper than a leak.\n label: \"credential-assignment\",\n regex:\n /\\b(?:api[_-]?key|secret|token|password|passwd|pwd|access[_-]?key|authorization|auth[_-]?token|credential[s]?)\\b\\s*(?:[:=]|\\bis\\b|\\bwas\\b|\\bequals\\b)\\s*\\S{6,}/i,\n },\n // HTTP Basic-auth style inline credentials outside a URL scheme\n // (`Authorization: Basic dXNlcjpwYXNz`).\n {\n label: \"basic-auth-header\",\n regex: /\\bAuthorization\\s*[:=]\\s*Basic\\s+[A-Za-z0-9+/=]{8,}/i,\n },\n { label: \"email\", regex: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/ },\n];\n\n/**\n * Deterministic, review-oriented high-entropy heuristic. Flags a token that\n * looks like a random secret — long, mixed character classes, high Shannon\n * entropy — that the prefix-based patterns above wouldn't catch. Tuned to stay\n * quiet on ordinary code and prose: identifiers are usually low-entropy words\n * (`camelCase`, `snake_case`), and hex hashes / SHAs lack the required mix of\n * upper, lower, and digit. Never a hard block; at most one extra review item.\n * OpenCode session/message/part ids (`ses_` + 12 hex + 14 random base62) are\n * host identifiers recorded as provenance, not secrets, so they are skipped;\n * otherwise every OpenCode capture would be queued for review.\n */\nconst OPENCODE_ID = /^(?:ses|msg|prt)_[0-9a-f]{12}[0-9A-Za-z]{14}$/;\n\nexport function hasHighEntropyToken(text: string): boolean {\n for (const token of text.split(/[^A-Za-z0-9+/_=-]+/)) {\n if (token.length < 24 || token.length > 200 || OPENCODE_ID.test(token)) continue;\n if (!/[a-z]/.test(token) || !/[A-Z]/.test(token) || !/[0-9]/.test(token)) continue;\n if (shannonEntropy(token) >= 3.5) return true;\n }\n return false;\n}\n\nfunction shannonEntropy(value: string): number {\n const counts = new Map<string, number>();\n for (const char of value) counts.set(char, (counts.get(char) ?? 0) + 1);\n let entropy = 0;\n for (const count of counts.values()) {\n const p = count / value.length;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\nexport function scanSensitive(text: string): SensitivityResult {\n const reasons: string[] = [];\n for (const { label, regex } of PATTERNS) {\n if (regex.test(text) && !reasons.includes(label)) reasons.push(label);\n }\n if (hasHighEntropyToken(text)) reasons.push(\"high-entropy-string\");\n return { sensitive: reasons.length > 0, reasons };\n}\n\nexport const REDACTED = \"[REDACTED SENSITIVE CONTENT]\";\n\n/** Whole-field redaction: a field that scans sensitive is replaced, never partially kept. */\nexport function redactSensitive(text: string): string {\n return scanSensitive(text).sensitive ? REDACTED : text;\n}\n\n/**\n * The claim: fields that together state one memory. A credential can be split\n * across them (\"password\" / \"is\" / \"hunter2secret\"), so they are scanned\n * jointly and redacted as a unit.\n */\nexport const CLAIM_KEYS = [\"subject\", \"predicate\", \"object\", \"content\"] as const;\n\n/** Independent free-text provenance fields (string or string[]), redacted one by one. */\nexport const PROVENANCE_TEXT_KEYS = [\n \"paths\",\n \"verification\",\n \"reason\",\n \"createdByAgent\",\n \"sourcePlanId\",\n \"sourceSessionId\",\n \"introducedBy\",\n] as const;\n\n/** The claim fields joined in reading order, for a joint scan. */\nexport function claimText(record: Partial<Record<(typeof CLAIM_KEYS)[number], unknown>>): string {\n return CLAIM_KEYS.map((key) => record[key])\n .filter((value): value is string => typeof value === \"string\")\n .join(\" \");\n}\n\n/**\n * Persistence-boundary redaction: returns a copy of `record` with every\n * free-text field redacted. If any claim field, or the joined claim, scans\n * sensitive, ALL claim fields are replaced (a sibling may hold the value half\n * of the credential). Provenance fields are redacted individually. Idempotent,\n * so re-persisting (e.g. approving a queued item) never re-introduces a raw\n * value. Structural fields (ids, fingerprint, namespace, timestamps, enums)\n * are left alone.\n */\nexport function redactPersisted<T extends object>(record: T): T {\n const out = { ...record } as Record<string, unknown>;\n const present = CLAIM_KEYS.filter((key) => typeof out[key] === \"string\");\n const claimSensitive =\n present.length > 0 &&\n (scanSensitive(claimText(out)).sensitive ||\n present.some((key) => scanSensitive(out[key] as string).sensitive));\n if (claimSensitive) for (const key of present) out[key] = REDACTED;\n for (const key of PROVENANCE_TEXT_KEYS) {\n const value = out[key];\n if (typeof value === \"string\") out[key] = redactSensitive(value);\n else if (Array.isArray(value)) {\n out[key] = value.map((item) => (typeof item === \"string\" ? redactSensitive(item) : item));\n }\n }\n return out as T;\n}\n\n/** Scan several surfaces separately and merge the reason labels (never the matched text). */\nexport function scanFields(fields: Array<string | null | undefined>): SensitivityResult {\n const reasons: string[] = [];\n for (const field of fields) {\n if (!field) continue;\n for (const reason of scanSensitive(field).reasons) {\n if (!reasons.includes(reason)) reasons.push(reason);\n }\n }\n return { sensitive: reasons.length > 0, reasons };\n}\n"],"mappings":"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","names":[]}
|