@orquentra/codebuddy 2.4.0 → 2.5.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 (61) hide show
  1. package/README.md +42 -1090
  2. package/dist/chunk-3XYQGFPB.js +91 -0
  3. package/dist/chunk-3XYQGFPB.js.map +1 -0
  4. package/dist/chunk-4BYGTEJT.js +103 -0
  5. package/dist/chunk-4BYGTEJT.js.map +1 -0
  6. package/dist/{chunk-ITGRANYW.js → chunk-6MKEIDX5.js} +2 -2
  7. package/dist/{chunk-K63EOA4B.js → chunk-776EQXDV.js} +25 -6
  8. package/dist/chunk-776EQXDV.js.map +1 -0
  9. package/dist/{chunk-5OWZVZSO.js → chunk-CQPCHMBS.js} +8 -165
  10. package/dist/chunk-CQPCHMBS.js.map +1 -0
  11. package/dist/chunk-JDZ3VK26.js +4006 -0
  12. package/dist/chunk-JDZ3VK26.js.map +1 -0
  13. package/dist/chunk-JMWJ7V6Y.js +99 -0
  14. package/dist/chunk-JMWJ7V6Y.js.map +1 -0
  15. package/dist/chunk-KYK6RPAJ.js +99 -0
  16. package/dist/chunk-KYK6RPAJ.js.map +1 -0
  17. package/dist/{chunk-IBMJYIEP.js → chunk-L5AYZC3F.js} +7 -7
  18. package/dist/{chunk-3ZELDCR3.js → chunk-PCE4STAY.js} +7 -7
  19. package/dist/{chunk-TI5TQD6D.js → chunk-QDBDYCM2.js} +2 -2
  20. package/dist/{chunk-DSN5CEHO.js → chunk-ROQTT5ZO.js} +3 -3
  21. package/dist/{chunk-IE2OUO54.js → chunk-UFZD3JAA.js} +3 -3
  22. package/dist/chunk-Z5QB367W.js +5028 -0
  23. package/dist/chunk-Z5QB367W.js.map +1 -0
  24. package/dist/cli/index.js +2 -2
  25. package/dist/cloud-GP6G3EAK.js +17 -0
  26. package/dist/{codebuddy-D2xJ6hFk.d.ts → codebuddy-DM-i0Fu5.d.ts} +49 -18
  27. package/dist/{config-S2TULOMK.js → config-KFEHLV6W.js} +3 -3
  28. package/dist/index.d.ts +23 -3
  29. package/dist/index.js +34 -25
  30. package/dist/langgraph/index.d.ts +1 -1
  31. package/dist/lean-command-DA4ZNKG3.js +15 -0
  32. package/dist/local-3NGNESNB.js +13 -0
  33. package/dist/{main-ZZ3EQLYA.js → main-BDVQAGAS.js} +1261 -242
  34. package/dist/main-BDVQAGAS.js.map +1 -0
  35. package/dist/{memory-NIBFAMY4.js → memory-4RGVLPKG.js} +6 -6
  36. package/dist/operations-EQGOFCBD.js +24 -0
  37. package/dist/operations-EQGOFCBD.js.map +1 -0
  38. package/dist/{rerank-Q7MIFNKA.js → rerank-3P3C2UHN.js} +4 -4
  39. package/dist/{summaries-TSE44KUQ.js → summaries-ZBV5IN7Y.js} +6 -6
  40. package/dist/summaries-ZBV5IN7Y.js.map +1 -0
  41. package/dist/{zvec-F27AR357.js → zvec-MAPSJQSR.js} +3 -3
  42. package/dist/zvec-MAPSJQSR.js.map +1 -0
  43. package/package.json +7 -3
  44. package/dist/chunk-5OWZVZSO.js.map +0 -1
  45. package/dist/chunk-K63EOA4B.js.map +0 -1
  46. package/dist/chunk-SYHB3AMM.js +0 -8580
  47. package/dist/chunk-SYHB3AMM.js.map +0 -1
  48. package/dist/lean-command-D6TZJEFX.js +0 -13
  49. package/dist/main-ZZ3EQLYA.js.map +0 -1
  50. /package/dist/{chunk-ITGRANYW.js.map → chunk-6MKEIDX5.js.map} +0 -0
  51. /package/dist/{chunk-IBMJYIEP.js.map → chunk-L5AYZC3F.js.map} +0 -0
  52. /package/dist/{chunk-3ZELDCR3.js.map → chunk-PCE4STAY.js.map} +0 -0
  53. /package/dist/{chunk-TI5TQD6D.js.map → chunk-QDBDYCM2.js.map} +0 -0
  54. /package/dist/{chunk-DSN5CEHO.js.map → chunk-ROQTT5ZO.js.map} +0 -0
  55. /package/dist/{chunk-IE2OUO54.js.map → chunk-UFZD3JAA.js.map} +0 -0
  56. /package/dist/{config-S2TULOMK.js.map → cloud-GP6G3EAK.js.map} +0 -0
  57. /package/dist/{lean-command-D6TZJEFX.js.map → config-KFEHLV6W.js.map} +0 -0
  58. /package/dist/{summaries-TSE44KUQ.js.map → lean-command-DA4ZNKG3.js.map} +0 -0
  59. /package/dist/{zvec-F27AR357.js.map → local-3NGNESNB.js.map} +0 -0
  60. /package/dist/{memory-NIBFAMY4.js.map → memory-4RGVLPKG.js.map} +0 -0
  61. /package/dist/{rerank-Q7MIFNKA.js.map → rerank-3P3C2UHN.js.map} +0 -0
