@orquentra/codebuddy 2.15.0 → 2.15.3

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 (82) hide show
  1. package/ARCHITECTURE.md +12 -7
  2. package/README.md +2 -2
  3. package/dist/{chunk-YMXFSHXT.js → chunk-3L5IR6OW.js} +2 -2
  4. package/dist/{chunk-YMXFSHXT.js.map → chunk-3L5IR6OW.js.map} +1 -1
  5. package/dist/{chunk-JJQDVGF7.js → chunk-3TZNQJTH.js} +3 -3
  6. package/dist/{chunk-4BSO3HST.js → chunk-4LMBZWIF.js} +48 -13
  7. package/dist/chunk-4LMBZWIF.js.map +1 -0
  8. package/dist/{chunk-WHKXUXHE.js → chunk-4TKKLQLH.js} +5 -5
  9. package/dist/chunk-4TKKLQLH.js.map +1 -0
  10. package/dist/{chunk-KQCP3ZYH.js → chunk-5ZLNHYJH.js} +31 -15
  11. package/dist/chunk-5ZLNHYJH.js.map +1 -0
  12. package/dist/{chunk-U7KDM62P.js → chunk-6KSSJJZO.js} +17 -9
  13. package/dist/chunk-6KSSJJZO.js.map +1 -0
  14. package/dist/{chunk-6YI2D5F5.js → chunk-7IXPARDM.js} +3 -3
  15. package/dist/{chunk-UTPC66MV.js → chunk-FEFOZ2ZW.js} +23 -4
  16. package/dist/chunk-FEFOZ2ZW.js.map +1 -0
  17. package/dist/{chunk-ITXE27SZ.js → chunk-FGJ2KPPN.js} +104 -22
  18. package/dist/chunk-FGJ2KPPN.js.map +1 -0
  19. package/dist/{chunk-D5E7SGI5.js → chunk-KGDRGA2L.js} +14 -14
  20. package/dist/{chunk-D5E7SGI5.js.map → chunk-KGDRGA2L.js.map} +1 -1
  21. package/dist/{chunk-UR3DF74I.js → chunk-LZTYPJ5I.js} +36 -18
  22. package/dist/chunk-LZTYPJ5I.js.map +1 -0
  23. package/dist/{chunk-UATJCD2T.js → chunk-R5E2YGLE.js} +14 -8
  24. package/dist/chunk-R5E2YGLE.js.map +1 -0
  25. package/dist/chunk-R5JRKMEP.js +55 -0
  26. package/dist/chunk-R5JRKMEP.js.map +1 -0
  27. package/dist/{chunk-RZ266NHE.js → chunk-SPQU2422.js} +5 -5
  28. package/dist/{chunk-RZ266NHE.js.map → chunk-SPQU2422.js.map} +1 -1
  29. package/dist/{chunk-BTTQWSDS.js → chunk-SXVH74NE.js} +36 -6
  30. package/dist/chunk-SXVH74NE.js.map +1 -0
  31. package/dist/{chunk-KYRM3HPP.js → chunk-TIMS3PB7.js} +3 -3
  32. package/dist/{chunk-M75SN4D4.js → chunk-Y4QQML24.js} +3 -3
  33. package/dist/{chunk-M75SN4D4.js.map → chunk-Y4QQML24.js.map} +1 -1
  34. package/dist/cli/index.js +4 -4
  35. package/dist/{config-TXQJCCVI.js → config-32X53LWY.js} +4 -4
  36. package/dist/{expand-KWYGNCKZ.js → expand-VFYOWNZS.js} +5 -5
  37. package/dist/{file-vectors-MKXTSGGH.js → file-vectors-UBOKGYXX.js} +4 -3
  38. package/dist/{index-facts-4MTEHKMN.js → index-facts-ZTUANWBF.js} +7 -7
  39. package/dist/index-pass-S6NC25P5.js +15 -0
  40. package/dist/index.js +9 -8
  41. package/dist/{install-E3JWINTN.js → install-ZTE3H7QM.js} +8 -7
  42. package/dist/{lean-MLTAFS5L.js → lean-5N6BOONS.js} +5 -4
  43. package/dist/{local-75DJECVR.js → local-5EYLEBEP.js} +4 -2
  44. package/dist/{local-llm-MECVJTVU.js → local-llm-ESMVP5LE.js} +4 -4
  45. package/dist/{main-RLMKQZW4.js → main-B3HZCGVE.js} +61 -50
  46. package/dist/main-B3HZCGVE.js.map +1 -0
  47. package/dist/{memory-PFFVMTCH.js → memory-CI7HIGHN.js} +5 -5
  48. package/dist/{operations-STR7S5UO.js → operations-MW52HIQ3.js} +4 -4
  49. package/dist/{rerank-XXKWDI5Q.js → rerank-O4S5BC45.js} +5 -5
  50. package/dist/{runtime-SWFWGTQ4.js → runtime-SZAWIXEB.js} +6 -5
  51. package/dist/{summaries-QXWFFDUZ.js → summaries-GCHZGIIA.js} +11 -8
  52. package/dist/{zvec-F73IBWO5.js → zvec-V34P24WF.js} +11 -6
  53. package/package.json +1 -1
  54. package/dist/chunk-4BSO3HST.js.map +0 -1
  55. package/dist/chunk-BTTQWSDS.js.map +0 -1
  56. package/dist/chunk-ITXE27SZ.js.map +0 -1
  57. package/dist/chunk-KQCP3ZYH.js.map +0 -1
  58. package/dist/chunk-U7KDM62P.js.map +0 -1
  59. package/dist/chunk-UATJCD2T.js.map +0 -1
  60. package/dist/chunk-UR3DF74I.js.map +0 -1
  61. package/dist/chunk-UTPC66MV.js.map +0 -1
  62. package/dist/chunk-WHKXUXHE.js.map +0 -1
  63. package/dist/index-pass-TBDUEZCB.js +0 -14
  64. package/dist/main-RLMKQZW4.js.map +0 -1
  65. /package/dist/{chunk-JJQDVGF7.js.map → chunk-3TZNQJTH.js.map} +0 -0
  66. /package/dist/{chunk-6YI2D5F5.js.map → chunk-7IXPARDM.js.map} +0 -0
  67. /package/dist/{chunk-KYRM3HPP.js.map → chunk-TIMS3PB7.js.map} +0 -0
  68. /package/dist/{config-TXQJCCVI.js.map → config-32X53LWY.js.map} +0 -0
  69. /package/dist/{expand-KWYGNCKZ.js.map → expand-VFYOWNZS.js.map} +0 -0
  70. /package/dist/{file-vectors-MKXTSGGH.js.map → file-vectors-UBOKGYXX.js.map} +0 -0
  71. /package/dist/{index-facts-4MTEHKMN.js.map → index-facts-ZTUANWBF.js.map} +0 -0
  72. /package/dist/{index-pass-TBDUEZCB.js.map → index-pass-S6NC25P5.js.map} +0 -0
  73. /package/dist/{install-E3JWINTN.js.map → install-ZTE3H7QM.js.map} +0 -0
  74. /package/dist/{lean-MLTAFS5L.js.map → lean-5N6BOONS.js.map} +0 -0
  75. /package/dist/{local-75DJECVR.js.map → local-5EYLEBEP.js.map} +0 -0
  76. /package/dist/{local-llm-MECVJTVU.js.map → local-llm-ESMVP5LE.js.map} +0 -0
  77. /package/dist/{memory-PFFVMTCH.js.map → memory-CI7HIGHN.js.map} +0 -0
  78. /package/dist/{operations-STR7S5UO.js.map → operations-MW52HIQ3.js.map} +0 -0
  79. /package/dist/{rerank-XXKWDI5Q.js.map → rerank-O4S5BC45.js.map} +0 -0
  80. /package/dist/{runtime-SWFWGTQ4.js.map → runtime-SZAWIXEB.js.map} +0 -0
  81. /package/dist/{summaries-QXWFFDUZ.js.map → summaries-GCHZGIIA.js.map} +0 -0
  82. /package/dist/{zvec-F73IBWO5.js.map → zvec-V34P24WF.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/llm/index-facts.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\nimport { resolveNamespaceFrom } from \"../core/config-file.js\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { MemoryFileStore } from \"../core/memory-file-store.js\";\nimport { termOverlap } from \"../core/memory-search.js\";\nimport { redactSensitive, scanSensitive } from \"../core/sensitive.js\";\nimport { chatJson } from \"./client.js\";\nimport { type EnabledLlmConfig, resolveLlmConfig } from \"./config.js\";\nimport { waitForForeground } from \"./local.js\";\n\n/**\n * Project facts drawn from the repository at index time, so a fresh project\n * does not start with empty memory: supported versions and the test/lint\n * commands from manifests (read deterministically), required CI checks from\n * workflows, and rules from README / CONTRIBUTING / AGENTS.md / CLAUDE.md and\n * deprecation or release policies (read by the small model, local or BYOK).\n *\n * Every fact keeps its source: `paths: [file]`, `verification: [\"file:Lline\"]`,\n * and a line the fact's words must actually appear near. They are ordinary\n * Markdown facts (the durable store `remember` writes too), marked\n * `createdByAgent: \"codebuddy-index\"`; re-extraction only ever replaces facts\n * with that origin whose source file changed, never a human or agent fact.\n * Sensitive text is dropped, not redacted. At most `MAX_FACTS` per repository.\n */\nexport const INDEX_FACT_AGENT = \"codebuddy-index\";\nexport const MAX_FACTS = 40;\nconst PER_SOURCE = 8;\nconst SECTION_CHARS = 1_600;\n/** A fact this close to an existing one (Jaccard of content words) is a duplicate. */\nconst DUPLICATE_OVERLAP = 0.8;\nconst TIMEOUT_MS = { local: 60_000, hosted: 20_000 } as const;\n\nexport type IndexFactsResult = {\n written: number;\n removed: number;\n skippedSources: number;\n stoppedBy: \"done\" | \"disabled\" | \"cap\" | \"aborted\";\n};\n\ntype Candidate = { path: string; line: number; text: string };\ntype Ledger = { version: 1; sources: Record<string, string> };\n\nconst PROSE_SOURCES = /^(README|CONTRIBUTING|AGENTS|CLAUDE)(\\.[a-z]+)?$/i;\nconst POLICY_DOC =\n /(deprecat|release[-_ ]?(policy|process)|versioning|support[-_ ]?policy|compatib)/i;\n\n/** Files facts are read from, relative to the root, in priority order. */\nexport async function factSources(root: string): Promise<string[]> {\n const top = await readdir(root).catch(() => [] as string[]);\n const docs = await readdir(join(root, \"docs\"), { recursive: true }).catch(() => [] as string[]);\n const workflows = await readdir(join(root, \".github\", \"workflows\")).catch(() => [] as string[]);\n return [\n ...[\"package.json\", \"pyproject.toml\"].filter((name) => top.includes(name)),\n ...top.filter((name) => PROSE_SOURCES.test(name)).sort(),\n ...workflows\n .filter((name) => /\\.ya?ml$/.test(name))\n .sort()\n .map((name) => `.github/workflows/${name}`),\n ...docs\n .map((name) => name.split(\"\\\\\").join(\"/\"))\n .filter((name) => /\\.(md|rst|txt)$/i.test(name) && POLICY_DOC.test(name))\n .sort()\n .slice(0, 4)\n .map((name) => `docs/${name}`),\n ];\n}\n\n/**\n * Extract (or refresh) index facts. Sources whose content hash is unchanged\n * are skipped; a changed or deleted source has its old index facts removed\n * first. Without an enabled model layer nothing happens (2.14 behaviour).\n */\nexport async function extractIndexFacts(\n root: string,\n options: { config?: EnabledLlmConfig; signal?: AbortSignal; namespace?: string } = {},\n): Promise<IndexFactsResult> {\n const resolved = options.config ?? (await resolveLlmConfig(root));\n if (!resolved.enabled || resolved.features.facts === false) {\n return { written: 0, removed: 0, skippedSources: 0, stoppedBy: \"disabled\" };\n }\n const config = resolved;\n const namespace = options.namespace ?? (await resolveNamespaceFrom(root));\n const store = new MemoryFileStore(root);\n const ledger = await readLedger(root);\n const sources = await factSources(root);\n let facts = await store.listFacts();\n let removed = 0;\n const removeFrom = async (path: string) => {\n for (const fact of facts) {\n if (fact.createdByAgent === INDEX_FACT_AGENT && fact.paths[0] === path) {\n await store.deleteFact(fact.id);\n removed++;\n }\n }\n facts = await store.listFacts();\n };\n for (const path of Object.keys(ledger.sources)) {\n if (!sources.includes(path)) {\n await removeFrom(path);\n delete ledger.sources[path];\n }\n }\n\n let written = 0;\n let skippedSources = 0;\n let stoppedBy: IndexFactsResult[\"stoppedBy\"] = \"done\";\n const indexFacts = () => facts.filter((fact) => fact.createdByAgent === INDEX_FACT_AGENT).length;\n for (const path of sources) {\n if (options.signal?.aborted) {\n stoppedBy = \"aborted\";\n break;\n }\n const text = await readFile(join(root, path), \"utf8\").catch(() => null);\n if (text === null) continue;\n const hash = sha(text);\n if (ledger.sources[path] === hash) {\n skippedSources++;\n continue;\n }\n await removeFrom(path);\n if (indexFacts() >= MAX_FACTS) {\n stoppedBy = \"cap\";\n break;\n }\n const candidates = await candidatesFor(root, path, text, config, options.signal);\n let fromSource = 0;\n for (const candidate of candidates) {\n if (fromSource >= PER_SOURCE || indexFacts() >= MAX_FACTS) break;\n if (scanSensitive(candidate.text).sensitive) continue;\n // Never a second copy of what a human, agent or earlier source already said.\n if (\n facts.some(\n (fact) =>\n Math.max(\n termOverlap(candidate.text, fact.object),\n termOverlap(candidate.text, fact.content),\n ) >= DUPLICATE_OVERLAP,\n )\n ) {\n continue;\n }\n const id = generateEntityId(\"fact\");\n const content = `${candidate.text} (source: ${path}:L${candidate.line})`;\n await store.writeFact({\n id,\n namespace,\n subject: path,\n predicate: \"project-fact\",\n object: candidate.text,\n confidence: 0.8,\n createdAt: new Date().toISOString(),\n createdByAgent: INDEX_FACT_AGENT,\n sourceInteractionId: null,\n sourceDeleted: false,\n content,\n paths: [path],\n verification: [`${path}:L${candidate.line}`],\n idempotencyKey: `index:${path}:${sha(candidate.text).slice(0, 16)}`.slice(0, 200),\n fingerprint: sha(`${namespace}\\n${content}`),\n });\n facts = await store.listFacts();\n written++;\n fromSource++;\n }\n ledger.sources[path] = hash;\n await writeLedger(root, ledger);\n }\n await writeLedger(root, ledger);\n return { written, removed, skippedSources, stoppedBy };\n}\n\n/** Index facts currently stored (for `memory list|prune --origin index`). */\nexport async function listIndexFacts(root: string) {\n return (await new MemoryFileStore(root).listFacts()).filter(\n (fact) => fact.createdByAgent === INDEX_FACT_AGENT,\n );\n}\n\n/** Delete every index fact and forget the source hashes, so the next pass re-extracts. */\nexport async function pruneIndexFacts(root: string): Promise<number> {\n const store = new MemoryFileStore(root);\n const facts = await listIndexFacts(root);\n for (const fact of facts) await store.deleteFact(fact.id);\n await writeLedger(root, { version: 1, sources: {} });\n return facts.length;\n}\n\nasync function candidatesFor(\n root: string,\n path: string,\n text: string,\n config: EnabledLlmConfig,\n signal: AbortSignal | undefined,\n): Promise<Candidate[]> {\n if (path === \"package.json\") return packageFacts(path, text);\n if (path === \"pyproject.toml\") return pyprojectFacts(path, text);\n if (path.startsWith(\".github/workflows/\")) return workflowFacts(path, text);\n return proseFacts(root, path, stripManagedBlocks(text), config, signal);\n}\n\n/** Supported Node version and the project's own check commands. */\nexport function packageFacts(path: string, text: string): Candidate[] {\n let manifest: { engines?: Record<string, string>; scripts?: Record<string, string> };\n try {\n manifest = JSON.parse(text);\n } catch {\n return [];\n }\n const out: Candidate[] = [];\n const node = manifest.engines?.node;\n if (node)\n out.push({ path, line: lineOf(text, '\"node\"'), text: `The project requires Node.js ${node}.` });\n for (const name of [\"test\", \"lint\", \"typecheck\", \"build\", \"format\"]) {\n const script = manifest.scripts?.[name];\n if (script) {\n out.push({\n path,\n line: lineOf(text, `\"${name}\"`),\n text: `Run \\`npm run ${name}\\` to ${VERB[name]} (it runs \\`${script.slice(0, 120)}\\`).`,\n });\n }\n }\n return out;\n}\nconst VERB: Record<string, string> = {\n test: \"run the tests\",\n lint: \"lint the code\",\n typecheck: \"type-check the code\",\n build: \"build the project\",\n format: \"format the code\",\n};\n\n/** Supported Python versions and the configured tools. */\nexport function pyprojectFacts(path: string, text: string): Candidate[] {\n const out: Candidate[] = [];\n const python = text.match(/^\\s*requires-python\\s*=\\s*[\"']([^\"']+)[\"']/m)?.[1];\n if (python) {\n out.push({\n path,\n line: lineOf(text, \"requires-python\"),\n text: `The project supports Python ${python}.`,\n });\n }\n for (const [tool, what] of [\n [\"ruff\", \"Ruff is the linter/formatter\"],\n [\"mypy\", \"mypy type-checks the code\"],\n [\"pyright\", \"Pyright type-checks the code\"],\n [\"pytest\", \"Tests run with pytest\"],\n ] as const) {\n const header = `[tool.${tool}`;\n if (text.includes(header)) {\n out.push({\n path,\n line: lineOf(text, header),\n text: `${what} (configured in pyproject.toml).`,\n });\n }\n }\n return out;\n}\n\nconst CHECK_COMMAND =\n /\\b(test|tests|pytest|lint|ruff|mypy|pyright|tsc|typecheck|vitest|jest|biome|eslint|go vet|cargo (test|clippy)|coverage)\\b/i;\n\n/** Steps that only report or branch (`echo \"test workflow exited with …\"`) are not checks. */\nconst SHELL_PLUMBING = /^(echo|printf|if|then|else|fi|exit|export|set|\\[|test\\s+-)\\b/i;\n\n/** CI's required checks: the run steps that test, lint or type-check. */\nexport function workflowFacts(path: string, text: string): Candidate[] {\n let workflow: {\n name?: string;\n on?: unknown;\n jobs?: Record<string, { steps?: Array<{ run?: string }> }>;\n };\n try {\n workflow = parseYaml(text) ?? {};\n } catch {\n return [];\n }\n const commands = new Set<string>();\n for (const job of Object.values(workflow.jobs ?? {})) {\n for (const step of job?.steps ?? []) {\n const run = typeof step?.run === \"string\" ? step.run.split(\"\\n\")[0]?.trim() : undefined;\n if (run && CHECK_COMMAND.test(run) && !SHELL_PLUMBING.test(run) && run.length <= 120)\n commands.add(run);\n }\n }\n // Only a workflow that runs on pull requests gates merging (a release job does not).\n // YAML 1.1 readers may parse the `on:` key as boolean true.\n const triggers = JSON.stringify(workflow.on ?? (workflow as Record<string, unknown>).true ?? \"\");\n const gates = triggers.includes(\"pull_request\");\n return [...commands].slice(0, 4).map((command) => ({\n path,\n line: lineOf(text, command),\n text: `CI (${workflow.name ?? path}) runs \\`${command}\\`${gates ? \" on pull requests; it must pass before merging\" : \"\"}.`,\n }));\n}\n\nconst proseSchema = z.object({\n facts: z.array(z.object({ line: z.number().int(), fact: z.string() })).default([]),\n});\n\n/** Grammar bounds for the local model (see summaries.ts CARD_GRAMMAR). */\nconst FACT_GRAMMAR = {\n type: \"object\",\n properties: {\n facts: {\n type: \"array\",\n maxItems: 6,\n items: {\n type: \"object\",\n properties: { line: { type: \"integer\" }, fact: { type: \"string\", maxLength: 240 } },\n },\n },\n },\n};\n\nconst SYSTEM = [\n \"You extract durable project facts from a repository document for a team memory.\",\n 'Answer with JSON only: {\"facts\":[{\"line\":12,\"fact\":\"...\"}]}.',\n \"Keep only lasting rules and facts a developer must know: conventions, required checks, supported versions, deprecation and release policy, how to run or test the project.\",\n \"Each fact is one short sentence in the document's own words; line is the number of the line it comes from.\",\n \"Skip marketing, installation steps for end users, links, badges and anything temporary. Give at most 6 facts; none is fine.\",\n].join(\" \");\n\nasync function proseFacts(\n root: string,\n path: string,\n text: string,\n config: EnabledLlmConfig,\n signal: AbortSignal | undefined,\n): Promise<Candidate[]> {\n const lines = text.split(/\\r?\\n/);\n const out: Candidate[] = [];\n for (const section of sections(lines)) {\n if (signal?.aborted || out.length >= PER_SOURCE) break;\n if (config.local) await waitForForeground(root, signal);\n const numbered = section\n .map(({ line, text }) => `${line}: ${redactSensitive(text)}`)\n .join(\"\\n\");\n const result = await chatJson(config, {\n system: SYSTEM,\n user: `File: ${path}\\n\\n${numbered}`,\n schema: proseSchema,\n jsonSchema: FACT_GRAMMAR,\n maxTokens: 500,\n deadlineMs: config.local ? TIMEOUT_MS.local : TIMEOUT_MS.hosted,\n purpose: \"background\",\n repositoryRoot: root,\n });\n if (!result.ok) continue;\n for (const fact of result.value.facts) {\n const sentence = fact.fact.trim().replace(/\\s+/g, \" \").slice(0, 240);\n if (isStatement(sentence) && hasLineEvidence(lines, fact.line, sentence)) {\n out.push({ path, line: fact.line, text: sentence });\n }\n }\n }\n return out;\n}\n\n/**\n * A fact is a statement: at least five words of prose, not a shell line, a\n * code fence, a code line, an installer option (\"Using pyenv\") or a bare\n * heading the small model copied out of a README.\n */\nexport function isStatement(text: string): boolean {\n if (/```|^\\$\\s|^(cd|npm|npx|pip|uv|git|make|curl|echo)\\s/i.test(text)) return false;\n // Code: an import line, an assignment or call (`app = Litestar(...)`), or\n // dense code punctuation.\n if (/^(from\\s+[\\w.]+\\s+import|import\\s+[\\w.]+)\\b/.test(text)) return false;\n if (/\\b\\w+\\s*=\\s*[\\w.]+\\(/.test(text) || (text.match(/[(){}[\\]=;<>]/g) ?? []).length >= 4)\n return false;\n // One option in a list of ways to install (\"Using asdf\", \"With Homebrew: …\").\n if (/^(using|via|with)\\s+[\\w.-]+\\s*(:|$|\\(|,)/i.test(text)) return false;\n return (text.match(/\\p{L}{2,}/gu) ?? []).length >= 5;\n}\n\n/**\n * The model's fact must be supported by its cited line (± 2): at least two of\n * its content words (≥ 4 letters) appear there, or all of them for a short fact.\n */\nexport function hasLineEvidence(lines: string[], line: number, fact: string): boolean {\n if (line < 1 || line > lines.length) return false;\n const nearby = new Set(\n lines\n .slice(Math.max(0, line - 3), line + 2)\n .join(\" \")\n .toLowerCase()\n .match(/[\\p{L}\\p{N}_]{4,}/gu) ?? [],\n );\n const words = [...new Set(fact.toLowerCase().match(/[\\p{L}\\p{N}_]{4,}/gu) ?? [])];\n const shared = words.filter((word) => nearby.has(word)).length;\n return shared >= Math.min(2, words.length) && shared > 0;\n}\n\n/** Heading-delimited chunks of at most `SECTION_CHARS`, with 1-based line numbers. */\nfunction sections(lines: string[]): Array<Array<{ line: number; text: string }>> {\n const out: Array<Array<{ line: number; text: string }>> = [];\n let current: Array<{ line: number; text: string }> = [];\n let size = 0;\n lines.forEach((text, index) => {\n const heading = /^#{1,3}\\s/.test(text);\n if (current.length > 0 && (heading || size + text.length > SECTION_CHARS)) {\n out.push(current);\n current = [];\n size = 0;\n }\n if (text.trim()) {\n current.push({ line: index + 1, text: text.slice(0, 400) });\n size += Math.min(text.length, 400) + 6;\n }\n });\n if (current.length > 0) out.push(current);\n // A long README: the first sections carry the rules that matter.\n return out.slice(0, 12);\n}\n\n/** CodeBuddy's own managed blocks in CLAUDE.md / AGENTS.md are not project facts. */\nfunction stripManagedBlocks(text: string): string {\n // Blank the block (keeping its line breaks) so cited line numbers stay true.\n return text.replace(\n /<!-- codebuddy:[\\w-]+:start -->[\\s\\S]*?<!-- codebuddy:[\\w-]+:end -->/g,\n (block) => block.replace(/[^\\n]/g, \"\"),\n );\n}\n\nfunction lineOf(text: string, needle: string): number {\n const index = text.indexOf(needle);\n return index < 0 ? 1 : text.slice(0, index).split(\"\\n\").length;\n}\n\nfunction sha(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n\nfunction ledgerPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"index-facts.json\");\n}\n\nasync function readLedger(root: string): Promise<Ledger> {\n try {\n const parsed = JSON.parse(await readFile(ledgerPath(root), \"utf8\")) as Ledger;\n if (parsed.version === 1 && parsed.sources) return parsed;\n } catch {\n // Missing: every source is new.\n }\n return { version: 1, sources: {} };\n}\n\nasync function writeLedger(root: string, ledger: Ledger): Promise<void> {\n await mkdir(dirname(ledgerPath(root)), { recursive: true });\n await writeAtomic(ledgerPath(root), JSON.stringify(ledger));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,SAAS,gBAAgB;AACzC,SAAS,SAAS,YAAY;AAC9B,SAAS,SAAS,iBAAiB;AACnC,SAAS,SAAS;AAyBX,IAAM,mBAAmB;AACzB,IAAM,YAAY;AACzB,IAAM,aAAa;AACnB,IAAM,gBAAgB;AAEtB,IAAM,oBAAoB;AAC1B,IAAM,aAAa,EAAE,OAAO,KAAQ,QAAQ,IAAO;AAYnD,IAAM,gBAAgB;AACtB,IAAM,aACJ;AAGF,eAAsB,YAAY,MAAiC;AACjE,QAAM,MAAM,MAAM,QAAQ,IAAI,EAAE,MAAM,MAAM,CAAC,CAAa;AAC1D,QAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC,CAAa;AAC9F,QAAM,YAAY,MAAM,QAAQ,KAAK,MAAM,WAAW,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC,CAAa;AAC9F,SAAO;AAAA,IACL,GAAG,CAAC,gBAAgB,gBAAgB,EAAE,OAAO,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC;AAAA,IACzE,GAAG,IAAI,OAAO,CAAC,SAAS,cAAc,KAAK,IAAI,CAAC,EAAE,KAAK;AAAA,IACvD,GAAG,UACA,OAAO,CAAC,SAAS,WAAW,KAAK,IAAI,CAAC,EACtC,KAAK,EACL,IAAI,CAAC,SAAS,qBAAqB,IAAI,EAAE;AAAA,IAC5C,GAAG,KACA,IAAI,CAAC,SAAS,KAAK,MAAM,IAAI,EAAE,KAAK,GAAG,CAAC,EACxC,OAAO,CAAC,SAAS,mBAAmB,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,CAAC,EACvE,KAAK,EACL,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,SAAS,QAAQ,IAAI,EAAE;AAAA,EACjC;AACF;AAOA,eAAsB,kBACpB,MACA,UAAmF,CAAC,GACzD;AAC3B,QAAM,WAAW,QAAQ,UAAW,MAAM,iBAAiB,IAAI;AAC/D,MAAI,CAAC,SAAS,WAAW,SAAS,SAAS,UAAU,OAAO;AAC1D,WAAO,EAAE,SAAS,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,WAAW;AAAA,EAC5E;AACA,QAAM,SAAS;AACf,QAAM,YAAY,QAAQ,aAAc,MAAM,qBAAqB,IAAI;AACvE,QAAM,QAAQ,IAAI,gBAAgB,IAAI;AACtC,QAAM,SAAS,MAAM,WAAW,IAAI;AACpC,QAAM,UAAU,MAAM,YAAY,IAAI;AACtC,MAAI,QAAQ,MAAM,MAAM,UAAU;AAClC,MAAI,UAAU;AACd,QAAM,aAAa,OAAO,SAAiB;AACzC,eAAW,QAAQ,OAAO;AACxB,UAAI,KAAK,mBAAmB,oBAAoB,KAAK,MAAM,CAAC,MAAM,MAAM;AACtE,cAAM,MAAM,WAAW,KAAK,EAAE;AAC9B;AAAA,MACF;AAAA,IACF;AACA,YAAQ,MAAM,MAAM,UAAU;AAAA,EAChC;AACA,aAAW,QAAQ,OAAO,KAAK,OAAO,OAAO,GAAG;AAC9C,QAAI,CAAC,QAAQ,SAAS,IAAI,GAAG;AAC3B,YAAM,WAAW,IAAI;AACrB,aAAO,OAAO,QAAQ,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,MAAI,UAAU;AACd,MAAI,iBAAiB;AACrB,MAAI,YAA2C;AAC/C,QAAM,aAAa,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,mBAAmB,gBAAgB,EAAE;AAC1F,aAAW,QAAQ,SAAS;AAC1B,QAAI,QAAQ,QAAQ,SAAS;AAC3B,kBAAY;AACZ;AAAA,IACF;AACA,UAAM,OAAO,MAAM,SAAS,KAAK,MAAM,IAAI,GAAG,MAAM,EAAE,MAAM,MAAM,IAAI;AACtE,QAAI,SAAS,KAAM;AACnB,UAAM,OAAO,IAAI,IAAI;AACrB,QAAI,OAAO,QAAQ,IAAI,MAAM,MAAM;AACjC;AACA;AAAA,IACF;AACA,UAAM,WAAW,IAAI;AACrB,QAAI,WAAW,KAAK,WAAW;AAC7B,kBAAY;AACZ;AAAA,IACF;AACA,UAAM,aAAa,MAAM,cAAc,MAAM,MAAM,MAAM,QAAQ,QAAQ,MAAM;AAC/E,QAAI,aAAa;AACjB,eAAW,aAAa,YAAY;AAClC,UAAI,cAAc,cAAc,WAAW,KAAK,UAAW;AAC3D,UAAI,cAAc,UAAU,IAAI,EAAE,UAAW;AAE7C,UACE,MAAM;AAAA,QACJ,CAAC,SACC,KAAK;AAAA,UACH,YAAY,UAAU,MAAM,KAAK,MAAM;AAAA,UACvC,YAAY,UAAU,MAAM,KAAK,OAAO;AAAA,QAC1C,KAAK;AAAA,MACT,GACA;AACA;AAAA,MACF;AACA,YAAM,KAAK,iBAAiB,MAAM;AAClC,YAAM,UAAU,GAAG,UAAU,IAAI,aAAa,IAAI,KAAK,UAAU,IAAI;AACrE,YAAM,MAAM,UAAU;AAAA,QACpB;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT,WAAW;AAAA,QACX,QAAQ,UAAU;AAAA,QAClB,YAAY;AAAA,QACZ,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,QACrB,eAAe;AAAA,QACf;AAAA,QACA,OAAO,CAAC,IAAI;AAAA,QACZ,cAAc,CAAC,GAAG,IAAI,KAAK,UAAU,IAAI,EAAE;AAAA,QAC3C,gBAAgB,SAAS,IAAI,IAAI,IAAI,UAAU,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,GAAG;AAAA,QAChF,aAAa,IAAI,GAAG,SAAS;AAAA,EAAK,OAAO,EAAE;AAAA,MAC7C,CAAC;AACD,cAAQ,MAAM,MAAM,UAAU;AAC9B;AACA;AAAA,IACF;AACA,WAAO,QAAQ,IAAI,IAAI;AACvB,UAAM,YAAY,MAAM,MAAM;AAAA,EAChC;AACA,QAAM,YAAY,MAAM,MAAM;AAC9B,SAAO,EAAE,SAAS,SAAS,gBAAgB,UAAU;AACvD;AAGA,eAAsB,eAAe,MAAc;AACjD,UAAQ,MAAM,IAAI,gBAAgB,IAAI,EAAE,UAAU,GAAG;AAAA,IACnD,CAAC,SAAS,KAAK,mBAAmB;AAAA,EACpC;AACF;AAGA,eAAsB,gBAAgB,MAA+B;AACnE,QAAM,QAAQ,IAAI,gBAAgB,IAAI;AACtC,QAAM,QAAQ,MAAM,eAAe,IAAI;AACvC,aAAW,QAAQ,MAAO,OAAM,MAAM,WAAW,KAAK,EAAE;AACxD,QAAM,YAAY,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC,EAAE,CAAC;AACnD,SAAO,MAAM;AACf;AAEA,eAAe,cACb,MACA,MACA,MACA,QACA,QACsB;AACtB,MAAI,SAAS,eAAgB,QAAO,aAAa,MAAM,IAAI;AAC3D,MAAI,SAAS,iBAAkB,QAAO,eAAe,MAAM,IAAI;AAC/D,MAAI,KAAK,WAAW,oBAAoB,EAAG,QAAO,cAAc,MAAM,IAAI;AAC1E,SAAO,WAAW,MAAM,MAAM,mBAAmB,IAAI,GAAG,QAAQ,MAAM;AACxE;AAGO,SAAS,aAAa,MAAc,MAA2B;AACpE,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,MAAM,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAmB,CAAC;AAC1B,QAAM,OAAO,SAAS,SAAS;AAC/B,MAAI;AACF,QAAI,KAAK,EAAE,MAAM,MAAM,OAAO,MAAM,QAAQ,GAAG,MAAM,gCAAgC,IAAI,IAAI,CAAC;AAChG,aAAW,QAAQ,CAAC,QAAQ,QAAQ,aAAa,SAAS,QAAQ,GAAG;AACnE,UAAM,SAAS,SAAS,UAAU,IAAI;AACtC,QAAI,QAAQ;AACV,UAAI,KAAK;AAAA,QACP;AAAA,QACA,MAAM,OAAO,MAAM,IAAI,IAAI,GAAG;AAAA,QAC9B,MAAM,iBAAiB,IAAI,SAAS,KAAK,IAAI,CAAC,eAAe,OAAO,MAAM,GAAG,GAAG,CAAC;AAAA,MACnF,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AACA,IAAM,OAA+B;AAAA,EACnC,MAAM;AAAA,EACN,MAAM;AAAA,EACN,WAAW;AAAA,EACX,OAAO;AAAA,EACP,QAAQ;AACV;AAGO,SAAS,eAAe,MAAc,MAA2B;AACtE,QAAM,MAAmB,CAAC;AAC1B,QAAM,SAAS,KAAK,MAAM,6CAA6C,IAAI,CAAC;AAC5E,MAAI,QAAQ;AACV,QAAI,KAAK;AAAA,MACP;AAAA,MACA,MAAM,OAAO,MAAM,iBAAiB;AAAA,MACpC,MAAM,+BAA+B,MAAM;AAAA,IAC7C,CAAC;AAAA,EACH;AACA,aAAW,CAAC,MAAM,IAAI,KAAK;AAAA,IACzB,CAAC,QAAQ,8BAA8B;AAAA,IACvC,CAAC,QAAQ,2BAA2B;AAAA,IACpC,CAAC,WAAW,8BAA8B;AAAA,IAC1C,CAAC,UAAU,uBAAuB;AAAA,EACpC,GAAY;AACV,UAAM,SAAS,SAAS,IAAI;AAC5B,QAAI,KAAK,SAAS,MAAM,GAAG;AACzB,UAAI,KAAK;AAAA,QACP;AAAA,QACA,MAAM,OAAO,MAAM,MAAM;AAAA,QACzB,MAAM,GAAG,IAAI;AAAA,MACf,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,gBACJ;AAGF,IAAM,iBAAiB;AAGhB,SAAS,cAAc,MAAc,MAA2B;AACrE,MAAI;AAKJ,MAAI;AACF,eAAW,UAAU,IAAI,KAAK,CAAC;AAAA,EACjC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,OAAO,OAAO,OAAO,SAAS,QAAQ,CAAC,CAAC,GAAG;AACpD,eAAW,QAAQ,KAAK,SAAS,CAAC,GAAG;AACnC,YAAM,MAAM,OAAO,MAAM,QAAQ,WAAW,KAAK,IAAI,MAAM,IAAI,EAAE,CAAC,GAAG,KAAK,IAAI;AAC9E,UAAI,OAAO,cAAc,KAAK,GAAG,KAAK,CAAC,eAAe,KAAK,GAAG,KAAK,IAAI,UAAU;AAC/E,iBAAS,IAAI,GAAG;AAAA,IACpB;AAAA,EACF;AAGA,QAAM,WAAW,KAAK,UAAU,SAAS,MAAO,SAAqC,QAAQ,EAAE;AAC/F,QAAM,QAAQ,SAAS,SAAS,cAAc;AAC9C,SAAO,CAAC,GAAG,QAAQ,EAAE,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,aAAa;AAAA,IACjD;AAAA,IACA,MAAM,OAAO,MAAM,OAAO;AAAA,IAC1B,MAAM,OAAO,SAAS,QAAQ,IAAI,YAAY,OAAO,KAAK,QAAQ,mDAAmD,EAAE;AAAA,EACzH,EAAE;AACJ;AAEA,IAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACnF,CAAC;AAGD,IAAM,eAAe;AAAA,EACnB,MAAM;AAAA,EACN,YAAY;AAAA,IACV,OAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO;AAAA,QACL,MAAM;AAAA,QACN,YAAY,EAAE,MAAM,EAAE,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,UAAU,WAAW,IAAI,EAAE;AAAA,MACpF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAM,SAAS;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AAEV,eAAe,WACb,MACA,MACA,MACA,QACA,QACsB;AACtB,QAAM,QAAQ,KAAK,MAAM,OAAO;AAChC,QAAM,MAAmB,CAAC;AAC1B,aAAW,WAAW,SAAS,KAAK,GAAG;AACrC,QAAI,QAAQ,WAAW,IAAI,UAAU,WAAY;AACjD,QAAI,OAAO,MAAO,OAAM,kBAAkB,MAAM,MAAM;AACtD,UAAM,WAAW,QACd,IAAI,CAAC,EAAE,MAAM,MAAAA,MAAK,MAAM,GAAG,IAAI,KAAK,gBAAgBA,KAAI,CAAC,EAAE,EAC3D,KAAK,IAAI;AACZ,UAAM,SAAS,MAAM,SAAS,QAAQ;AAAA,MACpC,QAAQ;AAAA,MACR,MAAM,SAAS,IAAI;AAAA;AAAA,EAAO,QAAQ;AAAA,MAClC,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,YAAY,OAAO,QAAQ,WAAW,QAAQ,WAAW;AAAA,MACzD,SAAS;AAAA,MACT,gBAAgB;AAAA,IAClB,CAAC;AACD,QAAI,CAAC,OAAO,GAAI;AAChB,eAAW,QAAQ,OAAO,MAAM,OAAO;AACrC,YAAM,WAAW,KAAK,KAAK,KAAK,EAAE,QAAQ,QAAQ,GAAG,EAAE,MAAM,GAAG,GAAG;AACnE,UAAI,YAAY,QAAQ,KAAK,gBAAgB,OAAO,KAAK,MAAM,QAAQ,GAAG;AACxE,YAAI,KAAK,EAAE,MAAM,MAAM,KAAK,MAAM,MAAM,SAAS,CAAC;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,YAAY,MAAuB;AACjD,MAAI,uDAAuD,KAAK,IAAI,EAAG,QAAO;AAG9E,MAAI,8CAA8C,KAAK,IAAI,EAAG,QAAO;AACrE,MAAI,uBAAuB,KAAK,IAAI,MAAM,KAAK,MAAM,gBAAgB,KAAK,CAAC,GAAG,UAAU;AACtF,WAAO;AAET,MAAI,4CAA4C,KAAK,IAAI,EAAG,QAAO;AACnE,UAAQ,KAAK,MAAM,WAAC,cAAU,IAAE,MAAK,CAAC,GAAG,UAAU;AACrD;AAMO,SAAS,gBAAgB,OAAiB,MAAc,MAAuB;AACpF,MAAI,OAAO,KAAK,OAAO,MAAM,OAAQ,QAAO;AAC5C,QAAM,SAAS,IAAI;AAAA,IACjB,MACG,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,EACrC,KAAK,GAAG,EACR,YAAY,EACZ,MAAM,qBAAqB,KAAK,CAAC;AAAA,EACtC;AACA,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,KAAK,YAAY,EAAE,MAAM,qBAAqB,KAAK,CAAC,CAAC,CAAC;AAChF,QAAM,SAAS,MAAM,OAAO,CAAC,SAAS,OAAO,IAAI,IAAI,CAAC,EAAE;AACxD,SAAO,UAAU,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,SAAS;AACzD;AAGA,SAAS,SAAS,OAA+D;AAC/E,QAAM,MAAoD,CAAC;AAC3D,MAAI,UAAiD,CAAC;AACtD,MAAI,OAAO;AACX,QAAM,QAAQ,CAAC,MAAM,UAAU;AAC7B,UAAM,UAAU,YAAY,KAAK,IAAI;AACrC,QAAI,QAAQ,SAAS,MAAM,WAAW,OAAO,KAAK,SAAS,gBAAgB;AACzE,UAAI,KAAK,OAAO;AAChB,gBAAU,CAAC;AACX,aAAO;AAAA,IACT;AACA,QAAI,KAAK,KAAK,GAAG;AACf,cAAQ,KAAK,EAAE,MAAM,QAAQ,GAAG,MAAM,KAAK,MAAM,GAAG,GAAG,EAAE,CAAC;AAC1D,cAAQ,KAAK,IAAI,KAAK,QAAQ,GAAG,IAAI;AAAA,IACvC;AAAA,EACF,CAAC;AACD,MAAI,QAAQ,SAAS,EAAG,KAAI,KAAK,OAAO;AAExC,SAAO,IAAI,MAAM,GAAG,EAAE;AACxB;AAGA,SAAS,mBAAmB,MAAsB;AAEhD,SAAO,KAAK;AAAA,IACV;AAAA,IACA,CAAC,UAAU,MAAM,QAAQ,UAAU,EAAE;AAAA,EACvC;AACF;AAEA,SAAS,OAAO,MAAc,QAAwB;AACpD,QAAM,QAAQ,KAAK,QAAQ,MAAM;AACjC,SAAO,QAAQ,IAAI,IAAI,KAAK,MAAM,GAAG,KAAK,EAAE,MAAM,IAAI,EAAE;AAC1D;AAEA,SAAS,IAAI,MAAsB;AACjC,SAAO,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvD;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,KAAK,MAAM,cAAc,SAAS,kBAAkB;AAC7D;AAEA,eAAe,WAAW,MAA+B;AACvD,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,MAAM,SAAS,WAAW,IAAI,GAAG,MAAM,CAAC;AAClE,QAAI,OAAO,YAAY,KAAK,OAAO,QAAS,QAAO;AAAA,EACrD,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,SAAS,GAAG,SAAS,CAAC,EAAE;AACnC;AAEA,eAAe,YAAY,MAAc,QAA+B;AACtE,QAAM,MAAM,QAAQ,WAAW,IAAI,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,QAAM,YAAY,WAAW,IAAI,GAAG,KAAK,UAAU,MAAM,CAAC;AAC5D;","names":["text"]}