@@ -0,0 +1,91 @@
1
+ import {
2
+ LOCAL_EMBEDDING_MODEL
3
+ } from "./chunk-JMWJ7V6Y.js";
4
+
5
+ // src/providers/local.ts
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ var MAX_INPUT_CHARS = 8e3;
9
+ var transformers = null;
10
+ var extractor = null;
11
+ function modelCacheDir() {
12
+ return process.env.CODEBUDDY_MODEL_CACHE ?? join(homedir(), ".cache", "codebuddy", "models");
13
+ }
14
+ function loadTransformers() {
15
+ transformers ??= import("@huggingface/transformers").then((module) => {
16
+ module.env.cacheDir = modelCacheDir();
17
+ module.env.logLevel = module.LogLevel.ERROR;
18
+ return module;
19
+ });
20
+ transformers.catch(() => {
21
+ transformers = null;
22
+ });
23
+ return transformers;
24
+ }
25
+ var LocalModelProvider = class {
26
+ space = {
27
+ model: LOCAL_EMBEDDING_MODEL.id,
28
+ revision: LOCAL_EMBEDDING_MODEL.revision,
29
+ schema: LOCAL_EMBEDDING_MODEL.schema,
30
+ dimensions: LOCAL_EMBEDDING_MODEL.dimensions
31
+ };
32
+ /** `request.model` is ignored: one provider is one embedding space. */
33
+ async embed(request) {
34
+ const inputs = (typeof request.input === "string" ? [request.input] : request.input).map(
35
+ (text) => text.slice(0, MAX_INPUT_CHARS)
36
+ );
37
+ extractor ??= loadTransformers().then(
38
+ async (module) => await module.pipeline("feature-extraction", this.space.model, {
39
+ revision: this.space.revision,
40
+ dtype: "q8"
41
+ })
42
+ );
43
+ extractor.catch(() => {
44
+ extractor = null;
45
+ });
46
+ const extract = await extractor;
47
+ const output = await extract(inputs, { pooling: "cls", normalize: true });
48
+ return { ...this.space, vectors: output.tolist() };
49
+ }
50
+ async generateText() {
51
+ throw new Error("The local provider only embeds; text generation uses the BYOK LLM layer.");
52
+ }
53
+ };
54
+ var rerankers = /* @__PURE__ */ new Map();
55
+ async function rerankLocally(model, query, documents) {
56
+ if (documents.length === 0) return [];
57
+ let pending = rerankers.get(model.id);
58
+ if (!pending) {
59
+ pending = loadTransformers().then(async (module) => {
60
+ const options = model.revision ? { revision: model.revision } : {};
61
+ const [tokenizer2, classifier2] = await Promise.all([
62
+ module.AutoTokenizer.from_pretrained(model.id, options),
63
+ module.AutoModelForSequenceClassification.from_pretrained(model.id, {
64
+ ...options,
65
+ dtype: "q8"
66
+ })
67
+ ]);
68
+ return { tokenizer: tokenizer2, model: classifier2 };
69
+ });
70
+ rerankers.set(model.id, pending);
71
+ pending.catch(() => rerankers.delete(model.id));
72
+ }
73
+ const { tokenizer, model: classifier } = await pending;
74
+ const inputs = tokenizer(
75
+ documents.map(() => query.slice(0, MAX_INPUT_CHARS)),
76
+ {
77
+ text_pair: documents.map((document) => document.slice(0, MAX_INPUT_CHARS)),
78
+ padding: true,
79
+ truncation: true
80
+ }
81
+ );
82
+ const { logits } = await classifier(inputs);
83
+ return logits.tolist().map((row) => row[0] ?? Number.NEGATIVE_INFINITY);
84
+ }
85
+
86
+ export {
87
+ modelCacheDir,
88
+ LocalModelProvider,
89
+ rerankLocally
90
+ };
91
+ //# sourceMappingURL=chunk-3XYQGFPB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/local.ts"],"sourcesContent":["import { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type {\n EmbeddingRequest,\n EmbeddingResponse,\n ModelProvider,\n TextGenerationResponse,\n} from \"./adapter.js\";\nimport type { EmbeddingSpace } from \"./embedding-space.js\";\nimport { LOCAL_EMBEDDING_MODEL } from \"./models.js\";\n\n/**\n * Local Hugging Face models through transformers.js (ONNX Runtime, CPU). The\n * weights download once into `~/.cache/codebuddy/models` (override with\n * `CODEBUDDY_MODEL_CACHE`); afterwards nothing leaves the machine and no token\n * is needed. `@huggingface/transformers` is an optional dependency, loaded on\n * first use, so a missing install only disables semantic ranking.\n */\n\ntype Transformers = typeof import(\"@huggingface/transformers\");\n/** The slice of transformers.js' FeatureExtractionPipeline used here. */\ntype Extractor = (\n texts: string[],\n options: { pooling: \"cls\"; normalize: boolean },\n) => Promise<{ tolist(): number[][] }>;\n\n/** Hard cap on a single input; ModernBERT reads 8k tokens, memories are far shorter. */\nconst MAX_INPUT_CHARS = 8_000;\n\nlet transformers: Promise<Transformers> | null = null;\nlet extractor: Promise<Extractor> | null = null;\n\nexport function modelCacheDir(): string {\n return process.env.CODEBUDDY_MODEL_CACHE ?? join(homedir(), \".cache\", \"codebuddy\", \"models\");\n}\n\nfunction loadTransformers(): Promise<Transformers> {\n transformers ??= import(\"@huggingface/transformers\").then((module) => {\n module.env.cacheDir = modelCacheDir();\n // Info/debug logs go to console.log, i.e. stdout: the MCP JSON-RPC channel\n // under `codebuddy serve`. Errors only (they use stderr), here and in ONNX Runtime.\n module.env.logLevel = module.LogLevel.ERROR;\n return module;\n });\n // A failed import (optional dependency missing) is retried on the next call.\n transformers.catch(() => {\n transformers = null;\n });\n return transformers;\n}\n\nexport class LocalModelProvider implements ModelProvider {\n readonly space: EmbeddingSpace = {\n model: LOCAL_EMBEDDING_MODEL.id,\n revision: LOCAL_EMBEDDING_MODEL.revision,\n schema: LOCAL_EMBEDDING_MODEL.schema,\n dimensions: LOCAL_EMBEDDING_MODEL.dimensions,\n };\n\n /** `request.model` is ignored: one provider is one embedding space. */\n async embed(request: EmbeddingRequest): Promise<EmbeddingResponse> {\n const inputs = (typeof request.input === \"string\" ? [request.input] : request.input).map(\n (text) => text.slice(0, MAX_INPUT_CHARS),\n );\n extractor ??= loadTransformers().then(\n async (module) =>\n (await module.pipeline(\"feature-extraction\", this.space.model, {\n revision: this.space.revision,\n dtype: \"q8\",\n })) as unknown as Extractor,\n );\n // A failed load (no network on first use) is retried on the next call.\n extractor.catch(() => {\n extractor = null;\n });\n const extract = await extractor;\n // Granite r2 is trained with CLS pooling; L2-normalized so cosine = dot.\n const output = await extract(inputs, { pooling: \"cls\", normalize: true });\n return { ...this.space, vectors: output.tolist() };\n }\n\n async generateText(): Promise<TextGenerationResponse> {\n throw new Error(\"The local provider only embeds; text generation uses the BYOK LLM layer.\");\n }\n}\n\ntype Reranker = {\n tokenizer: (\n texts: string[],\n options: { text_pair: string[]; padding: boolean; truncation: boolean },\n ) => unknown;\n model: (inputs: unknown) => Promise<{ logits: { tolist(): number[][] } }>;\n};\nconst rerankers = new Map<string, Promise<Reranker>>();\n\n/**\n * Local cross-encoder logits for (query, document) pairs, in input order.\n * Higher is more relevant; values order documents for one query and are not\n * calibrated across queries, so callers order by them and never threshold.\n */\nexport async function rerankLocally(\n model: { id: string; revision?: string },\n query: string,\n documents: string[],\n): Promise<number[]> {\n if (documents.length === 0) return [];\n let pending = rerankers.get(model.id);\n if (!pending) {\n pending = loadTransformers().then(async (module) => {\n const options = model.revision ? { revision: model.revision } : {};\n const [tokenizer, classifier] = await Promise.all([\n module.AutoTokenizer.from_pretrained(model.id, options),\n module.AutoModelForSequenceClassification.from_pretrained(model.id, {\n ...options,\n dtype: \"q8\",\n }),\n ]);\n return { tokenizer, model: classifier } as unknown as Reranker;\n });\n rerankers.set(model.id, pending);\n pending.catch(() => rerankers.delete(model.id));\n }\n const { tokenizer, model: classifier } = await pending;\n const inputs = tokenizer(\n documents.map(() => query.slice(0, MAX_INPUT_CHARS)),\n {\n text_pair: documents.map((document) => document.slice(0, MAX_INPUT_CHARS)),\n padding: true,\n truncation: true,\n },\n );\n const { logits } = await classifier(inputs);\n return logits.tolist().map((row) => row[0] ?? Number.NEGATIVE_INFINITY);\n}\n"],"mappings":";;;;;AAAA,SAAS,eAAe;AACxB,SAAS,YAAY;AA0BrB,IAAM,kBAAkB;AAExB,IAAI,eAA6C;AACjD,IAAI,YAAuC;AAEpC,SAAS,gBAAwB;AACtC,SAAO,QAAQ,IAAI,yBAAyB,KAAK,QAAQ,GAAG,UAAU,aAAa,QAAQ;AAC7F;AAEA,SAAS,mBAA0C;AACjD,mBAAiB,OAAO,2BAA2B,EAAE,KAAK,CAAC,WAAW;AACpE,WAAO,IAAI,WAAW,cAAc;AAGpC,WAAO,IAAI,WAAW,OAAO,SAAS;AACtC,WAAO;AAAA,EACT,CAAC;AAED,eAAa,MAAM,MAAM;AACvB,mBAAe;AAAA,EACjB,CAAC;AACD,SAAO;AACT;AAEO,IAAM,qBAAN,MAAkD;AAAA,EAC9C,QAAwB;AAAA,IAC/B,OAAO,sBAAsB;AAAA,IAC7B,UAAU,sBAAsB;AAAA,IAChC,QAAQ,sBAAsB;AAAA,IAC9B,YAAY,sBAAsB;AAAA,EACpC;AAAA;AAAA,EAGA,MAAM,MAAM,SAAuD;AACjE,UAAM,UAAU,OAAO,QAAQ,UAAU,WAAW,CAAC,QAAQ,KAAK,IAAI,QAAQ,OAAO;AAAA,MACnF,CAAC,SAAS,KAAK,MAAM,GAAG,eAAe;AAAA,IACzC;AACA,kBAAc,iBAAiB,EAAE;AAAA,MAC/B,OAAO,WACJ,MAAM,OAAO,SAAS,sBAAsB,KAAK,MAAM,OAAO;AAAA,QAC7D,UAAU,KAAK,MAAM;AAAA,QACrB,OAAO;AAAA,MACT,CAAC;AAAA,IACL;AAEA,cAAU,MAAM,MAAM;AACpB,kBAAY;AAAA,IACd,CAAC;AACD,UAAM,UAAU,MAAM;AAEtB,UAAM,SAAS,MAAM,QAAQ,QAAQ,EAAE,SAAS,OAAO,WAAW,KAAK,CAAC;AACxE,WAAO,EAAE,GAAG,KAAK,OAAO,SAAS,OAAO,OAAO,EAAE;AAAA,EACnD;AAAA,EAEA,MAAM,eAAgD;AACpD,UAAM,IAAI,MAAM,0EAA0E;AAAA,EAC5F;AACF;AASA,IAAM,YAAY,oBAAI,IAA+B;AAOrD,eAAsB,cACpB,OACA,OACA,WACmB;AACnB,MAAI,UAAU,WAAW,EAAG,QAAO,CAAC;AACpC,MAAI,UAAU,UAAU,IAAI,MAAM,EAAE;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,iBAAiB,EAAE,KAAK,OAAO,WAAW;AAClD,YAAM,UAAU,MAAM,WAAW,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AACjE,YAAM,CAACA,YAAWC,WAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,QAChD,OAAO,cAAc,gBAAgB,MAAM,IAAI,OAAO;AAAA,QACtD,OAAO,mCAAmC,gBAAgB,MAAM,IAAI;AAAA,UAClE,GAAG;AAAA,UACH,OAAO;AAAA,QACT,CAAC;AAAA,MACH,CAAC;AACD,aAAO,EAAE,WAAAD,YAAW,OAAOC,YAAW;AAAA,IACxC,CAAC;AACD,cAAU,IAAI,MAAM,IAAI,OAAO;AAC/B,YAAQ,MAAM,MAAM,UAAU,OAAO,MAAM,EAAE,CAAC;AAAA,EAChD;AACA,QAAM,EAAE,WAAW,OAAO,WAAW,IAAI,MAAM;AAC/C,QAAM,SAAS;AAAA,IACb,UAAU,IAAI,MAAM,MAAM,MAAM,GAAG,eAAe,CAAC;AAAA,IACnD;AAAA,MACE,WAAW,UAAU,IAAI,CAAC,aAAa,SAAS,MAAM,GAAG,eAAe,CAAC;AAAA,MACzE,SAAS;AAAA,MACT,YAAY;AAAA,IACd;AAAA,EACF;AACA,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,MAAM;AAC1C,SAAO,OAAO,OAAO,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,OAAO,iBAAiB;AACxE;","names":["tokenizer","classifier"]}
@@ -0,0 +1,103 @@
1
+ import {
2
+ getModelContextWindow
3
+ } from "./chunk-JMWJ7V6Y.js";
4
+
5
+ // src/planner/budget.ts
6
+ import { Tiktoken } from "js-tiktoken/lite";
7
+ import cl100kBase from "js-tiktoken/ranks/cl100k_base";
8
+ var encoder = null;
9
+ function getEncoder() {
10
+ if (!encoder) {
11
+ encoder = new Tiktoken(cl100kBase);
12
+ }
13
+ return encoder;
14
+ }
15
+ var PIECE = new RegExp(cl100kBase.pat_str, "gu");
16
+ var SPECIAL_TOKENS = Object.keys(cl100kBase.special_tokens);
17
+ var MAX_CACHED_PIECES = 2e5;
18
+ var pieceTokens = /* @__PURE__ */ new Map();
19
+ function countTokens(text) {
20
+ if (!text) return 0;
21
+ try {
22
+ if (SPECIAL_TOKENS.some((token) => text.includes(token))) throw new Error("special token");
23
+ let total = 0;
24
+ for (const [piece] of text.matchAll(PIECE)) {
25
+ let count = pieceTokens.get(piece);
26
+ if (count === void 0) {
27
+ count = getEncoder().encode(piece).length;
28
+ if (pieceTokens.size >= MAX_CACHED_PIECES) pieceTokens.clear();
29
+ pieceTokens.set(piece, count);
30
+ }
31
+ total += count;
32
+ }
33
+ return total;
34
+ } catch {
35
+ return Math.max(1, Math.ceil(text.length / 4));
36
+ }
37
+ }
38
+ function clampBudgetForCallerModel(requestedBudget, callerModel) {
39
+ if (!callerModel) {
40
+ return { budget: requestedBudget, clamped: false };
41
+ }
42
+ const ctx = getModelContextWindow(callerModel);
43
+ if (!ctx) {
44
+ return { budget: requestedBudget, clamped: false };
45
+ }
46
+ const headroom = Math.floor(ctx * 0.75);
47
+ if (requestedBudget > headroom) {
48
+ return {
49
+ budget: headroom,
50
+ clamped: true,
51
+ reason: `budget ${requestedBudget} exceeds 75% of ${callerModel} context window (${ctx}); clamped to ${headroom}.`
52
+ };
53
+ }
54
+ return { budget: requestedBudget, clamped: false };
55
+ }
56
+
57
+ // src/core/ids.ts
58
+ import { createHash, randomBytes } from "crypto";
59
+ var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
60
+ function generateUlid(now = Date.now()) {
61
+ let timePart = "";
62
+ let ts = now;
63
+ for (let i = 0; i < 10; i++) {
64
+ timePart = (CROCKFORD[ts % 32] ?? "0") + timePart;
65
+ ts = Math.floor(ts / 32);
66
+ }
67
+ const bytes = randomBytes(10);
68
+ let randPart = "";
69
+ for (let i = 0; i < 16; i++) {
70
+ const bitOffset = i * 5;
71
+ const byteIndex = Math.floor(bitOffset / 8);
72
+ const bitInByte = bitOffset % 8;
73
+ const hi = bytes[byteIndex] ?? 0;
74
+ const lo = bytes[byteIndex + 1] ?? 0;
75
+ const combined = (hi << 8 | lo) >>> 0;
76
+ const shift = 16 - bitInByte - 5;
77
+ const value = combined >>> shift & 31;
78
+ randPart += CROCKFORD[value] ?? "0";
79
+ }
80
+ return timePart + randPart;
81
+ }
82
+ function generateSessionId(now) {
83
+ return `sess_${generateUlid(now).toLowerCase()}`;
84
+ }
85
+ function generateEntityId(prefix, now) {
86
+ return `${prefix}_${generateUlid(now).toLowerCase()}`;
87
+ }
88
+ function computeContentHash(namespace, sessionId, content) {
89
+ return createHash("sha256").update(`${namespace}${sessionId}${content}`).digest("hex");
90
+ }
91
+ function computeFactHash(namespace, content) {
92
+ return createHash("sha256").update(`${namespace}fact${content}`).digest("hex");
93
+ }
94
+
95
+ export {
96
+ countTokens,
97
+ clampBudgetForCallerModel,
98
+ generateSessionId,
99
+ generateEntityId,
100
+ computeContentHash,
101
+ computeFactHash
102
+ };
103
+ //# sourceMappingURL=chunk-4BYGTEJT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/planner/budget.ts","../src/core/ids.ts"],"sourcesContent":["import { Tiktoken } from \"js-tiktoken/lite\";\nimport cl100kBase from \"js-tiktoken/ranks/cl100k_base\";\nimport { getModelContextWindow } from \"../providers/models.js\";\n\nlet encoder: Tiktoken | null = null;\n\nfunction getEncoder(): Tiktoken {\n if (!encoder) {\n encoder = new Tiktoken(cl100kBase);\n }\n return encoder;\n}\n\n/** cl100k pre-tokenizer: BPE merges never cross these pieces, so counts add up. */\nconst PIECE = new RegExp(cl100kBase.pat_str, \"gu\");\nconst SPECIAL_TOKENS = Object.keys(cl100kBase.special_tokens);\n// ponytail: cleared when full rather than LRU; source and JSON pieces repeat\n// heavily, so a simple bound keeps the hit rate high.\nconst MAX_CACHED_PIECES = 200_000;\nconst pieceTokens = new Map<string, number>();\n\n/**\n * Exact cl100k token count. A context request tokenizes nearly the same JSON\n * several times (stats, stages, budget fixed point, delivery), so BPE results\n * are memoized per pre-tokenized piece; the sum equals `encode(text).length`.\n */\nexport function countTokens(text: string): number {\n if (!text) return 0;\n try {\n // `encode` rejects special-token text; keep the same fallback for it.\n if (SPECIAL_TOKENS.some((token) => text.includes(token))) throw new Error(\"special token\");\n let total = 0;\n for (const [piece] of text.matchAll(PIECE)) {\n let count = pieceTokens.get(piece);\n if (count === undefined) {\n count = getEncoder().encode(piece).length;\n if (pieceTokens.size >= MAX_CACHED_PIECES) pieceTokens.clear();\n pieceTokens.set(piece, count);\n }\n total += count;\n }\n return total;\n } catch {\n return Math.max(1, Math.ceil(text.length / 4));\n }\n}\n\nexport type BudgetClampResult = {\n budget: number;\n clamped: boolean;\n reason?: string;\n};\n\nexport function clampBudgetForCallerModel(\n requestedBudget: number,\n callerModel: string | undefined,\n): BudgetClampResult {\n if (!callerModel) {\n return { budget: requestedBudget, clamped: false };\n }\n const ctx = getModelContextWindow(callerModel);\n if (!ctx) {\n return { budget: requestedBudget, clamped: false };\n }\n const headroom = Math.floor(ctx * 0.75);\n if (requestedBudget > headroom) {\n return {\n budget: headroom,\n clamped: true,\n reason: `budget ${requestedBudget} exceeds 75% of ${callerModel} context window (${ctx}); clamped to ${headroom}.`,\n };\n }\n return { budget: requestedBudget, clamped: false };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nconst CROCKFORD = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nexport function generateUlid(now: number = Date.now()): string {\n let timePart = \"\";\n let ts = now;\n for (let i = 0; i < 10; i++) {\n timePart = (CROCKFORD[ts % 32] ?? \"0\") + timePart;\n ts = Math.floor(ts / 32);\n }\n\n const bytes = randomBytes(10);\n let randPart = \"\";\n for (let i = 0; i < 16; i++) {\n const bitOffset = i * 5;\n const byteIndex = Math.floor(bitOffset / 8);\n const bitInByte = bitOffset % 8;\n const hi = bytes[byteIndex] ?? 0;\n const lo = bytes[byteIndex + 1] ?? 0;\n const combined = ((hi << 8) | lo) >>> 0;\n const shift = 16 - bitInByte - 5;\n const value = (combined >>> shift) & 0x1f;\n randPart += CROCKFORD[value] ?? \"0\";\n }\n\n return timePart + randPart;\n}\n\nexport function generateSessionId(now?: number): string {\n return `sess_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function generateEntityId(prefix: string, now?: number): string {\n return `${prefix}_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function computeContentHash(namespace: string, sessionId: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1f${sessionId}\\x1f${content}`).digest(\"hex\");\n}\n\nexport function computeFactHash(namespace: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1ffact\\x1f${content}`).digest(\"hex\");\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readConfigFile
3
- } from "./chunk-K63EOA4B.js";
3
+ } from "./chunk-776EQXDV.js";
4
4
 