@@ -0,0 +1,55 @@
1
+ // src/llm/card-lock.ts
2
+ import { link, mkdir, readFile, rm, writeFile } from "fs/promises";
3
+ import { dirname, join } from "path";
4
+ function cardLockPath(root) {
5
+ return join(root, ".codebuddy", "cache", "cards.lock");
6
+ }
7
+ async function withCardLock(root, fn) {
8
+ const path = cardLockPath(root);
9
+ await mkdir(dirname(path), { recursive: true });
10
+ const temporary = `${path}.${process.pid}`;
11
+ await writeFile(temporary, `${process.pid}
12
+ `, { mode: 384 });
13
+ try {
14
+ for (let attempt = 0; ; attempt++) {
15
+ try {
16
+ await link(temporary, path);
17
+ break;
18
+ } catch (error) {
19
+ if (error.code !== "EEXIST") throw error;
20
+ const holder = await cardLockHolder(root);
21
+ if (holder !== null || attempt > 0) return { ok: false, holder };
22
+ await rm(path, { force: true });
23
+ }
24
+ }
25
+ } finally {
26
+ await rm(temporary, { force: true });
27
+ }
28
+ try {
29
+ return { ok: true, value: await fn() };
30
+ } finally {
31
+ if (await readPid(path) === process.pid) await rm(path, { force: true });
32
+ }
33
+ }
34
+ async function cardLockHolder(root) {
35
+ const pid = await readPid(cardLockPath(root));
36
+ return pid !== null && isAlive(pid) ? pid : null;
37
+ }
38
+ function isAlive(pid) {
39
+ try {
40
+ process.kill(pid, 0);
41
+ return true;
42
+ } catch (error) {
43
+ return error.code === "EPERM";
44
+ }
45
+ }
46
+ async function readPid(path) {
47
+ const pid = Number.parseInt(await readFile(path, "utf8").catch(() => ""), 10);
48
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
49
+ }
50
+
51
+ export {
52
+ withCardLock,
53
+ cardLockHolder
54
+ };
55
+ //# sourceMappingURL=chunk-R5JRKMEP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/llm/card-lock.ts"],"sourcesContent":["import { link, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * One model writer per repository: intent cards, index facts, file and reuse\n * vectors. Setup (`codebuddy`) starts a detached `index --cards`, every\n * `codebuddy serve` schedules its own pass and every capture a `hooks sync`:\n * without this each loads its own model copy, they oversubscribe the CPU until\n * every file times out, and each rewrites the shared cache from its own copy.\n * The lock file holds the writer's PID; a dead PID is a stale lock.\n */\nexport function cardLockPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"cards.lock\");\n}\n\nexport type CardLockResult<T> = { ok: true; value: T } | { ok: false; holder: number | null };\n\n/** Run `fn` holding the lock, or return the live holder's PID without running it. */\nexport async function withCardLock<T>(\n root: string,\n fn: () => Promise<T>,\n): Promise<CardLockResult<T>> {\n const path = cardLockPath(root);\n await mkdir(dirname(path), { recursive: true });\n // Written whole, then linked: a reader never sees an empty lock file.\n const temporary = `${path}.${process.pid}`;\n await writeFile(temporary, `${process.pid}\\n`, { mode: 0o600 });\n try {\n for (let attempt = 0; ; attempt++) {\n try {\n await link(temporary, path);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n const holder = await cardLockHolder(root);\n if (holder !== null || attempt > 0) return { ok: false, holder };\n // ponytail: two processes clearing the same stale lock at once can both\n // win; needs a rename-and-recheck if that window ever matters.\n await rm(path, { force: true });\n }\n }\n } finally {\n await rm(temporary, { force: true });\n }\n try {\n return { ok: true, value: await fn() };\n } finally {\n if ((await readPid(path)) === process.pid) await rm(path, { force: true });\n }\n}\n\n/** The live process writing cards, or null (no lock, or its process is gone). */\nexport async function cardLockHolder(root: string): Promise<number | null> {\n const pid = await readPid(cardLockPath(root));\n return pid !== null && isAlive(pid) ? pid : null;\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nasync function readPid(path: string): Promise<number | null> {\n const pid = Number.parseInt(await readFile(path, \"utf8\").catch(() => \"\"), 10);\n return Number.isInteger(pid) && pid > 0 ? pid : null;\n}\n"],"mappings":";AAAA,SAAS,MAAM,OAAO,UAAU,IAAI,iBAAiB;AACrD,SAAS,SAAS,YAAY;AAUvB,SAAS,aAAa,MAAsB;AACjD,SAAO,KAAK,MAAM,cAAc,SAAS,YAAY;AACvD;AAKA,eAAsB,aACpB,MACA,IAC4B;AAC5B,QAAM,OAAO,aAAa,IAAI;AAC9B,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAE9C,QAAM,YAAY,GAAG,IAAI,IAAI,QAAQ,GAAG;AACxC,QAAM,UAAU,WAAW,GAAG,QAAQ,GAAG;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAC9D,MAAI;AACF,aAAS,UAAU,KAAK,WAAW;AACjC,UAAI;AACF,cAAM,KAAK,WAAW,IAAI;AAC1B;AAAA,MACF,SAAS,OAAO;AACd,YAAK,MAAgC,SAAS,SAAU,OAAM;AAC9D,cAAM,SAAS,MAAM,eAAe,IAAI;AACxC,YAAI,WAAW,QAAQ,UAAU,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO;AAG/D,cAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,MAChC;AAAA,IACF;AAAA,EACF,UAAE;AACA,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,EACrC;AACA,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,OAAO,MAAM,GAAG,EAAE;AAAA,EACvC,UAAE;AACA,QAAK,MAAM,QAAQ,IAAI,MAAO,QAAQ,IAAK,OAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,EAC3E;AACF;AAGA,eAAsB,eAAe,MAAsC;AACzE,QAAM,MAAM,MAAM,QAAQ,aAAa,IAAI,CAAC;AAC5C,SAAO,QAAQ,QAAQ,QAAQ,GAAG,IAAI,MAAM;AAC9C;AAEA,SAAS,QAAQ,KAAsB;AACrC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAQ,MAAgC,SAAS;AAAA,EACnD;AACF;AAEA,eAAe,QAAQ,MAAsC;AAC3D,QAAM,MAAM,OAAO,SAAS,MAAM,SAAS,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE;AAC5E,SAAO,OAAO,UAAU,GAAG,KAAK,MAAM,IAAI,MAAM;AAClD;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runLeanHook
3
- } from "./chunk-JJQDVGF7.js";
3
+ } from "./chunk-3TZNQJTH.js";
4
4
  import {
5
5
  CLIENT_ADAPTERS,
6
6
  CLIENT_IDS,
@@ -21,16 +21,16 @@ import {
21
21
  runResetHook,
22
22
  runResumeHook,
23
23
  runStageHook
24
- } from "./chunk-KQCP3ZYH.js";
24
+ } from "./chunk-5ZLNHYJH.js";
25
25
  import {
26
26
  hookRoot
27
27
  } from "./chunk-TGFTKWOR.js";
28
28
  import {
29
29
  indexRepository
30
- } from "./chunk-UR3DF74I.js";
30
+ } from "./chunk-LZTYPJ5I.js";
31
31
  import {
32
32
  resolveNamespaceFrom
33
- } from "./chunk-M75SN4D4.js";
33
+ } from "./chunk-Y4QQML24.js";
34
34
  import {
35
35
  writeAtomic
36
36
  } from "./chunk-CG5B3LYG.js";
@@ -1662,4 +1662,4 @@ export {
1662
1662
  setHooks,
1663
1663
  detectClient
1664
1664
  };
1665
- //# sourceMappingURL=chunk-RZ266NHE.js.map
1665
+ //# sourceMappingURL=chunk-SPQU2422.js.map