5
5
  // src/llm/config.ts
6
6
  var BASE_URL = {
@@ -60,4 +60,4 @@ export {
60
60
  resolveLlmConfig,
61
61
  redactLlmKeys
62
62
  };
63
- //# sourceMappingURL=chunk-ITGRANYW.js.map
63
+ //# sourceMappingURL=chunk-6MKEIDX5.js.map
@@ -95,8 +95,15 @@ var fileSchema = z.object({
95
95
  namespace: z.string().min(1).optional(),
96
96
  tokenBudget: z.number().int().positive().optional(),
97
97
  provider: z.object({
98
- type: z.literal("huggingface").default("huggingface"),
99
- apiKey: z.string().min(1).optional()
98
+ type: z.enum(["local", "huggingface", "openrouter"]).default("local"),
99
+ apiKey: z.string().min(1).optional(),
100
+ model: z.string().min(1).optional(),
101
+ relevanceFloor: z.number().min(-1).max(1).optional()
102
+ }).optional(),
103
+ /** Optional cross-encoder that orders recalled facts (off by default). */
104
+ rerank: z.object({
105
+ provider: z.enum(["off", "local", "huggingface", "openrouter"]).default("off"),
106
+ model: z.string().min(1).optional()
100
107
  }).optional(),
101
108
  plan: z.object({
102
109
  policy: z.enum(PLAN_POLICIES).default("suggest")
@@ -175,7 +182,7 @@ async function initConfigFile(cwd = process.cwd()) {
175
182
  // Default to the folder name (sanitized) so a fresh scaffold agrees with
176
183
  // doctor's expected default instead of warning immediately after init.
177
184
  namespace: defaultNamespaceForCwd(cwd),
178
- provider: { type: "huggingface" },
185
+ provider: { type: "local" },
179
186
  tokenBudget: 4e3
180
187
  };
181
188
  await writeFile(path, `${JSON.stringify(template, null, 2)}
@@ -227,8 +234,10 @@ async function loadRuntimeConfig(cwd = process.cwd(), options = {}) {
227
234
  namespace: process.env.CODEBUDDY_NAMESPACE ?? file.namespace ?? defaultNamespaceForCwd(projectRoot),
228
235
  projectRoot,
229
236
  provider: {
230
- type: "huggingface",
231
- ...hfToken ? { apiKey: hfToken } : {}
237
+ type: file.provider?.type ?? "local",
238
+ ...hfToken ? { apiKey: hfToken } : {},
239
+ ...file.provider?.model ? { model: file.provider.model } : {},
240
+ ...file.provider?.relevanceFloor !== void 0 ? { relevanceFloor: file.provider.relevanceFloor } : {}
232
241
  },
233
242
  ...file.tokenBudget ? { tokenBudget: file.tokenBudget } : {},
234
243
  ...file.graphify ? { graphify: file.graphify } : {}
@@ -355,6 +364,15 @@ async function setLlmEnabled(enabled, options = {}, cwd = process.cwd()) {
355
364
  await writeAtomic(path, `${JSON.stringify(raw, null, 2)}
356
365
  `, { mode: 384 });
357
366
  }
367
+ async function setConfigBlock(key, value, cwd = process.cwd()) {
368
+ await initConfigFile(cwd);
369
+ const path = configPath(cwd);
370
+ const raw = JSON.parse(await readFile(path, "utf8"));
371
+ raw[key] = value;
372
+ fileSchema.parse(raw);
373
+ await writeAtomic(path, `${JSON.stringify(raw, null, 2)}
374
+ `, { mode: 384 });
375
+ }
358
376
  function redactSecrets(value) {
359
377
  if (typeof value === "string") {
360
378
  let redacted = value;
@@ -411,6 +429,7 @@ export {
411
429
  setPlanPolicy,
412
430
  setZvecEnabled,
413
431
  setLlmEnabled,
432
+ setConfigBlock,
414
433
  redactSecrets
415
434
  };
416
- //# sourceMappingURL=chunk-K63EOA4B.js.map
435
+ //# sourceMappingURL=chunk-776EQXDV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/markdown-store-fs.ts","../src/core/config-file.ts"],"sourcesContent":["import { lstat, open, rename } from \"node:fs/promises\";\nimport { join, relative, resolve, sep } from \"node:path\";\n\n/**\n * Filesystem helpers shared by the Markdown-backed stores\n * (`MemoryFileStore`, `PlanFileStore`). These enforce the security\n * invariants that keep `.codebuddy/` writes inside the repository root:\n * no path traversal, no symlinked path components, and crash-safe atomic\n * writes. Keep this module small and audited; both stores depend on it.\n */\n\n/**\n * Resolve `path` and assert it stays inside `root`. Returns the absolute\n * path. Throws if the resolved path escapes the root.\n */\nexport function resolveWithinRoot(root: string, path: string): string {\n const absoluteRoot = resolve(root);\n const absolute = resolve(path);\n const relativePath = relative(absoluteRoot, absolute);\n if (relativePath === \"..\" || relativePath.startsWith(`..${sep}`) || relativePath === \"\") {\n throw new Error(`Path escapes repository root: ${path}`);\n }\n return absolute;\n}\n\n/**\n * Walk each path component between `root` and `target` and refuse if any\n * component is a symbolic link. Prevents a planted symlink from\n * redirecting a write outside the repository. Missing components are\n * fine — the caller creates them.\n */\nexport async function assertNoSymlinkBetween(root: string, target: string): Promise<void> {\n const absoluteRoot = resolve(root);\n const absoluteTarget = resolve(target);\n const relativeTarget = relative(absoluteRoot, absoluteTarget);\n if (relativeTarget === \"..\" || relativeTarget.startsWith(`..${sep}`)) {\n throw new Error(`Path escapes repository root: ${target}`);\n }\n\n let current = absoluteRoot;\n const parts = relativeTarget.split(sep).filter(Boolean);\n for (const part of parts) {\n current = join(current, part);\n try {\n const info = await lstat(current);\n if (info.isSymbolicLink()) {\n throw new Error(`Refusing to use symlinked path: ${current}`);\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return;\n throw error;\n }\n }\n}\n\n/**\n * Write `document` to `path` atomically: write to a unique temp file with\n * an exclusive (`wx`) create, fsync, then rename into place. A crash mid-\n * write leaves either the old file or nothing — never a partial file.\n *\n * `mode` sets permissions on NEWLY created files (an existing file keeps its\n * mode, matching rename semantics). Secrets-bearing configs should use 0o600.\n */\nexport async function writeAtomic(\n path: string,\n document: string,\n options?: { mode?: number },\n): Promise<void> {\n const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;\n const handle = await open(temporaryPath, \"wx\", options?.mode ?? 0o600);\n try {\n await handle.writeFile(document, \"utf8\");\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(temporaryPath, path);\n}\n\n/**\n * Split a Markdown document with leading YAML front matter into its\n * front-matter text and body. Throws on malformed or unterminated\n * front matter so hand-edited files fail loudly rather than silently\n * losing fields.\n */\nexport function parseFrontMatter(document: string): { frontMatter: string; content: string } {\n if (!document.startsWith(\"---\\n\")) {\n throw new Error(\"Markdown file must start with YAML front matter.\");\n }\n const closingIndex = document.indexOf(\"\\n---\\n\", 4);\n if (closingIndex === -1) {\n throw new Error(\"Markdown file has unterminated YAML front matter.\");\n }\n return {\n frontMatter: document.slice(4, closingIndex),\n content: document.slice(closingIndex + 5),\n };\n}\n\n/**\n * Compose a Markdown document from serialized front-matter YAML and a\n * body. Ensures exactly one trailing newline on the body so files are\n * stable across rewrites (minimal git diffs).\n */\nexport function composeMarkdown(frontMatterYaml: string, body: string): string {\n const trimmedFrontMatter = frontMatterYaml.trimEnd();\n const content = body.endsWith(\"\\n\") ? body : `${body}\\n`;\n return `---\\n${trimmedFrontMatter}\\n---\\n${content}`;\n}\n","import { constants } from \"node:fs\";\nimport { access, chmod, mkdir, readFile, stat, writeFile } from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport { z } from \"zod\";\nimport { writeAtomic } from \"./markdown-store-fs.js\";\nimport type { CodeBuddyConfig } from \"./types.js\";\n\nexport const CONFIG_DIR = \".codebuddy\";\nexport const CONFIG_FILE = \"config.json\";\nexport const POLICY_FILE = \"policies.yaml\";\n\nconst PROJECT_GITIGNORE = [\n \"# CodeBuddy local-only files\",\n \"/config.json\",\n \"/cache/\",\n \"/memory/review/\",\n \"/memory/review-archive/\",\n \"/plans/.locks/\",\n \"/memory/.locks/\",\n \"\",\n \"# Keep these reviewable project artifacts shareable by default:\",\n \"# /memory/facts/*.md\",\n \"# /memory/summaries/*.md\",\n \"# /plans/*.md\",\n \"# If your project memory is private, ignore /memory/ in the repo root .gitignore.\",\n \"\",\n].join(\"\\n\");\n\nconst DEFAULT_POLICY_FILE = [\n \"rules:\",\n \" - id: auth-sensitive\",\n ' pattern: \"src/auth/**\"',\n ' message: \"Auth changes need careful review.\"',\n \" weight: 70\",\n \"\",\n].join(\"\\n\");\n\n/**\n * Plan policy controls how strongly CodeBuddy nudges agents toward writing\n * a plan before substantive work:\n * - off: plans are never suggested or required.\n * - suggest: agents are reminded to plan, but small tasks stay lightweight (default).\n * - required: agents should refuse substantive changes without an active plan.\n */\nexport const PLAN_POLICIES = [\"off\", \"suggest\", \"required\"] as const;\nexport type PlanPolicy = (typeof PLAN_POLICIES)[number];\n\nconst fileSchema = z.object({\n postgresUrl: z.string().min(1).optional(),\n namespace: z.string().min(1).optional(),\n tokenBudget: z.number().int().positive().optional(),\n provider: z\n .object({\n type: z.enum([\"local\", \"huggingface\", \"openrouter\"]).default(\"local\"),\n apiKey: z.string().min(1).optional(),\n model: z.string().min(1).optional(),\n relevanceFloor: z.number().min(-1).max(1).optional(),\n })\n .optional(),\n /** Optional cross-encoder that orders recalled facts (off by default). */\n rerank: z\n .object({\n provider: z.enum([\"off\", \"local\", \"huggingface\", \"openrouter\"]).default(\"off\"),\n model: z.string().min(1).optional(),\n })\n .optional(),\n plan: z\n .object({\n policy: z.enum(PLAN_POLICIES).default(\"suggest\"),\n })\n .optional(),\n graphify: z\n .object({\n enabled: z.boolean(),\n graphPath: z.string().min(1).optional(),\n })\n .optional(),\n /**\n * Optional hosted small-model layer (bring your own key). Keys are read from\n * the environment only, never from this file (`src/llm/config.ts`).\n */\n llm: z\n .object({\n enabled: z.boolean(),\n provider: z.enum([\"groq\", \"openrouter\"]).optional(),\n model: z.string().min(1).optional(),\n fallback: z\n .object({ provider: z.enum([\"groq\", \"openrouter\"]), model: z.string().min(1) })\n .optional(),\n timeoutMs: z.number().int().min(100).max(5_000).optional(),\n dailyTokenBudget: z.number().int().positive().optional(),\n features: z\n .object({\n summaries: z.boolean().optional(),\n rerank: z.boolean().optional(),\n memory: z.boolean().optional(),\n })\n .optional(),\n })\n .optional(),\n /** Optional local semantic retrieval via zvec-grep (`zg`). Off unless enabled. */\n zvec: z\n .object({\n enabled: z.boolean(),\n binary: z.string().min(1).optional(),\n timeoutMs: z.number().int().positive().max(10_000).optional(),\n })\n .optional(),\n});\n\nexport type CodeBuddyFileConfig = z.infer<typeof fileSchema>;\n\nexport type ConfigPermissionReport = {\n path: string;\n exists: boolean;\n mode: string | null;\n ok: boolean;\n warning?: string;\n};\n\nexport type ProjectScaffoldReport = {\n codebuddyDir: { path: string; exists: boolean; ok: boolean; warning?: string };\n memoryFactsDir: { path: string; exists: boolean; ok: boolean; warning?: string };\n memorySummariesDir: { path: string; exists: boolean; ok: boolean; warning?: string };\n plansDir: { path: string; exists: boolean; ok: boolean; warning?: string };\n gitignore: { path: string; exists: boolean; ok: boolean; warning?: string };\n policies: { path: string; exists: boolean; ok: boolean; warning?: string };\n};\n\nexport function configPath(cwd = process.cwd()): string {\n return join(cwd, CONFIG_DIR, CONFIG_FILE);\n}\n\nexport async function initProjectScaffold(cwd = process.cwd()): Promise<{\n codebuddyDir: string;\n gitignorePath: string;\n policyPath: string;\n}> {\n const codebuddyDir = join(cwd, CONFIG_DIR);\n await mkdir(join(codebuddyDir, \"memory\", \"facts\"), { recursive: true, mode: 0o700 });\n await mkdir(join(codebuddyDir, \"memory\", \"summaries\"), { recursive: true, mode: 0o700 });\n await mkdir(join(codebuddyDir, \"plans\"), { recursive: true, mode: 0o700 });\n\n const gitignorePath = join(codebuddyDir, \".gitignore\");\n try {\n await access(gitignorePath, constants.F_OK);\n let gitignore = await readFile(gitignorePath, \"utf8\");\n const lines = new Set(gitignore.split(/\\r?\\n/));\n const missing = [\"/memory/review/\", \"/memory/review-archive/\"].filter(\n (line) => !lines.has(line),\n );\n if (missing.length > 0) {\n gitignore = `${gitignore.trimEnd()}\\n${missing.join(\"\\n\")}\\n`;\n await writeFile(gitignorePath, gitignore, { mode: 0o644 });\n }\n } catch {\n await writeFile(gitignorePath, PROJECT_GITIGNORE, { mode: 0o644 });\n }\n\n const policyPath = join(codebuddyDir, POLICY_FILE);\n try {\n await access(policyPath, constants.F_OK);\n } catch {\n await writeFile(policyPath, DEFAULT_POLICY_FILE, { mode: 0o644 });\n }\n\n return { codebuddyDir, gitignorePath, policyPath };\n}\n\nexport async function initConfigFile(\n cwd = process.cwd(),\n): Promise<{ path: string; created: boolean }> {\n const path = configPath(cwd);\n await initProjectScaffold(cwd);\n try {\n await access(path, constants.F_OK);\n await chmod(path, 0o600);\n return { path, created: false };\n } catch {\n const template: CodeBuddyFileConfig = {\n postgresUrl: process.env.DATABASE_URL ?? \"postgres://localhost:5432/codebuddy\",\n // Default to the folder name (sanitized) so a fresh scaffold agrees with\n // doctor's expected default instead of warning immediately after init.\n namespace: defaultNamespaceForCwd(cwd),\n provider: { type: \"local\" },\n tokenBudget: 4000,\n };\n await writeFile(path, `${JSON.stringify(template, null, 2)}\\n`, { mode: 0o600 });\n await chmod(path, 0o600);\n return { path, created: true };\n }\n}\n\n/** Persist a complete project config without changing its private permissions. */\nexport async function writeConfigFile(\n config: CodeBuddyFileConfig,\n cwd = process.cwd(),\n): Promise<{ path: string }> {\n const path = configPath(cwd);\n await initProjectScaffold(cwd);\n await writeFile(path, `${JSON.stringify(config, null, 2)}\\n`, { mode: 0o600 });\n await chmod(path, 0o600);\n return { path };\n}\n\nexport async function readConfigFile(cwd = process.cwd()): Promise<CodeBuddyFileConfig> {\n const path = configPath(cwd);\n try {\n const raw = await readFile(path, \"utf8\");\n return fileSchema.parse(JSON.parse(raw));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return {};\n throw error;\n }\n}\n\n/**\n * The ONE namespace-resolution rule, shared by the DB/MCP runtime, every\n * file-based CLI command, and doctor. Precedence:\n * 1. CODEBUDDY_NAMESPACE env\n * 2. `.codebuddy/config.json` `namespace`\n * 3. the current folder name (sanitized) — matching what doctor reports as\n * the expected default.\n */\nexport function sanitizeNamespace(value: string): string {\n return value.replace(/[^a-zA-Z0-9_.:-]/g, \"-\").slice(0, 128);\n}\n\n/** Folder-name fallback used when neither env nor config specify a namespace. */\nexport function defaultNamespaceForCwd(cwd = process.cwd()): string {\n return sanitizeNamespace(basename(cwd)) || \"default\";\n}\n\nexport async function resolveNamespaceFrom(cwd = process.cwd()): Promise<string> {\n if (process.env.CODEBUDDY_NAMESPACE) return process.env.CODEBUDDY_NAMESPACE;\n const file = await readConfigFile(cwd);\n return file.namespace ?? defaultNamespaceForCwd(cwd);\n}\n\nexport const MISSING_DATABASE_URL =\n \"DATABASE_URL or .codebuddy/config.json postgresUrl is required.\";\n\n/**\n * `requireDatabase: false` (the MCP server) returns an empty `postgresUrl`\n * instead of throwing, so file-based tools still run without PostgreSQL;\n * `createRuntime` rejects the empty URL with the same message.\n */\nexport async function loadRuntimeConfig(\n cwd = process.cwd(),\n options: { requireDatabase?: boolean } = {},\n): Promise<CodeBuddyConfig> {\n const projectRoot = process.env.CODEBUDDY_PROJECT_ROOT ?? cwd;\n const file = await readConfigFile(projectRoot);\n const postgresUrl = process.env.DATABASE_URL ?? file.postgresUrl ?? \"\";\n if (!postgresUrl && options.requireDatabase !== false) {\n throw new Error(MISSING_DATABASE_URL);\n }\n const hfToken = process.env.HF_TOKEN ?? file.provider?.apiKey;\n return {\n postgresUrl,\n namespace:\n process.env.CODEBUDDY_NAMESPACE ?? file.namespace ?? defaultNamespaceForCwd(projectRoot),\n projectRoot,\n provider: {\n type: file.provider?.type ?? \"local\",\n ...(hfToken ? { apiKey: hfToken } : {}),\n ...(file.provider?.model ? { model: file.provider.model } : {}),\n ...(file.provider?.relevanceFloor !== undefined\n ? { relevanceFloor: file.provider.relevanceFloor }\n : {}),\n },\n ...(file.tokenBudget ? { tokenBudget: file.tokenBudget } : {}),\n ...(file.graphify ? { graphify: file.graphify } : {}),\n };\n}\n\nexport async function checkConfigPermissions(cwd = process.cwd()): Promise<ConfigPermissionReport> {\n const path = configPath(cwd);\n try {\n const info = await stat(path);\n const modeNumber = info.mode & 0o777;\n const mode = modeNumber.toString(8).padStart(4, \"0\");\n const ok = modeNumber === 0o600;\n return {\n path,\n exists: true,\n mode,\n ok,\n ...(ok ? {} : { warning: \".codebuddy/config.json should be chmod 0600.\" }),\n };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { path, exists: false, mode: null, ok: true };\n }\n throw error;\n }\n}\n\nexport async function checkProjectScaffold(cwd = process.cwd()): Promise<ProjectScaffoldReport> {\n const codebuddyDir = join(cwd, CONFIG_DIR);\n const memoryFactsDir = join(codebuddyDir, \"memory\", \"facts\");\n const memorySummariesDir = join(codebuddyDir, \"memory\", \"summaries\");\n const plansDir = join(codebuddyDir, \"plans\");\n const gitignorePath = join(codebuddyDir, \".gitignore\");\n const policyPath = join(codebuddyDir, POLICY_FILE);\n\n const [\n codebuddyDirExists,\n memoryFactsDirExists,\n memorySummariesDirExists,\n plansDirExists,\n gitignoreExists,\n policiesExist,\n ] = await Promise.all([\n pathExists(codebuddyDir),\n pathExists(memoryFactsDir),\n pathExists(memorySummariesDir),\n pathExists(plansDir),\n pathExists(gitignorePath),\n pathExists(policyPath),\n ]);\n\n return {\n codebuddyDir: {\n path: codebuddyDir,\n exists: codebuddyDirExists,\n ok: codebuddyDirExists,\n ...(codebuddyDirExists\n ? {}\n : { warning: \"Run `codebuddy use` or `codebuddy init` to create the project scaffold.\" }),\n },\n memoryFactsDir: {\n path: memoryFactsDir,\n exists: memoryFactsDirExists,\n ok: memoryFactsDirExists,\n ...(memoryFactsDirExists ? {} : { warning: \"Missing .codebuddy/memory/facts directory.\" }),\n },\n memorySummariesDir: {\n path: memorySummariesDir,\n exists: memorySummariesDirExists,\n ok: memorySummariesDirExists,\n ...(memorySummariesDirExists\n ? {}\n : { warning: \"Missing .codebuddy/memory/summaries directory.\" }),\n },\n plansDir: {\n path: plansDir,\n exists: plansDirExists,\n ok: plansDirExists,\n ...(plansDirExists ? {} : { warning: \"Missing .codebuddy/plans directory.\" }),\n },\n gitignore: {\n path: gitignorePath,\n exists: gitignoreExists,\n ok: gitignoreExists,\n ...(gitignoreExists ? {} : { warning: \"Missing .codebuddy/.gitignore.\" }),\n },\n policies: {\n path: policyPath,\n exists: policiesExist,\n ok: policiesExist,\n ...(policiesExist\n ? {}\n : {\n warning:\n \"Missing .codebuddy/policies.yaml. Run `codebuddy use` or `codebuddy init` to scaffold it.\",\n }),\n },\n };\n}\n\n/** Read the plan policy, honoring CODEBUDDY_PLAN_POLICY then the config file. */\nexport async function loadPlanPolicy(cwd = process.cwd()): Promise<PlanPolicy> {\n const fromEnv = process.env.CODEBUDDY_PLAN_POLICY;\n if (fromEnv && (PLAN_POLICIES as readonly string[]).includes(fromEnv)) {\n return fromEnv as PlanPolicy;\n }\n const file = await readConfigFile(cwd);\n return file.plan?.policy ?? \"suggest\";\n}\n\n/** Persist the plan policy into `.codebuddy/config.json`, creating it if needed. */\nexport async function setPlanPolicy(policy: PlanPolicy, cwd = process.cwd()): Promise<void> {\n await initConfigFile(cwd);\n const path = configPath(cwd);\n const raw = JSON.parse(await readFile(path, \"utf8\")) as Record<string, unknown>;\n raw.plan = { ...(raw.plan as Record<string, unknown> | undefined), policy };\n // Atomic rewrite: this file holds the postgresUrl and provider apiKey.\n await writeAtomic(path, `${JSON.stringify(raw, null, 2)}\\n`, { mode: 0o600 });\n}\n\n/** Turn optional zvec-grep retrieval on or off in `.codebuddy/config.json`. */\nexport async function setZvecEnabled(enabled: boolean, cwd = process.cwd()): Promise<void> {\n await initConfigFile(cwd);\n const path = configPath(cwd);\n const raw = JSON.parse(await readFile(path, \"utf8\")) as Record<string, unknown>;\n raw.zvec = { ...(raw.zvec as Record<string, unknown> | undefined), enabled };\n await writeAtomic(path, `${JSON.stringify(raw, null, 2)}\\n`, { mode: 0o600 });\n}\n\n/** Turn the BYOK LLM layer on or off; never writes a key (keys live in the environment). */\nexport async function setLlmEnabled(\n enabled: boolean,\n options: { provider?: \"groq\" | \"openrouter\"; model?: string } = {},\n cwd = process.cwd(),\n): Promise<void> {\n await initConfigFile(cwd);\n const path = configPath(cwd);\n const raw = JSON.parse(await readFile(path, \"utf8\")) as Record<string, unknown>;\n raw.llm = {\n ...(raw.llm as Record<string, unknown> | undefined),\n enabled,\n ...(options.provider ? { provider: options.provider } : {}),\n ...(options.model ? { model: options.model } : {}),\n };\n await writeAtomic(path, `${JSON.stringify(raw, null, 2)}\\n`, { mode: 0o600 });\n}\n\n/**\n * Replace one top-level block of `.codebuddy/config.json` (`provider`,\n * `rerank`), keeping every other key and the private file mode. The result\n * must still parse, so a bad value never reaches disk.\n */\nexport async function setConfigBlock(\n key: \"provider\" | \"rerank\",\n value: Record<string, unknown>,\n cwd = process.cwd(),\n): Promise<void> {\n await initConfigFile(cwd);\n const path = configPath(cwd);\n const raw = JSON.parse(await readFile(path, \"utf8\")) as Record<string, unknown>;\n raw[key] = value;\n fileSchema.parse(raw);\n await writeAtomic(path, `${JSON.stringify(raw, null, 2)}\\n`, { mode: 0o600 });\n}\n\nexport function redactSecrets(value: unknown): unknown {\n if (typeof value === \"string\") {\n let redacted = value;\n // Provider keys (HF, BYOK LLM) literally anywhere in a string.\n for (const key of [\n process.env.HF_TOKEN,\n process.env.GROQ_API_KEY,\n process.env.OPENROUTER_API_KEY,\n process.env.CODEBUDDY_LLM_API_KEY,\n ]) {\n if (key) redacted = redacted.split(key).join(\"[REDACTED]\");\n }\n // Credentials embedded in connection URLs (postgres://user:pass@host…).\n // The password — not the username — is the secret.\n redacted = redacted.replace(\n /(\\b[a-z][a-z0-9+.-]*:\\/\\/[^:/\\s@]+:)[^/\\s@]+(@)/gi,\n \"$1[REDACTED]$2\",\n );\n return redacted;\n }\n if (Array.isArray(value)) return value.map(redactSecrets);\n if (!value || typeof value !== \"object\") return value;\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, nested]) => [\n key,\n /token|apiKey|authorization|secret/i.test(key) ? \"[REDACTED]\" : redactSecrets(nested),\n ]),\n );\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["join","join"]}
@@ -1,157 +1,10 @@
1
+ import {
2
+ countTokens,
3
+ generateEntityId
4
+ } from "./chunk-4BYGTEJT.js";
1
5
  import {
2
6
  writeAtomic
3
- } from "./chunk-K63EOA4B.js";
4
-
5
- // src/planner/budget.ts
6
- import { Tiktoken } from "js-tiktoken/lite";
7
- import cl100kBase from "js-tiktoken/ranks/cl100k_base";
8
-
9
- // src/providers/models.ts
10
- var EMBEDDING_DIMENSIONS = 384;
11
- var DEFAULT_EMBEDDING_MODELS = [
12
- {
13
- id: "sentence-transformers/all-MiniLM-L6-v2",
14
- role: "primary",
15
- kind: "embedding",
16
- dimensions: EMBEDDING_DIMENSIONS,
17
- dailyCallBudget: 1e3,
18
- supportsBatch: true
19
- },
20
- {
21
- id: "BAAI/bge-small-en-v1.5",
22
- role: "fallback",
23
- kind: "embedding",
24
- dimensions: EMBEDDING_DIMENSIONS,
25
- dailyCallBudget: 1e3,
26
- supportsBatch: true
27
- }
28
- ];
29
- var DEFAULT_LLM_MODELS = [
30
- {
31
- id: "meta-llama/Llama-3.2-3B-Instruct",
32
- role: "primary",
33
- kind: "llm",
34
- contextWindow: 131072,
35
- dailyCallBudget: 300
36
- },
37
- {
38
- id: "Qwen/Qwen2.5-7B-Instruct",
39
- role: "fallback",
40
- kind: "llm",
41
- contextWindow: 32768,
42
- dailyCallBudget: 300
43
- },
44
- {
45
- id: "mistralai/Mistral-7B-Instruct-v0.3",
46
- role: "last_resort",
47
- kind: "llm",
48
- contextWindow: 32768,
49
- dailyCallBudget: 300
50
- }
51
- ];
52
- var MODEL_REGISTRY = [...DEFAULT_EMBEDDING_MODELS, ...DEFAULT_LLM_MODELS];
53
- function getModelDefinition(modelId) {
54
- return MODEL_REGISTRY.find((model) => model.id === modelId);
55
- }
56
- function getModelContextWindow(modelId) {
57
- return getModelDefinition(modelId)?.contextWindow;
58
- }
59
- function assertCompatibleEmbeddingModels(models) {
60
- const mismatchedModel = models.find((model) => model.dimensions !== EMBEDDING_DIMENSIONS);
61
- if (mismatchedModel) {
62
- throw new Error(
63
- `Embedding model ${mismatchedModel.id} has ${mismatchedModel.dimensions ?? "unknown"} dimensions; expected ${EMBEDDING_DIMENSIONS}.`
64
- );
65
- }
66
- }
67
-
68
- // src/planner/budget.ts
69
- var encoder = null;
70
- function getEncoder() {
71
- if (!encoder) {
72
- encoder = new Tiktoken(cl100kBase);
73
- }
74
- return encoder;
75
- }
76
- var PIECE = new RegExp(cl100kBase.pat_str, "gu");
77
- var SPECIAL_TOKENS = Object.keys(cl100kBase.special_tokens);
78
- var MAX_CACHED_PIECES = 2e5;
79
- var pieceTokens = /* @__PURE__ */ new Map();
80
- function countTokens(text) {
81
- if (!text) return 0;
82
- try {
83
- if (SPECIAL_TOKENS.some((token) => text.includes(token))) throw new Error("special token");
84
- let total = 0;
85
- for (const [piece] of text.matchAll(PIECE)) {
86
- let count = pieceTokens.get(piece);
87
- if (count === void 0) {
88
- count = getEncoder().encode(piece).length;
89
- if (pieceTokens.size >= MAX_CACHED_PIECES) pieceTokens.clear();
90
- pieceTokens.set(piece, count);
91
- }
92
- total += count;
93
- }
94
- return total;
95
- } catch {
96
- return Math.max(1, Math.ceil(text.length / 4));
97
- }
98
- }
99
- function clampBudgetForCallerModel(requestedBudget, callerModel) {
100
- if (!callerModel) {
101
- return { budget: requestedBudget, clamped: false };
102
- }
103
- const ctx = getModelContextWindow(callerModel);
104
- if (!ctx) {
105
- return { budget: requestedBudget, clamped: false };
106
- }
107
- const headroom = Math.floor(ctx * 0.75);
108
- if (requestedBudget > headroom) {
109
- return {
110
- budget: headroom,
111
- clamped: true,
112
- reason: `budget ${requestedBudget} exceeds 75% of ${callerModel} context window (${ctx}); clamped to ${headroom}.`
113
- };
114
- }
115
- return { budget: requestedBudget, clamped: false };
116
- }
117
-
118
- // src/core/ids.ts
119
- import { createHash, randomBytes } from "crypto";
120
- var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
121
- function generateUlid(now = Date.now()) {
122
- let timePart = "";
123
- let ts = now;
124
- for (let i = 0; i < 10; i++) {
125
- timePart = (CROCKFORD[ts % 32] ?? "0") + timePart;
126
- ts = Math.floor(ts / 32);
127
- }
128
- const bytes = randomBytes(10);
129
- let randPart = "";
130
- for (let i = 0; i < 16; i++) {
131
- const bitOffset = i * 5;
132
- const byteIndex = Math.floor(bitOffset / 8);
133
- const bitInByte = bitOffset % 8;
134
- const hi = bytes[byteIndex] ?? 0;
135
- const lo = bytes[byteIndex + 1] ?? 0;
136
- const combined = (hi << 8 | lo) >>> 0;
137
- const shift = 16 - bitInByte - 5;
138
- const value = combined >>> shift & 31;
139
- randPart += CROCKFORD[value] ?? "0";
140
- }
141
- return timePart + randPart;
142
- }
143
- function generateSessionId(now) {
144
- return `sess_${generateUlid(now).toLowerCase()}`;
145
- }
146
- function generateEntityId(prefix, now) {
147
- return `${prefix}_${generateUlid(now).toLowerCase()}`;
148
- }
149
- function computeContentHash(namespace, sessionId, content) {
150
- return createHash("sha256").update(`${namespace}${sessionId}${content}`).digest("hex");
151
- }
152
- function computeFactHash(namespace, content) {
153
- return createHash("sha256").update(`${namespace}fact${content}`).digest("hex");
154
- }
7
+ } from "./chunk-776EQXDV.js";
155
8
 
156
9
  // src/savings/tokens.ts
157
10
  function estimateTokens(value) {
@@ -162,7 +15,7 @@ function estimateTokens(value) {
162
15
  }
163
16
 
164
17
  // src/context/session-ledger.ts
165
- import { createHash as createHash2 } from "crypto";
18
+ import { createHash } from "crypto";
166
19
  import { mkdir, readdir, readFile, rm, stat } from "fs/promises";
167
20
  import { join, resolve } from "path";
168
21
  var VERSION = 2;
@@ -406,7 +259,7 @@ function empty(now) {
406
259
  return { version: VERSION, updatedAt: now.toISOString(), records: {} };
407
260
  }
408
261
  function digest(text) {
409
- return createHash2("sha256").update(text).digest("hex");
262
+ return createHash("sha256").update(text).digest("hex");
410
263
  }
411
264
 
412
265
  // src/context/tests.ts
@@ -458,19 +311,9 @@ async function detectTestCommand(repositoryRoot) {
458
311
  }
459
312
 
460
313
  export {
461
- EMBEDDING_DIMENSIONS,
462
- DEFAULT_EMBEDDING_MODELS,
463
- DEFAULT_LLM_MODELS,
464
- assertCompatibleEmbeddingModels,
465
- countTokens,
466
- clampBudgetForCallerModel,
467
- generateSessionId,
468
- generateEntityId,
469
- computeContentHash,
470
- computeFactHash,
471
314
  estimateTokens,
472
315
  SessionCapsuleLedger,
473
316
  findCoveringTest,
474
317
  detectTestCommand
475
318
  };
476
- //# sourceMappingURL=chunk-5OWZVZSO.js.map
319
+ //# sourceMappingURL=chunk-CQPCHMBS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/savings/tokens.ts","../src/context/session-ledger.ts","../src/context/tests.ts"],"sourcesContent":["import { countTokens } from \"../planner/budget.js\";\n\n/**\n * The one place token counts are computed. Strings use the same deterministic\n * cl100k tokenizer as planner budgets; objects are measured by their JSON\n * serialization, which matches how they travel over MCP.\n */\n\nexport function estimateTokens(value: unknown): number {\n if (value === undefined || value === null) return 0;\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n if (!text) return 0;\n return countTokens(text);\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\n\nconst VERSION = 2;\nconst MAX_CAPSULES = 128;\nconst MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_PENDING = 8;\nconst PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst STALE_LOCK_MS = 60_000;\n/** Acknowledged versions kept per section, so switching back to a task reuses them. */\nconst MAX_ACKNOWLEDGED_PER_KEY = 8;\n\ntype LedgerRecord = { hash: string; lastSentAt: string };\n/** A delivery sent to a client that has not yet confirmed receipt. */\ntype PendingDelivery = { createdAt: string; records: Record<string, string> };\n/** One acknowledged version of an `exchange` section: content hash and when it was sent. */\ntype AcknowledgedVersion = { hash: string; sentAt: string };\ntype LedgerFile = {\n version: 2;\n updatedAt: string;\n /** `deliver`/`markSent` capsules (hooks): one current hash per key. */\n records: Record<string, LedgerRecord>;\n pending?: Record<string, PendingDelivery>;\n /** `exchange` sections (MCP): every recently acknowledged version, newest first. */\n acknowledged?: Record<string, AcknowledgedVersion[]>;\n};\n\nexport type ContextCapsule = { key: string; text: string };\nexport type CapsuleDelivery = {\n /** Full text of new or changed capsules, in input order. */\n lines: string[];\n /** Keys of capsules already sent this session and not resent. */\n unchanged: string[];\n full: number;\n referenced: number;\n sentTokens: number;\n avoidedTokens: number;\n};\n\nexport type SectionHash = { key: string; hash: string };\nexport type AcknowledgedExchange = {\n deliveryId: string;\n /** Keys whose content the client confirmed receiving and that have not changed. */\n unchanged: string[];\n ack: \"none\" | \"accepted\" | \"unknown\";\n};\n\nexport class SessionCapsuleLedger {\n private readonly directory: string;\n\n constructor(repositoryRoot = process.cwd()) {\n this.directory = join(resolve(repositoryRoot), \".codebuddy\", \"cache\", \"session\");\n }\n\n async deliver(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<CapsuleDelivery> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n const lines: string[] = [];\n const unchanged: string[] = [];\n let full = 0;\n let referenced = 0;\n let sentTokens = 0;\n let avoidedTokens = 0;\n\n for (const capsule of capsules) {\n const hash = digest(capsule.text);\n const existing = ledger.records[capsule.key];\n if (existing?.hash === hash) {\n unchanged.push(capsule.key);\n referenced++;\n avoidedTokens += estimateTokens(capsule.text);\n } else {\n lines.push(capsule.text);\n full++;\n sentTokens += estimateTokens(capsule.text);\n }\n ledger.records[capsule.key] = { hash, lastSentAt: now.toISOString() };\n }\n\n await this.write(path, ledger, now);\n return { lines, unchanged, full, referenced, sentTokens, avoidedTokens };\n } finally {\n await release();\n }\n }\n\n /** Keys of capsules whose current text was already recorded as sent this session. */\n async seen(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<Set<string>> {\n const ledger = await this.read(this.path(sessionId), now);\n return new Set(\n capsules\n .filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text))\n .map((capsule) => capsule.key),\n );\n }\n\n /**\n * Record capsules as delivered. Call only after their text actually reached\n * the client, so capsules that were clipped or failed to write stay unseen\n * and are delivered by a later prompt.\n */\n async markSent(sessionId: string, capsules: ContextCapsule[], now = new Date()): Promise<void> {\n if (capsules.length === 0) return;\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n for (const capsule of capsules) {\n ledger.records[capsule.key] = { hash: digest(capsule.text), lastSentAt: now.toISOString() };\n }\n await this.write(path, ledger, now);\n } finally {\n await release();\n }\n }\n\n /**\n * Acknowledged delivery for clients that can lose context (MCP). Unlike\n * `deliver`, sending marks nothing as seen: the sent hashes are held as a\n * pending delivery until the client echoes its `deliveryId` back as `ack` on\n * a later call. Only acknowledged, unchanged sections sent within `ttlMs` are\n * reported unchanged, so a dropped response or a compacted client context\n * fails safe to a full resend.\n *\n * A section keeps its last few acknowledged versions (by content hash), not\n * just the latest, so returning to an earlier task in the same session does\n * not resend what the client already holds. `full` means the client lost its\n * context: every acknowledged version and unconfirmed delivery is forgotten.\n */\n async exchange(\n sessionId: string,\n sections: SectionHash[],\n options: {\n ack?: string;\n ttlMs: number;\n /** The client lost its context: nothing acknowledged before this call counts. */\n full?: boolean;\n now?: Date;\n /** Finalize the response before recording hashes of the actual sent sections. */\n prepare?: (exchange: AcknowledgedExchange) => SectionHash[];\n },\n ): Promise<AcknowledgedExchange> {\n const now = options.now ?? new Date();\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n let pending = ledger.pending ?? {};\n let acknowledged = ledger.acknowledged ?? {};\n\n let ack: AcknowledgedExchange[\"ack\"] = \"none\";\n if (options.ack !== undefined) {\n const confirmed = pending[options.ack];\n if (confirmed) {\n for (const [key, hash] of Object.entries(confirmed.records)) {\n acknowledged[key] = [\n { hash, sentAt: confirmed.createdAt },\n ...(acknowledged[key] ?? []).filter((version) => version.hash !== hash),\n ]\n .sort((left, right) => right.sentAt.localeCompare(left.sentAt))\n .slice(0, MAX_ACKNOWLEDGED_PER_KEY);\n }\n delete pending[options.ack];\n ack = \"accepted\";\n } else {\n ack = \"unknown\";\n }\n }\n if (options.full) {\n acknowledged = {};\n pending = {};\n }\n\n const unchanged: string[] = [];\n const sent: Record<string, string> = {};\n for (const section of sections) {\n const held = acknowledged[section.key]?.some((version) => {\n const age = now.getTime() - Date.parse(version.sentAt);\n return version.hash === section.hash && age >= 0 && age <= options.ttlMs;\n });\n if (held) {\n unchanged.push(section.key);\n } else {\n sent[section.key] = section.hash;\n }\n }\n\n const deliveryId = generateEntityId(\"dlv\", now.getTime());\n const exchange = { deliveryId, unchanged, ack };\n const delivered = options.prepare?.(exchange);\n pending[deliveryId] = {\n createdAt: now.toISOString(),\n records: delivered\n ? Object.fromEntries(delivered.map(({ key, hash }) => [key, hash]))\n : sent,\n };\n ledger.pending = Object.fromEntries(\n Object.entries(pending)\n .filter(([, item]) => now.getTime() - Date.parse(item.createdAt) <= PENDING_MAX_AGE_MS)\n .sort((left, right) => right[1].createdAt.localeCompare(left[1].createdAt))\n .slice(0, MAX_PENDING),\n );\n ledger.acknowledged = Object.fromEntries(\n Object.entries(acknowledged)\n .map(([key, versions]): [string, AcknowledgedVersion[]] => [\n key,\n versions.filter((version) => now.getTime() - Date.parse(version.sentAt) <= MAX_AGE_MS),\n ])\n .filter(([, versions]) => versions.length > 0)\n .slice(0, MAX_CAPSULES),\n );\n await this.write(path, ledger, now);\n return { deliveryId, unchanged, ack };\n } finally {\n await release();\n }\n }\n\n /** Forget everything sent in a session, e.g. after the client compacted its context. */\n async reset(sessionId: string): Promise<void> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n await rm(this.path(sessionId), { force: true });\n } finally {\n await release();\n }\n }\n\n /** Remove expired disposable state; locked sessions are left for the next pass. */\n async pruneExpired(now = new Date()): Promise<number> {\n let removed = 0;\n for (const entry of await readdir(this.directory).catch(() => [])) {\n if (!entry.endsWith(\".json\")) continue;\n const path = join(this.directory, entry);\n try {\n await stat(`${path}.lock`);\n continue;\n } catch {\n // No active lock.\n }\n try {\n const metadata = await stat(path);\n if (now.getTime() - metadata.mtimeMs > MAX_AGE_MS) {\n await rm(path, { force: true });\n removed++;\n }\n } catch {\n // Another cleanup may have removed it already.\n }\n }\n return removed;\n }\n\n private async write(path: string, ledger: LedgerFile, now: Date): Promise<void> {\n ledger.updatedAt = now.toISOString();\n ledger.records = Object.fromEntries(\n Object.entries(ledger.records)\n .sort((left, right) => right[1].lastSentAt.localeCompare(left[1].lastSentAt))\n .slice(0, MAX_CAPSULES),\n );\n await writeAtomic(path, `${JSON.stringify(ledger, null, 2)}\\n`);\n }\n\n private async read(path: string, now: Date): Promise<LedgerFile> {\n try {\n const parsed = JSON.parse(await readFile(path, \"utf8\")) as LedgerFile;\n if (parsed.version !== VERSION || typeof parsed.records !== \"object\") return empty(now);\n const records = Object.fromEntries(\n Object.entries(parsed.records).filter(([, record]) => {\n const age = now.getTime() - Date.parse(record.lastSentAt);\n return Number.isFinite(age) && age <= MAX_AGE_MS;\n }),\n );\n const pending =\n parsed.pending && typeof parsed.pending === \"object\" ? parsed.pending : undefined;\n const acknowledged =\n parsed.acknowledged && typeof parsed.acknowledged === \"object\"\n ? Object.fromEntries(\n Object.entries(parsed.acknowledged).filter(([, versions]) => Array.isArray(versions)),\n )\n : undefined;\n return {\n version: VERSION,\n updatedAt: parsed.updatedAt,\n records,\n ...(pending ? { pending } : {}),\n ...(acknowledged ? { acknowledged } : {}),\n };\n } catch {\n return empty(now);\n }\n }\n\n private path(sessionId: string): string {\n // The full identity participates in the key. Legacy sanitized filenames\n // are intentionally ignored, so a fresh session cannot inherit collisions.\n return join(this.directory, `${digest(sessionId)}.json`);\n }\n\n private async acquire(sessionId: string): Promise<() => Promise<void>> {\n const lock = `${this.path(sessionId)}.lock`;\n for (let attempt = 0; ; attempt++) {\n try {\n await mkdir(lock);\n return async () => rm(lock, { recursive: true, force: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt >= 200) throw error;\n try {\n const age = Date.now() - (await stat(lock)).mtimeMs;\n if (age > STALE_LOCK_MS) {\n await rm(lock, { recursive: true, force: true });\n continue;\n }\n } catch {\n // The owner may have released the lock between checks.\n }\n await new Promise((resolveWait) => setTimeout(resolveWait, 10));\n }\n }\n }\n}\n\nfunction empty(now: Date): LedgerFile {\n return { version: VERSION, updatedAt: now.toISOString(), records: {} };\n}\n\nfunction digest(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n","import { access, readFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, join } from \"node:path\";\n\n/**\n * Which test covers a file and how this repo runs its tests, from naming\n * conventions and project markers only. Agents otherwise spend turns finding\n * both; a wrong guess costs one failed command, so first match wins.\n */\nexport function findCoveringTest(path: string, paths: ReadonlySet<string>): string | null {\n const ext = extname(path);\n const stem = basename(path, ext);\n const dir = dirname(path);\n const join2 = (...parts: string[]) => parts.filter((part) => part && part !== \".\").join(\"/\");\n // `src/a/x.ts` mirrors to `test/a/x.test.ts`: drop a leading source root.\n const mirrored = dir.replace(/^(src|lib|app)(\\/|$)/, \"\");\n const candidates =\n ext === \".py\"\n ? [\n join2(dir, `test_${stem}.py`),\n join2(\"tests\", `test_${stem}.py`),\n join2(dir, \"tests\", `test_${stem}.py`),\n join2(dir, `${stem}_test.py`),\n ]\n : ext === \".go\"\n ? [join2(dir, `${stem}_test.go`)]\n : [\n join2(dir, `${stem}.test${ext}`),\n join2(dir, `${stem}.spec${ext}`),\n join2(dir, \"__tests__\", `${stem}.test${ext}`),\n join2(dir, \"__tests__\", `${stem}${ext}`),\n join2(\"test\", mirrored, `${stem}.test${ext}`),\n join2(\"tests\", mirrored, `${stem}.test${ext}`),\n ];\n return candidates.find((candidate) => paths.has(candidate)) ?? null;\n}\n\nconst NPM_PLACEHOLDER = /no test specified/;\n\nexport async function detectTestCommand(repositoryRoot: string): Promise<string | null> {\n const exists = (file: string) =>\n access(join(repositoryRoot, file)).then(\n () => true,\n () => false,\n );\n try {\n const pkg = JSON.parse(await readFile(join(repositoryRoot, \"package.json\"), \"utf8\")) as {\n scripts?: Record<string, unknown>;\n };\n const script = pkg.scripts?.test;\n if (typeof script === \"string\" && script.trim() && !NPM_PLACEHOLDER.test(script)) {\n if (await exists(\"pnpm-lock.yaml\")) return \"pnpm test\";\n if (await exists(\"yarn.lock\")) return \"yarn test\";\n return \"npm test\";\n }\n } catch {\n // No or unreadable package.json: try other ecosystems.\n }\n for (const marker of [\"pyproject.toml\", \"pytest.ini\", \"setup.cfg\"]) {\n if (await exists(marker)) return \"pytest\";\n }\n if (await exists(\"go.mod\")) return \"go test ./...\";\n if (await exists(\"Cargo.toml\")) return \"cargo test\";\n return null;\n}\n"],"mappings":"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","names":["readFile","join","join2"]}