@theglitchking/semantic-memory 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +90 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +49 -29
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/{chunk-7PNF3AP2.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.js.map +1 -0
- package/dist/{chunk-WZEKIUYG.js → chunk-6TIKWOY4.js} +10 -8
- package/dist/{chunk-WZEKIUYG.js.map → chunk-6TIKWOY4.js.map} +1 -1
- package/dist/{chunk-NBY4TRP7.js → chunk-7M34JDTH.js} +3 -3
- package/dist/{chunk-FTRXH2WC.js → chunk-7UAVDOI6.js} +15 -12
- package/dist/chunk-7UAVDOI6.js.map +1 -0
- package/dist/{chunk-BBP3DG2U.js → chunk-AIXQFTSB.js} +25 -26
- package/dist/{chunk-BBP3DG2U.js.map → chunk-AIXQFTSB.js.map} +1 -1
- package/dist/chunk-BEACL25M.js +224 -0
- package/dist/chunk-BEACL25M.js.map +1 -0
- package/dist/chunk-CATPGINC.js +186 -0
- package/dist/chunk-CATPGINC.js.map +1 -0
- package/dist/{chunk-IGHCVAAL.js → chunk-CJUY55GG.js} +6 -3
- package/dist/{chunk-IGHCVAAL.js.map → chunk-CJUY55GG.js.map} +1 -1
- package/dist/chunk-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.js.map +1 -0
- package/dist/{chunk-XAXK7NM4.js → chunk-EXAUVIGW.js} +9 -7
- package/dist/{chunk-XAXK7NM4.js.map → chunk-EXAUVIGW.js.map} +1 -1
- package/dist/{chunk-LGQPP5FQ.js → chunk-EYVFC745.js} +2 -2
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/{chunk-OEF5U5GV.js → chunk-H2BES6FU.js} +10 -2
- package/dist/{chunk-OEF5U5GV.js.map → chunk-H2BES6FU.js.map} +1 -1
- package/dist/chunk-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.js.map +1 -0
- package/dist/chunk-J7LYDRML.js +9 -0
- package/dist/chunk-J7LYDRML.js.map +1 -0
- package/dist/chunk-MQS7JEBD.js +137 -0
- package/dist/chunk-MQS7JEBD.js.map +1 -0
- package/dist/chunk-NCGM36NX.js +118 -0
- package/dist/chunk-NCGM36NX.js.map +1 -0
- package/dist/chunk-NIZLBNH4.js +165 -0
- package/dist/chunk-NIZLBNH4.js.map +1 -0
- package/dist/chunk-OT3MV7XF.js +123 -0
- package/dist/chunk-OT3MV7XF.js.map +1 -0
- package/dist/chunk-P3GLY42T.js +291 -0
- package/dist/chunk-P3GLY42T.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/{chunk-44BQDRXD.js → chunk-SHSOCFJH.js} +4 -1
- package/dist/chunk-SHSOCFJH.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/{chunk-CAX46XLY.js → chunk-VBLCUWT4.js} +23 -12
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-SX427M2R.js → chunk-XKGK643R.js} +2 -2
- package/dist/{chunk-FURCIJBO.js → chunk-Y7H76Q7F.js} +6 -3
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +389 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -20
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/redact.js +1 -1
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +40 -0
- package/dist/core/skill-queue.js +38 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-RWTIMGZN.js → dossier-RXBLQOXC.js} +5 -4
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/healthcheck-S7D6YNTA.js +21 -0
- package/dist/indexer-RZPH2PVL.js +9 -0
- package/dist/{lexicon-IVNJTMWE.js → lexicon-SEJSLBHO.js} +6 -5
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/lint-3JNLRM5U.js +18 -0
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/log-RM2NDNPH.js +11 -0
- package/dist/log-RM2NDNPH.js.map +1 -0
- package/dist/matter-SNJ2WDL6.js +8 -0
- package/dist/matter-SNJ2WDL6.js.map +1 -0
- package/dist/mcp/server.js +43 -38
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-UE4YVTDC.js → profile-SNULTJ27.js} +6 -5
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-LY6533NJ.js → schema-RDYUK6CC.js} +3 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/{search-MZC4DC7H.js → search-325ULY57.js} +4 -3
- package/dist/{search-MZC4DC7H.js.map → search-325ULY57.js.map} +1 -1
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-XU2APPBJ.js +31 -0
- package/dist/skill-land-XU2APPBJ.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-GXBQLBZA.js +24 -0
- package/dist/skill-merge-GXBQLBZA.js.map +1 -0
- package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/dist/{telemetry-B4KP3UVE.js → telemetry-YAG3VA4Y.js} +4 -4
- package/dist/telemetry-YAG3VA4Y.js.map +1 -0
- package/docs/regression-snapshot.md +5 -5
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-44BQDRXD.js.map +0 -1
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-CAX46XLY.js.map +0 -1
- package/dist/chunk-FTRXH2WC.js.map +0 -1
- package/dist/chunk-FURCIJBO.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- package/dist/healthcheck-3FSNL2U7.js +0 -20
- package/dist/indexer-RAKJ7JR3.js +0 -8
- package/dist/lint-36WTW762.js +0 -17
- package/dist/log-YIXF6CYM.js +0 -11
- /package/dist/{chunk-7PNF3AP2.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-NBY4TRP7.js.map → chunk-7M34JDTH.js.map} +0 -0
- /package/dist/{chunk-LGQPP5FQ.js.map → chunk-EYVFC745.js.map} +0 -0
- /package/dist/{chunk-SX427M2R.js.map → chunk-XKGK643R.js.map} +0 -0
- /package/dist/{dossier-RWTIMGZN.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-3FSNL2U7.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{indexer-RAKJ7JR3.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lexicon-IVNJTMWE.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{lint-36WTW762.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{log-YIXF6CYM.js.map → dossier-RXBLQOXC.js.map} +0 -0
- /package/dist/{profile-UE4YVTDC.js.map → failure-eval-IUJO47UC.js.map} +0 -0
- /package/dist/{schema-LY6533NJ.js.map → healthcheck-S7D6YNTA.js.map} +0 -0
- /package/dist/{telemetry-B4KP3UVE.js.map → indexer-RZPH2PVL.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/dossier.ts","../src/core/section.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { redact } from \"./redact.js\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { extractSection } from \"./section.js\";\n\n/**\n * Entity dossiers (v1.5 Phase 8) — the organizing unit of the resident expert.\n *\n * A dossier is a living note about one critical component/entity: what it is, how\n * it fails, which knobs to turn, its incident history, and its current state.\n * Knowledge accretes IN PLACE (incidents append to a log section; current-state is\n * revised) instead of spawning a new note per event — so the entity, not the file,\n * is the memory's unit. Canonical storage is markdown under <vault>/dossiers/\n * (type `dossier`); a derived JSON cache under .claude/.semantic-memory/ lets the\n * hot-path hook resolve an utterance to a dossier head for two-hop retrieval.\n */\n\nexport const DOSSIER_DIR = \"dossiers\";\nconst CACHE_FILE = \"dossier-cache.json\";\n\n/** The fixed section skeleton every dossier carries (order is meaningful). */\nexport const DOSSIER_SECTIONS = [\n \"Purpose\",\n \"Failure modes\",\n \"Knobs & commands\",\n \"Incident log\",\n \"Current state\",\n] as const;\n\nexport type DossierSection = (typeof DOSSIER_SECTIONS)[number];\n\nexport interface DossierEntry {\n /** The canonical entity name (e.g. \"payment-gateway\"). */\n entity: string;\n /** The human's phrases for this entity (feed the lexicon compiler). */\n aliases: string[];\n /** Dossier note path relative to vault root. */\n path: string;\n /** First paragraph of the Purpose section — the head injected on two-hop. */\n purpose: string;\n /** The Current state section body — the freshest one-liner about the entity. */\n current_state: string;\n}\n\nexport function slugifyEntity(entity: string): string {\n return (\n entity\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"entity\"\n );\n}\n\nexport function normalizeEntity(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\n/** Body-only text of a section (heading + leading/trailing blank lines stripped). */\nfunction sectionBody(markdown: string, heading: string): string {\n const section = extractSection(markdown, heading);\n if (!section) return \"\";\n // Drop the heading line, collapse to a trimmed body.\n return section\n .split(\"\\n\")\n .slice(1)\n .join(\"\\n\")\n .trim();\n}\n\n/** First non-empty paragraph of a section body (the \"head\" for injection). */\nfunction firstParagraph(body: string): string {\n const para = body.split(/\\n\\s*\\n/).map((p) => p.trim()).find(Boolean);\n return para ?? \"\";\n}\n\n/**\n * The dossier template — the fixed skeleton `dossier init` scaffolds and every\n * section edit patches into. Placeholders are intentionally terse prompts, not\n * prose, so an empty dossier is cheap to read and obvious to fill.\n */\nexport function dossierTemplate(entity: string, opts: { purpose?: string; seeded_from?: string } = {}): string {\n const purpose = opts.purpose?.trim() || `_What ${entity} is and why it exists._`;\n const seedNote = opts.seeded_from ? `\\n<!-- seeded from ${opts.seeded_from} -->\\n` : \"\";\n return `# ${entity}\n${seedNote}\n## Purpose\n\n${purpose}\n\n## Failure modes\n\n_How it breaks, and the tell-tale symptoms._\n\n## Knobs & commands\n\n_The buttons to push: commands, config, flags, dashboards._\n\n## Incident log\n\n_Dated entries append here (newest last). Each: what happened → cause → fix._\n\n## Current state\n\n_The single freshest sentence about where this entity stands right now._\n`;\n}\n\n/** Read every dossier note into an entry (best-effort; skips unreadable files). */\nexport async function listDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const files = await glob(`${DOSSIER_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: DossierEntry[] = [];\n for (const rel of files) {\n try {\n const raw = await readFile(join(vaultPath, rel), \"utf-8\");\n const { data, content } = matter(raw);\n if (data.type !== \"dossier\") continue;\n const entity = typeof data.entity === \"string\" && data.entity.trim() ? data.entity.trim() : rel.replace(/^dossiers\\//, \"\").replace(/\\.md$/, \"\");\n const aliases = Array.isArray(data.aliases) ? data.aliases.map(String).map((a) => a.trim()).filter(Boolean) : [];\n entries.push({\n entity,\n aliases,\n path: rel,\n purpose: firstParagraph(sectionBody(content, \"Purpose\")),\n current_state: sectionBody(content, \"Current state\"),\n });\n } catch {\n /* skip unreadable dossier */\n }\n }\n return entries.sort((a, b) => a.entity.localeCompare(b.entity));\n}\n\n/** Compile all dossiers into the derived cache the hook reads for two-hop routing. */\nexport async function compileDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const entries = await listDossiers(vaultPath);\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(\n join(dir, CACHE_FILE),\n JSON.stringify({ compiledAt: new Date().toISOString(), dossiers: entries }, null, 2),\n \"utf-8\"\n );\n } catch {\n /* cache write is best-effort */\n }\n return entries;\n}\n\n/** Fast read for the hook: use the cache if present, else compile. */\nexport async function loadDossierCache(vaultPath: string): Promise<DossierEntry[]> {\n const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);\n if (existsSync(cachePath)) {\n try {\n const data = JSON.parse(await readFile(cachePath, \"utf-8\"));\n if (Array.isArray(data?.dossiers)) return data.dossiers as DossierEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileDossiers(vaultPath);\n}\n\n/** Find a dossier by exact entity name or any of its aliases (normalized). */\nexport function findDossier(entries: DossierEntry[], entityOrAlias: string): DossierEntry | undefined {\n const n = normalizeEntity(entityOrAlias);\n return entries.find(\n (d) => normalizeEntity(d.entity) === n || d.aliases.some((a) => normalizeEntity(a) === n)\n );\n}\n\n/**\n * Resolve an utterance to a dossier for two-hop retrieval: the query mentions the\n * entity name or one of its aliases. Returns the best (longest-alias) match so a\n * specific phrase wins over a generic one. Deterministic — no LLM, no embedding.\n */\nexport function resolveDossierForQuery(entries: DossierEntry[], query: string): DossierEntry | undefined {\n const nq = normalizeEntity(query);\n let best: { entry: DossierEntry; keyLen: number } | undefined;\n for (const d of entries) {\n const keys = [d.entity, ...d.aliases].map(normalizeEntity).filter(Boolean);\n for (const k of keys) {\n // Word-ish containment: the key appears in the query.\n if (nq.includes(k) && (!best || k.length > best.keyLen)) {\n best = { entry: d, keyLen: k.length };\n }\n }\n }\n return best?.entry;\n}\n\nasync function readDossierRaw(vaultPath: string, rel: string): Promise<{ raw: string; entity: string } | null> {\n const abs = join(vaultPath, rel);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { data } = matter(raw);\n if (data.type !== \"dossier\") return null;\n const entity = typeof data.entity === \"string\" ? data.entity : rel;\n return { raw, entity };\n } catch {\n return null;\n }\n}\n\n/** Resolve an entity/alias/path argument to a concrete dossier note path. */\nexport async function resolveDossierPath(vaultPath: string, entityOrPath: string): Promise<string | null> {\n // Direct path?\n if (entityOrPath.endsWith(\".md\") && existsSync(join(vaultPath, entityOrPath))) return entityOrPath;\n const entries = await listDossiers(vaultPath);\n const match = findDossier(entries, entityOrPath);\n if (match) return match.path;\n // Fall back to the slug path even if not yet created.\n return null;\n}\n\nexport interface InitDossierResult {\n path: string;\n created: boolean;\n entity: string;\n redacted?: number;\n}\n\n/**\n * Scaffold a dossier for an entity. No-op (created:false) if one already exists for\n * that entity/slug. `seeded_from` records provenance (e.g. a babel-fish project-map\n * entry) without failing when that source is absent — same fail-open posture as the\n * v1.2 code-symbol drift seed.\n */\nexport async function initDossier(\n vaultPath: string,\n entity: string,\n opts: { aliases?: string[]; purpose?: string; seeded_from?: string } = {}\n): Promise<InitDossierResult> {\n const existing = await listDossiers(vaultPath);\n const already = findDossier(existing, entity);\n if (already) return { path: already.path, created: false, entity: already.entity };\n\n const rel = join(DOSSIER_DIR, `${slugifyEntity(entity)}.md`);\n const abs = join(vaultPath, rel);\n if (existsSync(abs)) return { path: rel, created: false, entity };\n\n const today = new Date().toISOString().slice(0, 10);\n const purpose = redact(opts.purpose ?? \"\");\n const fm: Record<string, unknown> = {\n title: entity,\n type: \"dossier\",\n entity,\n status: \"active\",\n last_verified: today,\n confidence: \"medium\",\n ...(opts.aliases && opts.aliases.length > 0 ? { aliases: opts.aliases } : {}),\n ...(opts.seeded_from ? { sources: [opts.seeded_from] } : {}),\n };\n const body = dossierTemplate(entity, { purpose: opts.purpose === undefined ? undefined : purpose.text, seeded_from: opts.seeded_from });\n await mkdir(join(vaultPath, DOSSIER_DIR), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, created: true, entity, ...(purpose.count ? { redacted: purpose.count } : {}) };\n}\n\n/**\n * Append a dated incident line to a dossier's Incident log (accretion, never a new\n * file). Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function appendIncident(\n vaultPath: string,\n entityOrPath: string,\n incident: string,\n opts: { today?: string } = {}\n): Promise<{ path: string; redacted?: number } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const today = opts.today ?? new Date().toISOString().slice(0, 10);\n const text = redact(incident.trim());\n const line = `- ${today} — ${text.text}`;\n const patched = appendToSection(doc.raw, \"Incident log\", line);\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, ...(text.count ? { redacted: text.count } : {}) };\n}\n\n/**\n * Replace the body of a dossier's Current state section (the freshest one-liner).\n * Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function setCurrentState(\n vaultPath: string,\n entityOrPath: string,\n state: string\n): Promise<{ path: string; redacted?: number } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const text = redact(state.trim());\n const patched = replaceSectionBody(doc.raw, \"Current state\", text.text);\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, ...(text.count ? { redacted: text.count } : {}) };\n}\n\n// --- section-level markdown editing (heading-scoped, format-preserving) ---\n\n/** Split into lines, find a `## <heading>` (case-insensitive), return its bounds. */\nfunction locateSection(markdown: string, heading: string): { start: number; level: number; end: number; lines: string[] } | null {\n const lines = markdown.split(\"\\n\");\n const want = heading.trim().toLowerCase();\n let start = -1;\n let level = 0;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+(.*?)\\s*$/);\n if (m && m[2].trim().toLowerCase() === want) {\n start = i;\n level = m[1].length;\n break;\n }\n }\n if (start === -1) return null;\n let end = lines.length;\n for (let i = start + 1; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+/);\n if (m && m[1].length <= level) {\n end = i;\n break;\n }\n }\n return { start, level, end, lines };\n}\n\n/** True if a section body is only the template placeholder (an italic prompt). */\nfunction isPlaceholderBody(body: string): boolean {\n const t = body.trim();\n return t === \"\" || (/^_.*_$/.test(t) && !t.includes(\"\\n\"));\n}\n\n/** Append a line to the end of a section's body (before the next heading/EOF). */\nexport function appendToSection(markdown: string, heading: string, line: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n // Section absent — append a fresh one at EOF.\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${line}\\n`;\n }\n const { lines, start, end } = loc;\n const body = lines.slice(start + 1, end).join(\"\\n\");\n // If the body is just the placeholder prompt, replace it with the first real line.\n const bodyStripped = body.trim();\n const replacePlaceholder = isPlaceholderBody(bodyStripped);\n // Find the last non-empty line index within the section body.\n let insertAt = end;\n while (insertAt - 1 > start && lines[insertAt - 1].trim() === \"\") insertAt--;\n const before = lines.slice(0, replacePlaceholder ? start + 1 : insertAt);\n const after = lines.slice(replacePlaceholder ? end : insertAt);\n const injected = replacePlaceholder ? [\"\", line] : [line];\n return [...before, ...injected, ...after].join(\"\\n\");\n}\n\n/** Replace a section's entire body (keeps the heading), preserving surrounding doc. */\nexport function replaceSectionBody(markdown: string, heading: string, newBody: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${newBody}\\n`;\n }\n const { lines, start, end } = loc;\n const before = lines.slice(0, start + 1);\n const after = lines.slice(end);\n return [...before, \"\", newBody, \"\", ...after].join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\");\n}\n","/**\n * Section-targeted read (v1.4 Phase 7). Extract one heading's section from a\n * markdown document so pulling a dossier's *Knobs* section doesn't cost the whole\n * file. Matches a heading by text (case-insensitive), returns from that heading\n * through to the next heading of the same or higher level.\n */\nexport function extractSection(markdown: string, heading: string): string | null {\n const target = heading.trim().toLowerCase();\n const lines = markdown.split(\"\\n\");\n let startIdx = -1;\n let startLevel = 0;\n\n const headingAt = (line: string): { level: number; text: string } | null => {\n const m = /^(#{1,6})\\s+(.*?)\\s*$/.exec(line);\n return m ? { level: m[1].length, text: m[2].trim().toLowerCase() } : null;\n };\n\n for (let i = 0; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.text === target) {\n startIdx = i;\n startLevel = h.level;\n break;\n }\n }\n if (startIdx === -1) return null;\n\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.level <= startLevel) {\n endIdx = i;\n break;\n }\n }\n return lines.slice(startIdx, endIdx).join(\"\\n\").trimEnd();\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,UAAU,WAAW,aAAa;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AACrB,SAAS,YAAY;AACrB,OAAO,YAAY;;;ACEZ,SAAS,eAAe,UAAkB,SAAgC;AAC/E,QAAM,SAAS,QAAQ,KAAK,EAAE,YAAY;AAC1C,QAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,MAAI,WAAW;AACf,MAAI,aAAa;AAEjB,QAAM,YAAY,CAAC,SAAyD;AAC1E,UAAM,IAAI,wBAAwB,KAAK,IAAI;AAC3C,WAAO,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI;AAAA,EACvE;AAEA,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,IAAI,UAAU,MAAM,CAAC,CAAC;AAC5B,QAAI,KAAK,EAAE,SAAS,QAAQ;AAC1B,iBAAW;AACX,mBAAa,EAAE;AACf;AAAA,IACF;AAAA,EACF;AACA,MAAI,aAAa,GAAI,QAAO;AAE5B,MAAI,SAAS,MAAM;AACnB,WAAS,IAAI,WAAW,GAAG,IAAI,MAAM,QAAQ,KAAK;AAChD,UAAM,IAAI,UAAU,MAAM,CAAC,CAAC;AAC5B,QAAI,KAAK,EAAE,SAAS,YAAY;AAC9B,eAAS;AACT;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,MAAM,UAAU,MAAM,EAAE,KAAK,IAAI,EAAE,QAAQ;AAC1D;;;ADfO,IAAM,cAAc;AAC3B,IAAM,aAAa;AAGZ,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAiBO,SAAS,cAAc,QAAwB;AACpD,SACE,OACG,YAAY,EACZ,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,KAAK;AAEvB;AAEO,SAAS,gBAAgB,GAAmB;AACjD,SAAO,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,QAAQ,GAAG;AACnD;AAGA,SAAS,YAAY,UAAkB,SAAyB;AAC9D,QAAM,UAAU,eAAe,UAAU,OAAO;AAChD,MAAI,CAAC,QAAS,QAAO;AAErB,SAAO,QACJ,MAAM,IAAI,EACV,MAAM,CAAC,EACP,KAAK,IAAI,EACT,KAAK;AACV;AAGA,SAAS,eAAe,MAAsB;AAC5C,QAAM,OAAO,KAAK,MAAM,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,OAAO;AACpE,SAAO,QAAQ;AACjB;AAOO,SAAS,gBAAgB,QAAgB,OAAmD,CAAC,GAAW;AAC7G,QAAM,UAAU,KAAK,SAAS,KAAK,KAAK,SAAS,MAAM;AACvD,QAAM,WAAW,KAAK,cAAc;AAAA,mBAAsB,KAAK,WAAW;AAAA,IAAW;AACrF,SAAO,KAAK,MAAM;AAAA,EAClB,QAAQ;AAAA;AAAA;AAAA,EAGR,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBT;AAGA,eAAsB,aAAa,WAA4C;AAC7E,QAAM,QAAQ,MAAM,KAAK,GAAG,WAAW,YAAY,EAAE,KAAK,UAAU,CAAC;AACrE,QAAM,UAA0B,CAAC;AACjC,aAAW,OAAO,OAAO;AACvB,QAAI;AACF,YAAM,MAAM,MAAM,SAAS,KAAK,WAAW,GAAG,GAAG,OAAO;AACxD,YAAM,EAAE,MAAM,QAAQ,IAAI,OAAO,GAAG;AACpC,UAAI,KAAK,SAAS,UAAW;AAC7B,YAAM,SAAS,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,KAAK,IAAI,KAAK,OAAO,KAAK,IAAI,IAAI,QAAQ,eAAe,EAAE,EAAE,QAAQ,SAAS,EAAE;AAC9I,YAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,IAAI,CAAC;AAC/G,cAAQ,KAAK;AAAA,QACX;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN,SAAS,eAAe,YAAY,SAAS,SAAS,CAAC;AAAA,QACvD,eAAe,YAAY,SAAS,eAAe;AAAA,MACrD,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAC;AAChE;AAGA,eAAsB,gBAAgB,WAA4C;AAChF,QAAM,UAAU,MAAM,aAAa,SAAS;AAC5C,MAAI;AACF,UAAM,MAAM,iBAAiB,SAAS;AACtC,UAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC,UAAM;AAAA,MACJ,KAAK,KAAK,UAAU;AAAA,MACpB,KAAK,UAAU,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,GAAG,UAAU,QAAQ,GAAG,MAAM,CAAC;AAAA,MACnF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,eAAsB,iBAAiB,WAA4C;AACjF,QAAM,YAAY,KAAK,iBAAiB,SAAS,GAAG,UAAU;AAC9D,MAAI,WAAW,SAAS,GAAG;AACzB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,MAAM,SAAS,WAAW,OAAO,CAAC;AAC1D,UAAI,MAAM,QAAQ,MAAM,QAAQ,EAAG,QAAO,KAAK;AAAA,IACjD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,gBAAgB,SAAS;AAClC;AAGO,SAAS,YAAY,SAAyB,eAAiD;AACpG,QAAM,IAAI,gBAAgB,aAAa;AACvC,SAAO,QAAQ;AAAA,IACb,CAAC,MAAM,gBAAgB,EAAE,MAAM,MAAM,KAAK,EAAE,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,CAAC;AAAA,EAC1F;AACF;AAOO,SAAS,uBAAuB,SAAyB,OAAyC;AACvG,QAAM,KAAK,gBAAgB,KAAK;AAChC,MAAI;AACJ,aAAW,KAAK,SAAS;AACvB,UAAM,OAAO,CAAC,EAAE,QAAQ,GAAG,EAAE,OAAO,EAAE,IAAI,eAAe,EAAE,OAAO,OAAO;AACzE,eAAW,KAAK,MAAM;AAEpB,UAAI,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,SAAS,KAAK,SAAS;AACvD,eAAO,EAAE,OAAO,GAAG,QAAQ,EAAE,OAAO;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM;AACf;AAEA,eAAe,eAAe,WAAmB,KAA8D;AAC7G,QAAM,MAAM,KAAK,WAAW,GAAG;AAC/B,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,UAAM,EAAE,KAAK,IAAI,OAAO,GAAG;AAC3B,QAAI,KAAK,SAAS,UAAW,QAAO;AACpC,UAAM,SAAS,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS;AAC/D,WAAO,EAAE,KAAK,OAAO;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,mBAAmB,WAAmB,cAA8C;AAExG,MAAI,aAAa,SAAS,KAAK,KAAK,WAAW,KAAK,WAAW,YAAY,CAAC,EAAG,QAAO;AACtF,QAAM,UAAU,MAAM,aAAa,SAAS;AAC5C,QAAM,QAAQ,YAAY,SAAS,YAAY;AAC/C,MAAI,MAAO,QAAO,MAAM;AAExB,SAAO;AACT;AAeA,eAAsB,YACpB,WACA,QACA,OAAuE,CAAC,GAC5C;AAC5B,QAAM,WAAW,MAAM,aAAa,SAAS;AAC7C,QAAM,UAAU,YAAY,UAAU,MAAM;AAC5C,MAAI,QAAS,QAAO,EAAE,MAAM,QAAQ,MAAM,SAAS,OAAO,QAAQ,QAAQ,OAAO;AAEjF,QAAM,MAAM,KAAK,aAAa,GAAG,cAAc,MAAM,CAAC,KAAK;AAC3D,QAAM,MAAM,KAAK,WAAW,GAAG;AAC/B,MAAI,WAAW,GAAG,EAAG,QAAO,EAAE,MAAM,KAAK,SAAS,OAAO,OAAO;AAEhE,QAAM,SAAQ,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAClD,QAAM,UAAU,OAAO,KAAK,WAAW,EAAE;AACzC,QAAM,KAA8B;AAAA,IAClC,OAAO;AAAA,IACP,MAAM;AAAA,IACN;AAAA,IACA,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,GAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,IAAI,EAAE,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,IAC3E,GAAI,KAAK,cAAc,EAAE,SAAS,CAAC,KAAK,WAAW,EAAE,IAAI,CAAC;AAAA,EAC5D;AACA,QAAM,OAAO,gBAAgB,QAAQ,EAAE,SAAS,KAAK,YAAY,SAAY,SAAY,QAAQ,MAAM,aAAa,KAAK,YAAY,CAAC;AACtI,QAAM,MAAM,KAAK,WAAW,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7D,QAAM,UAAU,KAAK,OAAO,UAAU,MAAM,EAAE,GAAG,OAAO;AACxD,QAAM,gBAAgB,SAAS;AAC/B,SAAO,EAAE,MAAM,KAAK,SAAS,MAAM,QAAQ,GAAI,QAAQ,QAAQ,EAAE,UAAU,QAAQ,MAAM,IAAI,CAAC,EAAG;AACnG;AAMA,eAAsB,eACpB,WACA,cACA,UACA,OAA2B,CAAC,GACyB;AACrD,QAAM,MAAM,MAAM,mBAAmB,WAAW,YAAY;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,MAAM,eAAe,WAAW,GAAG;AAC/C,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,KAAK,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAChE,QAAM,OAAO,OAAO,SAAS,KAAK,CAAC;AACnC,QAAM,OAAO,KAAK,KAAK,WAAM,KAAK,IAAI;AACtC,QAAM,UAAU,gBAAgB,IAAI,KAAK,gBAAgB,IAAI;AAC7D,QAAM,UAAU,KAAK,WAAW,GAAG,GAAG,SAAS,OAAO;AACtD,QAAM,gBAAgB,SAAS;AAC/B,SAAO,EAAE,MAAM,KAAK,GAAI,KAAK,QAAQ,EAAE,UAAU,KAAK,MAAM,IAAI,CAAC,EAAG;AACtE;AAMA,eAAsB,gBACpB,WACA,cACA,OACqD;AACrD,QAAM,MAAM,MAAM,mBAAmB,WAAW,YAAY;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,MAAM,MAAM,eAAe,WAAW,GAAG;AAC/C,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,OAAO,MAAM,KAAK,CAAC;AAChC,QAAM,UAAU,mBAAmB,IAAI,KAAK,iBAAiB,KAAK,IAAI;AACtE,QAAM,UAAU,KAAK,WAAW,GAAG,GAAG,SAAS,OAAO;AACtD,QAAM,gBAAgB,SAAS;AAC/B,SAAO,EAAE,MAAM,KAAK,GAAI,KAAK,QAAQ,EAAE,UAAU,KAAK,MAAM,IAAI,CAAC,EAAG;AACtE;AAKA,SAAS,cAAc,UAAkB,SAAwF;AAC/H,QAAM,QAAQ,SAAS,MAAM,IAAI;AACjC,QAAM,OAAO,QAAQ,KAAK,EAAE,YAAY;AACxC,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,IAAI,MAAM,CAAC,EAAE,MAAM,uBAAuB;AAChD,QAAI,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,MAAM,MAAM;AAC3C,cAAQ;AACR,cAAQ,EAAE,CAAC,EAAE;AACb;AAAA,IACF;AAAA,EACF;AACA,MAAI,UAAU,GAAI,QAAO;AACzB,MAAI,MAAM,MAAM;AAChB,WAAS,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,KAAK;AAC7C,UAAM,IAAI,MAAM,CAAC,EAAE,MAAM,cAAc;AACvC,QAAI,KAAK,EAAE,CAAC,EAAE,UAAU,OAAO;AAC7B,YAAM;AACN;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,OAAO,OAAO,KAAK,MAAM;AACpC;AAGA,SAAS,kBAAkB,MAAuB;AAChD,QAAM,IAAI,KAAK,KAAK;AACpB,SAAO,MAAM,MAAO,SAAS,KAAK,CAAC,KAAK,CAAC,EAAE,SAAS,IAAI;AAC1D;AAGO,SAAS,gBAAgB,UAAkB,SAAiB,MAAsB;AACvF,QAAM,MAAM,cAAc,UAAU,OAAO;AAC3C,MAAI,CAAC,KAAK;AAER,UAAM,MAAM,SAAS,SAAS,IAAI,IAAI,KAAK;AAC3C,WAAO,GAAG,QAAQ,GAAG,GAAG;AAAA,KAAQ,OAAO;AAAA;AAAA,EAAO,IAAI;AAAA;AAAA,EACpD;AACA,QAAM,EAAE,OAAO,OAAO,IAAI,IAAI;AAC9B,QAAM,OAAO,MAAM,MAAM,QAAQ,GAAG,GAAG,EAAE,KAAK,IAAI;AAElD,QAAM,eAAe,KAAK,KAAK;AAC/B,QAAM,qBAAqB,kBAAkB,YAAY;AAEzD,MAAI,WAAW;AACf,SAAO,WAAW,IAAI,SAAS,MAAM,WAAW,CAAC,EAAE,KAAK,MAAM,GAAI;AAClE,QAAM,SAAS,MAAM,MAAM,GAAG,qBAAqB,QAAQ,IAAI,QAAQ;AACvE,QAAM,QAAQ,MAAM,MAAM,qBAAqB,MAAM,QAAQ;AAC7D,QAAM,WAAW,qBAAqB,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI;AACxD,SAAO,CAAC,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,EAAE,KAAK,IAAI;AACrD;AAGO,SAAS,mBAAmB,UAAkB,SAAiB,SAAyB;AAC7F,QAAM,MAAM,cAAc,UAAU,OAAO;AAC3C,MAAI,CAAC,KAAK;AACR,UAAM,MAAM,SAAS,SAAS,IAAI,IAAI,KAAK;AAC3C,WAAO,GAAG,QAAQ,GAAG,GAAG;AAAA,KAAQ,OAAO;AAAA;AAAA,EAAO,OAAO;AAAA;AAAA,EACvD;AACA,QAAM,EAAE,OAAO,OAAO,IAAI,IAAI;AAC9B,QAAM,SAAS,MAAM,MAAM,GAAG,QAAQ,CAAC;AACvC,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,SAAO,CAAC,GAAG,QAAQ,IAAI,SAAS,IAAI,GAAG,KAAK,EAAE,KAAK,IAAI,EAAE,QAAQ,WAAW,MAAM;AACpF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/dossier.ts","../src/core/section.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"./matter.js\";\nimport { redact } from \"./redact.js\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { extractSection } from \"./section.js\";\n\n/**\n * Entity dossiers (v1.5 Phase 8) — the organizing unit of the resident expert.\n *\n * A dossier is a living note about one critical component/entity: what it is, how\n * it fails, which knobs to turn, its incident history, and its current state.\n * Knowledge accretes IN PLACE (incidents append to a log section; current-state is\n * revised) instead of spawning a new note per event — so the entity, not the file,\n * is the memory's unit. Canonical storage is markdown under <vault>/dossiers/\n * (type `dossier`); a derived JSON cache under .claude/.semantic-memory/ lets the\n * hot-path hook resolve an utterance to a dossier head for two-hop retrieval.\n */\n\nexport const DOSSIER_DIR = \"dossiers\";\nconst CACHE_FILE = \"dossier-cache.json\";\n\n/** The fixed section skeleton every dossier carries (order is meaningful). */\nexport const DOSSIER_SECTIONS = [\n \"Purpose\",\n \"Failure modes\",\n \"Knobs & commands\",\n \"Incident log\",\n \"Current state\",\n] as const;\n\nexport type DossierSection = (typeof DOSSIER_SECTIONS)[number];\n\nexport interface DossierEntry {\n /** The canonical entity name (e.g. \"payment-gateway\"). */\n entity: string;\n /** The human's phrases for this entity (feed the lexicon compiler). */\n aliases: string[];\n /** Dossier note path relative to vault root. */\n path: string;\n /** First paragraph of the Purpose section — the head injected on two-hop. */\n purpose: string;\n /** The Current state section body — the freshest one-liner about the entity. */\n current_state: string;\n}\n\nexport function slugifyEntity(entity: string): string {\n return (\n entity\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"entity\"\n );\n}\n\nexport function normalizeEntity(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\n/** Body-only text of a section (heading + leading/trailing blank lines stripped). */\nfunction sectionBody(markdown: string, heading: string): string {\n const section = extractSection(markdown, heading);\n if (!section) return \"\";\n // Drop the heading line, collapse to a trimmed body.\n return section\n .split(\"\\n\")\n .slice(1)\n .join(\"\\n\")\n .trim();\n}\n\n/** First non-empty paragraph of a section body (the \"head\" for injection). */\nfunction firstParagraph(body: string): string {\n const para = body.split(/\\n\\s*\\n/).map((p) => p.trim()).find(Boolean);\n return para ?? \"\";\n}\n\n/**\n * The dossier template — the fixed skeleton `dossier init` scaffolds and every\n * section edit patches into. Placeholders are intentionally terse prompts, not\n * prose, so an empty dossier is cheap to read and obvious to fill.\n */\nexport function dossierTemplate(entity: string, opts: { purpose?: string; seeded_from?: string } = {}): string {\n const purpose = opts.purpose?.trim() || `_What ${entity} is and why it exists._`;\n const seedNote = opts.seeded_from ? `\\n<!-- seeded from ${opts.seeded_from} -->\\n` : \"\";\n return `# ${entity}\n${seedNote}\n## Purpose\n\n${purpose}\n\n## Failure modes\n\n_How it breaks, and the tell-tale symptoms._\n\n## Knobs & commands\n\n_The buttons to push: commands, config, flags, dashboards._\n\n## Incident log\n\n_Dated entries append here (newest last). Each: what happened → cause → fix._\n\n## Current state\n\n_The single freshest sentence about where this entity stands right now._\n`;\n}\n\n/** Read every dossier note into an entry (best-effort; skips unreadable files). */\nexport async function listDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const files = await glob(`${DOSSIER_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: DossierEntry[] = [];\n for (const rel of files) {\n try {\n const raw = await readFile(join(vaultPath, rel), \"utf-8\");\n const { data, content } = matter(raw);\n if (data.type !== \"dossier\") continue;\n const entity = typeof data.entity === \"string\" && data.entity.trim() ? data.entity.trim() : rel.replace(/^dossiers\\//, \"\").replace(/\\.md$/, \"\");\n const aliases = Array.isArray(data.aliases) ? data.aliases.map(String).map((a) => a.trim()).filter(Boolean) : [];\n entries.push({\n entity,\n aliases,\n path: rel,\n purpose: firstParagraph(sectionBody(content, \"Purpose\")),\n current_state: sectionBody(content, \"Current state\"),\n });\n } catch {\n /* skip unreadable dossier */\n }\n }\n return entries.sort((a, b) => a.entity.localeCompare(b.entity));\n}\n\n/** Compile all dossiers into the derived cache the hook reads for two-hop routing. */\nexport async function compileDossiers(vaultPath: string): Promise<DossierEntry[]> {\n const entries = await listDossiers(vaultPath);\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(\n join(dir, CACHE_FILE),\n JSON.stringify({ compiledAt: new Date().toISOString(), dossiers: entries }, null, 2),\n \"utf-8\"\n );\n } catch {\n /* cache write is best-effort */\n }\n return entries;\n}\n\n/** Fast read for the hook: use the cache if present, else compile. */\nexport async function loadDossierCache(vaultPath: string): Promise<DossierEntry[]> {\n const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);\n if (existsSync(cachePath)) {\n try {\n const data = JSON.parse(await readFile(cachePath, \"utf-8\"));\n if (Array.isArray(data?.dossiers)) return data.dossiers as DossierEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileDossiers(vaultPath);\n}\n\n/** Find a dossier by exact entity name or any of its aliases (normalized). */\nexport function findDossier(entries: DossierEntry[], entityOrAlias: string): DossierEntry | undefined {\n const n = normalizeEntity(entityOrAlias);\n return entries.find(\n (d) => normalizeEntity(d.entity) === n || d.aliases.some((a) => normalizeEntity(a) === n)\n );\n}\n\n/**\n * Resolve an utterance to a dossier for two-hop retrieval: the query mentions the\n * entity name or one of its aliases. Returns the best (longest-alias) match so a\n * specific phrase wins over a generic one. Deterministic — no LLM, no embedding.\n */\nexport function resolveDossierForQuery(entries: DossierEntry[], query: string): DossierEntry | undefined {\n const nq = normalizeEntity(query);\n let best: { entry: DossierEntry; keyLen: number } | undefined;\n for (const d of entries) {\n const keys = [d.entity, ...d.aliases].map(normalizeEntity).filter(Boolean);\n for (const k of keys) {\n // Word-ish containment: the key appears in the query.\n if (nq.includes(k) && (!best || k.length > best.keyLen)) {\n best = { entry: d, keyLen: k.length };\n }\n }\n }\n return best?.entry;\n}\n\nasync function readDossierRaw(vaultPath: string, rel: string): Promise<{ raw: string; entity: string } | null> {\n const abs = join(vaultPath, rel);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { data } = matter(raw);\n if (data.type !== \"dossier\") return null;\n const entity = typeof data.entity === \"string\" ? data.entity : rel;\n return { raw, entity };\n } catch {\n return null;\n }\n}\n\n/** Resolve an entity/alias/path argument to a concrete dossier note path. */\nexport async function resolveDossierPath(vaultPath: string, entityOrPath: string): Promise<string | null> {\n // Direct path?\n if (entityOrPath.endsWith(\".md\") && existsSync(join(vaultPath, entityOrPath))) return entityOrPath;\n const entries = await listDossiers(vaultPath);\n const match = findDossier(entries, entityOrPath);\n if (match) return match.path;\n // Fall back to the slug path even if not yet created.\n return null;\n}\n\nexport interface InitDossierResult {\n path: string;\n created: boolean;\n entity: string;\n redacted?: number;\n}\n\n/**\n * Scaffold a dossier for an entity. No-op (created:false) if one already exists for\n * that entity/slug. `seeded_from` records provenance (e.g. a babel-fish project-map\n * entry) without failing when that source is absent — same fail-open posture as the\n * v1.2 code-symbol drift seed.\n */\nexport async function initDossier(\n vaultPath: string,\n entity: string,\n opts: { aliases?: string[]; purpose?: string; seeded_from?: string } = {}\n): Promise<InitDossierResult> {\n const existing = await listDossiers(vaultPath);\n const already = findDossier(existing, entity);\n if (already) return { path: already.path, created: false, entity: already.entity };\n\n const rel = join(DOSSIER_DIR, `${slugifyEntity(entity)}.md`);\n const abs = join(vaultPath, rel);\n if (existsSync(abs)) return { path: rel, created: false, entity };\n\n const today = new Date().toISOString().slice(0, 10);\n const purpose = redact(opts.purpose ?? \"\");\n const fm: Record<string, unknown> = {\n title: entity,\n type: \"dossier\",\n entity,\n status: \"active\",\n last_verified: today,\n confidence: \"medium\",\n ...(opts.aliases && opts.aliases.length > 0 ? { aliases: opts.aliases } : {}),\n ...(opts.seeded_from ? { sources: [opts.seeded_from] } : {}),\n };\n const body = dossierTemplate(entity, { purpose: opts.purpose === undefined ? undefined : purpose.text, seeded_from: opts.seeded_from });\n await mkdir(join(vaultPath, DOSSIER_DIR), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, created: true, entity, ...(purpose.count ? { redacted: purpose.count } : {}) };\n}\n\n/**\n * Append a dated incident line to a dossier's Incident log (accretion, never a new\n * file). Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function appendIncident(\n vaultPath: string,\n entityOrPath: string,\n incident: string,\n opts: { today?: string } = {}\n): Promise<{ path: string; redacted?: number } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const today = opts.today ?? new Date().toISOString().slice(0, 10);\n const text = redact(incident.trim());\n const line = `- ${today} — ${text.text}`;\n const patched = appendToSection(doc.raw, \"Incident log\", line);\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, ...(text.count ? { redacted: text.count } : {}) };\n}\n\n/**\n * Replace the body of a dossier's Current state section (the freshest one-liner).\n * Returns the resolved path, or null if no dossier exists for the entity.\n */\nexport async function setCurrentState(\n vaultPath: string,\n entityOrPath: string,\n state: string\n): Promise<{ path: string; redacted?: number } | null> {\n const rel = await resolveDossierPath(vaultPath, entityOrPath);\n if (!rel) return null;\n const doc = await readDossierRaw(vaultPath, rel);\n if (!doc) return null;\n const text = redact(state.trim());\n const patched = replaceSectionBody(doc.raw, \"Current state\", text.text);\n await writeFile(join(vaultPath, rel), patched, \"utf-8\");\n await compileDossiers(vaultPath);\n return { path: rel, ...(text.count ? { redacted: text.count } : {}) };\n}\n\n// --- section-level markdown editing (heading-scoped, format-preserving) ---\n\n/** Split into lines, find a `## <heading>` (case-insensitive), return its bounds. */\nfunction locateSection(markdown: string, heading: string): { start: number; level: number; end: number; lines: string[] } | null {\n const lines = markdown.split(\"\\n\");\n const want = heading.trim().toLowerCase();\n let start = -1;\n let level = 0;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+(.*?)\\s*$/);\n if (m && m[2].trim().toLowerCase() === want) {\n start = i;\n level = m[1].length;\n break;\n }\n }\n if (start === -1) return null;\n let end = lines.length;\n for (let i = start + 1; i < lines.length; i++) {\n const m = lines[i].match(/^(#{1,6})\\s+/);\n if (m && m[1].length <= level) {\n end = i;\n break;\n }\n }\n return { start, level, end, lines };\n}\n\n/** True if a section body is only the template placeholder (an italic prompt). */\nfunction isPlaceholderBody(body: string): boolean {\n const t = body.trim();\n return t === \"\" || (/^_.*_$/.test(t) && !t.includes(\"\\n\"));\n}\n\n/** Append a line to the end of a section's body (before the next heading/EOF). */\nexport function appendToSection(markdown: string, heading: string, line: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n // Section absent — append a fresh one at EOF.\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${line}\\n`;\n }\n const { lines, start, end } = loc;\n const body = lines.slice(start + 1, end).join(\"\\n\");\n // If the body is just the placeholder prompt, replace it with the first real line.\n const bodyStripped = body.trim();\n const replacePlaceholder = isPlaceholderBody(bodyStripped);\n // Find the last non-empty line index within the section body.\n let insertAt = end;\n while (insertAt - 1 > start && lines[insertAt - 1].trim() === \"\") insertAt--;\n const before = lines.slice(0, replacePlaceholder ? start + 1 : insertAt);\n const after = lines.slice(replacePlaceholder ? end : insertAt);\n const injected = replacePlaceholder ? [\"\", line] : [line];\n return [...before, ...injected, ...after].join(\"\\n\");\n}\n\n/** Replace a section's entire body (keeps the heading), preserving surrounding doc. */\nexport function replaceSectionBody(markdown: string, heading: string, newBody: string): string {\n const loc = locateSection(markdown, heading);\n if (!loc) {\n const sep = markdown.endsWith(\"\\n\") ? \"\" : \"\\n\";\n return `${markdown}${sep}\\n## ${heading}\\n\\n${newBody}\\n`;\n }\n const { lines, start, end } = loc;\n const before = lines.slice(0, start + 1);\n const after = lines.slice(end);\n return [...before, \"\", newBody, \"\", ...after].join(\"\\n\").replace(/\\n{3,}/g, \"\\n\\n\");\n}\n","/**\n * Section-targeted read (v1.4 Phase 7). Extract one heading's section from a\n * markdown document so pulling a dossier's *Knobs* section doesn't cost the whole\n * file. Matches a heading by text (case-insensitive), returns from that heading\n * through to the next heading of the same or higher level.\n */\nexport function extractSection(markdown: string, heading: string): string | null {\n const target = heading.trim().toLowerCase();\n const lines = markdown.split(\"\\n\");\n let startIdx = -1;\n let startLevel = 0;\n\n const headingAt = (line: string): { level: number; text: string } | null => {\n const m = /^(#{1,6})\\s+(.*?)\\s*$/.exec(line);\n return m ? { level: m[1].length, text: m[2].trim().toLowerCase() } : null;\n };\n\n for (let i = 0; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.text === target) {\n startIdx = i;\n startLevel = h.level;\n break;\n }\n }\n if (startIdx === -1) return null;\n\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n const h = headingAt(lines[i]);\n if (h && h.level <= startLevel) {\n endIdx = i;\n break;\n }\n }\n return lines.slice(startIdx, endIdx).join(\"\\n\").trimEnd();\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
redact
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-H2BES6FU.js";
|
|
4
4
|
|
|
5
5
|
// src/core/session.ts
|
|
6
6
|
import { mkdir, readFile, writeFile, rm } from "fs/promises";
|
|
@@ -205,4 +205,4 @@ export {
|
|
|
205
205
|
deriveProjectRoot,
|
|
206
206
|
deriveSessionDir
|
|
207
207
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-EYVFC745.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/core/jev.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var JEV_BAR = 0.95;
|
|
7
|
+
var BRIDGE = ["../../hooks/jev-bridge.py", "../hooks/jev-bridge.py"].map((p) => fileURLToPath(new URL(p, import.meta.url))).find((p) => existsSync(p)) ?? fileURLToPath(new URL("../../hooks/jev-bridge.py", import.meta.url));
|
|
8
|
+
function askJev(input, opts = {}) {
|
|
9
|
+
const bridge = opts.bridge ?? BRIDGE;
|
|
10
|
+
if (!existsSync(bridge)) return Promise.resolve({ scores: {}, error: `bridge missing at ${bridge}` });
|
|
11
|
+
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
12
|
+
return new Promise((done) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const finish = (a) => {
|
|
15
|
+
if (settled) return;
|
|
16
|
+
settled = true;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
done(a);
|
|
19
|
+
};
|
|
20
|
+
const child = spawn("python3", [bridge], { env: opts.env ?? process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
child.kill("SIGKILL");
|
|
23
|
+
finish({ scores: {}, error: `timed out after ${timeoutMs} ms` });
|
|
24
|
+
}, timeoutMs);
|
|
25
|
+
let stdout = "";
|
|
26
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
27
|
+
child.on("error", (e) => finish({ scores: {}, error: `bridge failed to start: ${e.message}` }));
|
|
28
|
+
child.on("close", () => {
|
|
29
|
+
try {
|
|
30
|
+
const line = stdout.trim().split("\n").pop() || "{}";
|
|
31
|
+
const a = JSON.parse(line);
|
|
32
|
+
finish({ ...a, scores: a.scores && typeof a.scores === "object" ? a.scores : {} });
|
|
33
|
+
} catch {
|
|
34
|
+
finish({ scores: {}, error: "bridge gave no JSON" });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
child.stdin.on("error", () => {
|
|
38
|
+
});
|
|
39
|
+
const live = input.caller === "semantic-memory-hook" ? { timeout_seconds: Math.max(0.2, (timeoutMs - 250) / 1e3) } : {};
|
|
40
|
+
child.stdin.end(JSON.stringify({ ...input, ...live }));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function jevSpendToday(jevDir, now = /* @__PURE__ */ new Date()) {
|
|
44
|
+
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
45
|
+
const out = {};
|
|
46
|
+
const logs = join(jevDir, "logs");
|
|
47
|
+
let files = [];
|
|
48
|
+
try {
|
|
49
|
+
files = readdirSync(logs).filter((f) => f.startsWith("jev-usage-") && f.endsWith(".jsonl"));
|
|
50
|
+
} catch {
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
for (const f of files) {
|
|
54
|
+
for (const line of readFileSync(join(logs, f), "utf8").split("\n")) {
|
|
55
|
+
try {
|
|
56
|
+
const r = JSON.parse(line);
|
|
57
|
+
if (String(r.caller).startsWith("semantic-memory-") && String(r.time).startsWith(day)) {
|
|
58
|
+
out[r.caller] = (out[r.caller] ?? 0) + (r.input_tokens ?? 0) + (r.output_tokens ?? 0);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
var jevVerdict = (p, bar = JEV_BAR) => p === void 0 ? null : p >= bar ? true : p <= 1 - bar + 1e-9 ? false : null;
|
|
67
|
+
var relevanceQuestion = (title, excerpt) => `Does this document explain the cause of this failure, or how to fix or avoid it \u2014 the same specific problem, not just the same tool or topic? Document: ${title} \u2014 ${excerpt.replace(/\s+/g, " ").trim().slice(0, 600)}`;
|
|
68
|
+
var failureState = (command, error) => `Command: ${command}
|
|
69
|
+
Error: ${error}`.slice(0, 2e3);
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
JEV_BAR,
|
|
73
|
+
BRIDGE,
|
|
74
|
+
askJev,
|
|
75
|
+
jevSpendToday,
|
|
76
|
+
jevVerdict,
|
|
77
|
+
relevanceQuestion,
|
|
78
|
+
failureState
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=chunk-GYR7U6KA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/jev.ts"],"sourcesContent":["/**\n * Ask Jev yes/no questions through hooks/jev-bridge.py → jev-loop's `router.ask` (v1.7 jev mode).\n * The bridge redacts (jev-loop's redact.py), caches, keeps the ledger and enforces budgets; this side\n * only spawns it, with a hard timeout. Every failure is `{scores: {}, error}` — callers treat a missing\n * score as \"no\" (the safe default), never as permission.\n */\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport type JevCaller = \"semantic-memory-hook\" | \"semantic-memory-judge\" | \"semantic-memory-skill-loop\";\n\nexport interface JevQuestion {\n key: string;\n instructions: string;\n}\n\nexport interface JevAnswer {\n scores: Record<string, number>;\n tokens?: number;\n error?: string;\n /** The live caller's daily ceiling was reached: nothing was sent. */\n ceiling?: boolean;\n}\n\n/** jev-loop's bar: act on a yes only at or above it, on a no only at or below 1 - it. */\nexport const JEV_BAR = 0.95;\n\n// src/core/jev.ts sits two levels below the package root; built, this code can land in a shared dist/ chunk one level below\nexport const BRIDGE =\n [\"../../hooks/jev-bridge.py\", \"../hooks/jev-bridge.py\"].map((p) => fileURLToPath(new URL(p, import.meta.url))).find((p) => existsSync(p)) ??\n fileURLToPath(new URL(\"../../hooks/jev-bridge.py\", import.meta.url));\n\nexport function askJev(\n input: { cwd: string; caller: JevCaller; state: string; questions: JevQuestion[] },\n opts: { timeoutMs?: number; env?: NodeJS.ProcessEnv; bridge?: string } = {},\n): Promise<JevAnswer> {\n const bridge = opts.bridge ?? BRIDGE;\n if (!existsSync(bridge)) return Promise.resolve({ scores: {}, error: `bridge missing at ${bridge}` });\n const timeoutMs = opts.timeoutMs ?? 60_000;\n return new Promise((done) => {\n let settled = false;\n const finish = (a: JevAnswer) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n done(a);\n };\n const child = spawn(\"python3\", [bridge], { env: opts.env ?? process.env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n finish({ scores: {}, error: `timed out after ${timeoutMs} ms` });\n }, timeoutMs);\n let stdout = \"\";\n child.stdout.on(\"data\", (d) => (stdout += d));\n child.on(\"error\", (e) => finish({ scores: {}, error: `bridge failed to start: ${e.message}` }));\n child.on(\"close\", () => {\n try {\n const line = stdout.trim().split(\"\\n\").pop() || \"{}\";\n const a = JSON.parse(line) as JevAnswer;\n finish({ ...a, scores: a.scores && typeof a.scores === \"object\" ? a.scores : {} });\n } catch {\n finish({ scores: {}, error: \"bridge gave no JSON\" });\n }\n });\n child.stdin.on(\"error\", () => {});\n // live: the HTTP call must end inside the hook's budget (python start-up and import take ~150 ms)\n const live = input.caller === \"semantic-memory-hook\" ? { timeout_seconds: Math.max(0.2, (timeoutMs - 250) / 1000) } : {};\n child.stdin.end(JSON.stringify({ ...input, ...live }));\n });\n}\n\n/** Tokens each semantic-memory caller spent with Jev today, from jev-loop's usage ledger (numbers only). */\nexport function jevSpendToday(jevDir: string, now = new Date()): Record<string, number> {\n const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}-${String(now.getDate()).padStart(2, \"0\")}`;\n const out: Record<string, number> = {};\n const logs = join(jevDir, \"logs\");\n let files: string[] = [];\n try {\n files = readdirSync(logs).filter((f) => f.startsWith(\"jev-usage-\") && f.endsWith(\".jsonl\"));\n } catch {\n return out;\n }\n for (const f of files) {\n for (const line of readFileSync(join(logs, f), \"utf8\").split(\"\\n\")) {\n try {\n const r = JSON.parse(line);\n if (String(r.caller).startsWith(\"semantic-memory-\") && String(r.time).startsWith(day)) {\n out[r.caller] = (out[r.caller] ?? 0) + (r.input_tokens ?? 0) + (r.output_tokens ?? 0);\n }\n } catch {}\n }\n }\n return out;\n}\n\n/** A Jev probability as a verdict: yes at or above the bar, no at or below 1 − bar, otherwise unsure (null). */\nexport const jevVerdict = (p: number | undefined, bar = JEV_BAR): boolean | null =>\n p === undefined ? null : p >= bar ? true : p <= 1 - bar + 1e-9 ? false : null;\n\n/** The failure → document relevance question (judge, gate and the failure hook ask the same thing). */\nexport const relevanceQuestion = (title: string, excerpt: string) =>\n `Does this document explain the cause of this failure, or how to fix or avoid it — the same specific problem, ` +\n `not just the same tool or topic? Document: ${title} — ${excerpt.replace(/\\s+/g, \" \").trim().slice(0, 600)}`;\n\n/** The state Jev sees for a failed tool call: the command and its error, capped. */\nexport const failureState = (command: string, error: string) => `Command: ${command}\\nError: ${error}`.slice(0, 2000);\n"],"mappings":"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","names":[]}
|
|
@@ -155,9 +155,17 @@ function redactDeep(value, mode = "broad") {
|
|
|
155
155
|
return r.text;
|
|
156
156
|
}
|
|
157
157
|
if (Array.isArray(v)) return v.map(walk);
|
|
158
|
-
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k,
|
|
158
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walkField(k, x)]));
|
|
159
159
|
return v;
|
|
160
160
|
};
|
|
161
|
+
const walkField = (k, x) => {
|
|
162
|
+
const prefix = `${k}=`;
|
|
163
|
+
if (typeof x !== "string") return walk(x);
|
|
164
|
+
const r = redact(prefix + x, mode);
|
|
165
|
+
if (!r.text.startsWith(prefix)) return walk(x);
|
|
166
|
+
count += r.count;
|
|
167
|
+
return r.text.slice(prefix.length);
|
|
168
|
+
};
|
|
161
169
|
return { value: walk(value), count };
|
|
162
170
|
}
|
|
163
171
|
function redactedNote(count) {
|
|
@@ -172,4 +180,4 @@ export {
|
|
|
172
180
|
redactDeep,
|
|
173
181
|
redactedNote
|
|
174
182
|
};
|
|
175
|
-
//# sourceMappingURL=chunk-
|
|
183
|
+
//# sourceMappingURL=chunk-H2BES6FU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/redact.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Secret redaction for everything semantic-memory writes to disk. Ported from jev-loop's\n * redact.py: (1) exact values of secret-named env vars on this machine, (2) patterns.\n *\n * Two pattern sets:\n * - \"precise\" (notes, frontmatter, dossiers, profile, lexicon): only shapes that are almost\n * never ordinary content, because a false positive in a note is data loss.\n * - \"broad\" (search log, log.md events, session state, capture-pending): precise plus long\n * base64 / letter-and-digit tokens; over-redaction is fine in logs.\n *\n * Cannot catch a secret written in plain prose (\"the password is hunter2\"). Never logs values.\n */\n\nexport const REDACTED = \"[REDACTED]\";\nexport type RedactMode = \"precise\" | \"broad\";\nexport interface RedactResult {\n text: string;\n count: number;\n}\n\nconst SECRET_ENV_NAME = /key|token|secret|passw|pwd|credential/i;\n// The secret word must start a name segment: api_key, DB_PASSWORD, client-secret, apikey; not monkey, tokens, tokenizer\nconst SECRET_WORD = String.raw`(?:api[_-]?)?(?:key|token|secret|password|passwd|pwd|credentials?)`;\n// prefix bounded so a long dotted run (a.b.c…) stays linear\nconst SECRET_NAME = String.raw`\\b(?:[\\w.]{0,64}[_.-])?${SECRET_WORD}`;\n// Value characters: stops at whitespace, quotes, separators and markdown punctuation\nconst VALUE = String.raw`[^\\s\"'\\`,;&|()<>\\[\\]{}]`;\n\n// Obvious placeholders in how-to notes are left alone: <your-key>, ${API_KEY}, process.env.X, changeme, xxxx, ****\nconst PLACEHOLDER =\n /^(?:<[^>]*>|\\$\\{?\\w+\\}?|(?:process\\.env|os\\.environ|env)\\b.*|\\[REDACTED\\]|(.)\\1{3,}|\\*+|x{3,}.*|password|passwd|pass|secret|token|pwd)$|example|changeme|placeholder|your[-_]|dummy|redacted/i;\nconst isPlaceholder = (value: string) => PLACEHOLDER.test(value) || /\\.\\.\\.|…|x{4}/i.test(value);\n// Precise mode: a value counts only if it looks random — contains a digit, or is a long run of\n// letters (a passphrase). Code in notes (`api_key = os.getenv`, `token: settings.get_decrypted_key`,\n// `password=self._key`) has dots/underscores and no digits, and must survive.\nconst looksRandom = (value: string) => /\\d/.test(value) || /^[A-Za-z]{16,}$/.test(value);\n\n// Vendor key prefixes. Word-shaped bodies must contain a digit, so identifiers like\n// pk_customer_orders or npm_package_version stay; real keys are random and always have digits.\nconst withDigit = (prefix: string, body: string, min: number) => String.raw`${prefix}(?=${body}*\\d)${body}{${min},}`;\nconst VENDOR = new RegExp(\n String.raw`\\b(?:` +\n [\n withDigit(String.raw`(?:sk|rk|pk)[-_]`, String.raw`[\\w-]`, 8),\n withDigit(\"whsec_\", String.raw`\\w`, 8),\n withDigit(\"gh[pousr]_\", String.raw`\\w`, 20),\n withDigit(\"github_pat_\", String.raw`\\w`, 20),\n withDigit(\"glpat-\", String.raw`[\\w-]`, 16),\n withDigit(\"xox[abprs]-\", String.raw`[\\w-]`, 10),\n withDigit(\"re_\", String.raw`\\w`, 16),\n withDigit(\"hf_\", String.raw`\\w`, 20),\n withDigit(\"npm_\", String.raw`\\w`, 20),\n withDigit(\"GOCSPX-\", String.raw`[\\w-]`, 20), // Google OAuth client secret\n withDigit(String.raw`hvs\\.`, String.raw`[\\w-]`, 20), // HashiCorp Vault service token\n String.raw`(?:AKIA|ASIA)[0-9A-Z]{16}\\b`,\n String.raw`AIza[\\w-]{30,}`,\n ].join(\"|\") +\n \")\",\n \"g\"\n);\n\ninterface Rule {\n name: string; // reported by lint_vault's secrets check\n re: RegExp;\n replace: (match: string, ...groups: string[]) => string | null; // null = leave as is\n}\n\nfunction rules(mode: RedactMode): Rule[] {\n const minValue = mode === \"precise\" ? 8 : 4;\n const isSecretValue = (value: string) => !isPlaceholder(value) && (mode === \"broad\" || looksRandom(value));\n const precise: Rule[] = [\n // PEM private keys, even when the END line was cut off\n {\n name: \"private key\",\n re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,\n replace: () => REDACTED,\n },\n // credentials inside URLs: scheme://user:pass@host\n {\n name: \"URL credentials\",\n re: /(:\\/\\/)([^/\\s:@]+):([^/\\s@]+)@/g,\n replace: (_m, scheme, user, pass) => (!isSecretValue(pass) || pass === user ? null : `${scheme}${REDACTED}@`),\n },\n // Authorization headers\n {\n name: \"authorization header\",\n re: /\\b(bearer|basic)(\\s+)(?=[A-Za-z0-9._~+/=-]*\\d)([A-Za-z0-9._~+/=-]{16,})/gi,\n replace: (_m, scheme, space) => `${scheme}${space}${REDACTED}`,\n },\n // name=value, name: value, \"name\": \"value\" where the name is a secret word\n {\n name: \"secret-named value\",\n re: new RegExp(String.raw`(${SECRET_NAME}[\"']?\\s*[:=]\\s*[\"']?)(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, prefix, value) => (isSecretValue(value) ? `${prefix}${REDACTED}` : null),\n },\n // command-line flags: --token VALUE, --api-key=VALUE\n {\n name: \"secret flag\",\n // a flag starts after whitespace/start, and its name is bounded: keeps long dash/hyphen runs linear\n re: new RegExp(String.raw`((?<![\\w-])--?[\\w-]{0,64}?${SECRET_WORD}[ =])(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, flag, value) => (isSecretValue(value) ? `${flag}${REDACTED}` : null),\n },\n { name: \"vendor API key\", re: VENDOR, replace: (m) => (isPlaceholder(m) ? null : REDACTED) },\n // JWTs\n { name: \"JWT\", re: /\\beyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,}/g, replace: () => REDACTED },\n ];\n if (mode === \"precise\") return precise;\n return [\n ...precise,\n // base64 blobs with mixed case and digits (the lookbehind starts a try only at a run's start: linear on huge blobs)\n {\n name: \"base64 blob\",\n re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*\\d)(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])[A-Za-z0-9+/]{40,}={0,2}/g,\n replace: () => REDACTED,\n },\n // any other long token mixing letters and digits (hashes, ids: over-redaction is fine in logs)\n { name: \"long token\", re: /(?<![\\w-])(?=[\\w-]*\\d)(?=[\\w-]*[A-Za-z])[\\w-]{32,}(?![\\w-])/g, replace: () => REDACTED },\n ];\n}\n\nlet knownSecrets: string[] | null = null;\n\n/** Exact values of secret-named variables on this machine (env + ~/.claude/settings.json env), longest first. */\nfunction loadKnownSecrets(): string[] {\n const values = new Set<string>();\n for (const [k, v] of Object.entries(process.env)) if (v && SECRET_ENV_NAME.test(k)) values.add(v);\n try {\n const env = JSON.parse(readFileSync(join(homedir(), \".claude\", \"settings.json\"), \"utf8\"))?.env ?? {};\n for (const [k, v] of Object.entries(env)) if (SECRET_ENV_NAME.test(k)) values.add(String(v));\n } catch {\n // no settings file: env vars only\n }\n // Values written into notes must look like secrets: long, no whitespace, not a path or URL\n return [...values]\n .filter((v) => v.length >= 12 && !/\\s/.test(v) && !v.startsWith(\"/\") && !v.includes(\"://\") && !isPlaceholder(v))\n .sort((a, b) => b.length - a.length);\n}\n\n/** Test seam: re-read env and settings on the next call. */\nexport function resetRedactCache(): void {\n knownSecrets = null;\n}\n\nexport function redact(text: string, mode: RedactMode = \"precise\"): RedactResult {\n if (!text) return { text, count: 0 };\n knownSecrets ??= loadKnownSecrets();\n let count = 0;\n let out = text;\n for (const value of knownSecrets) {\n const parts = out.split(value);\n if (parts.length > 1) {\n count += parts.length - 1;\n out = parts.join(REDACTED);\n }\n }\n for (const rule of rules(mode)) {\n out = out.replace(rule.re, (match: string, ...args: unknown[]) => {\n const groups = args.slice(0, -2).map((g) => (typeof g === \"string\" ? g : \"\"));\n const replaced = rule.replace(match, ...groups);\n if (replaced === null || replaced === match) return match;\n count++;\n return replaced;\n });\n }\n return { text: out, count };\n}\n\nexport interface SecretLocation {\n line: number;\n kind: string;\n}\n\n/** Where precise-set secrets sit in a text (for lint_vault): 1-based line and kind, never the value. */\nexport function findSecrets(text: string): SecretLocation[] {\n knownSecrets ??= loadKnownSecrets();\n const hits: { index: number; kind: string }[] = [];\n for (const value of knownSecrets) {\n for (let i = text.indexOf(value); i !== -1; i = text.indexOf(value, i + value.length)) {\n hits.push({ index: i, kind: \"secret env value\" });\n }\n }\n for (const rule of rules(\"precise\")) {\n for (const m of text.matchAll(rule.re)) {\n const replaced = rule.replace(m[0], ...m.slice(1).map((g) => g ?? \"\"));\n if (replaced !== null && replaced !== m[0]) hits.push({ index: m.index ?? 0, kind: rule.name });\n }\n }\n const seen = new Set<string>();\n const out: SecretLocation[] = [];\n for (const h of hits.sort((a, b) => a.index - b.index)) {\n const line = text.slice(0, h.index).split(\"\\n\").length;\n const key = `${line}:${h.kind}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push({ line, kind: h.kind });\n }\n }\n return out;\n}\n\n/** Redact every string inside a JSON-like value (for log payloads and state files). */\nexport function redactDeep<T>(value: T, mode: RedactMode = \"broad\"): { value: T; count: number } {\n let count = 0;\n const walk = (v: unknown): unknown => {\n if (typeof v === \"string\") {\n const r = redact(v, mode);\n count += r.count;\n return r.text;\n }\n if (Array.isArray(v)) return v.map(walk);\n if (v && typeof v === \"object\") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));\n return v;\n };\n return { value: walk(value) as T, count };\n}\n\n/** Suffix for a tool result: says how many secrets were masked, never which. \"\" when none. */\nexport function redactedNote(count: number): string {\n return count > 0 ? ` (${count} secret${count === 1 ? \"\" : \"s\"} redacted)` : \"\";\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/redact.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/**\n * Secret redaction for everything semantic-memory writes to disk. Ported from jev-loop's\n * redact.py: (1) exact values of secret-named env vars on this machine, (2) patterns.\n *\n * Two pattern sets:\n * - \"precise\" (notes, frontmatter, dossiers, profile, lexicon): only shapes that are almost\n * never ordinary content, because a false positive in a note is data loss.\n * - \"broad\" (search log, log.md events, session state, capture-pending): precise plus long\n * base64 / letter-and-digit tokens; over-redaction is fine in logs.\n *\n * Cannot catch a secret written in plain prose (\"the password is hunter2\"). Never logs values.\n */\n\nexport const REDACTED = \"[REDACTED]\";\nexport type RedactMode = \"precise\" | \"broad\";\nexport interface RedactResult {\n text: string;\n count: number;\n}\n\nconst SECRET_ENV_NAME = /key|token|secret|passw|pwd|credential/i;\n// The secret word must start a name segment: api_key, DB_PASSWORD, client-secret, apikey; not monkey, tokens, tokenizer\nconst SECRET_WORD = String.raw`(?:api[_-]?)?(?:key|token|secret|password|passwd|pwd|credentials?)`;\n// prefix bounded so a long dotted run (a.b.c…) stays linear\nconst SECRET_NAME = String.raw`\\b(?:[\\w.]{0,64}[_.-])?${SECRET_WORD}`;\n// Value characters: stops at whitespace, quotes, separators and markdown punctuation\nconst VALUE = String.raw`[^\\s\"'\\`,;&|()<>\\[\\]{}]`;\n\n// Obvious placeholders in how-to notes are left alone: <your-key>, ${API_KEY}, process.env.X, changeme, xxxx, ****\nconst PLACEHOLDER =\n /^(?:<[^>]*>|\\$\\{?\\w+\\}?|(?:process\\.env|os\\.environ|env)\\b.*|\\[REDACTED\\]|(.)\\1{3,}|\\*+|x{3,}.*|password|passwd|pass|secret|token|pwd)$|example|changeme|placeholder|your[-_]|dummy|redacted/i;\nconst isPlaceholder = (value: string) => PLACEHOLDER.test(value) || /\\.\\.\\.|…|x{4}/i.test(value);\n// Precise mode: a value counts only if it looks random — contains a digit, or is a long run of\n// letters (a passphrase). Code in notes (`api_key = os.getenv`, `token: settings.get_decrypted_key`,\n// `password=self._key`) has dots/underscores and no digits, and must survive.\nconst looksRandom = (value: string) => /\\d/.test(value) || /^[A-Za-z]{16,}$/.test(value);\n\n// Vendor key prefixes. Word-shaped bodies must contain a digit, so identifiers like\n// pk_customer_orders or npm_package_version stay; real keys are random and always have digits.\nconst withDigit = (prefix: string, body: string, min: number) => String.raw`${prefix}(?=${body}*\\d)${body}{${min},}`;\nconst VENDOR = new RegExp(\n String.raw`\\b(?:` +\n [\n withDigit(String.raw`(?:sk|rk|pk)[-_]`, String.raw`[\\w-]`, 8),\n withDigit(\"whsec_\", String.raw`\\w`, 8),\n withDigit(\"gh[pousr]_\", String.raw`\\w`, 20),\n withDigit(\"github_pat_\", String.raw`\\w`, 20),\n withDigit(\"glpat-\", String.raw`[\\w-]`, 16),\n withDigit(\"xox[abprs]-\", String.raw`[\\w-]`, 10),\n withDigit(\"re_\", String.raw`\\w`, 16),\n withDigit(\"hf_\", String.raw`\\w`, 20),\n withDigit(\"npm_\", String.raw`\\w`, 20),\n withDigit(\"GOCSPX-\", String.raw`[\\w-]`, 20), // Google OAuth client secret\n withDigit(String.raw`hvs\\.`, String.raw`[\\w-]`, 20), // HashiCorp Vault service token\n String.raw`(?:AKIA|ASIA)[0-9A-Z]{16}\\b`,\n String.raw`AIza[\\w-]{30,}`,\n ].join(\"|\") +\n \")\",\n \"g\"\n);\n\ninterface Rule {\n name: string; // reported by lint_vault's secrets check\n re: RegExp;\n replace: (match: string, ...groups: string[]) => string | null; // null = leave as is\n}\n\nfunction rules(mode: RedactMode): Rule[] {\n const minValue = mode === \"precise\" ? 8 : 4;\n const isSecretValue = (value: string) => !isPlaceholder(value) && (mode === \"broad\" || looksRandom(value));\n const precise: Rule[] = [\n // PEM private keys, even when the END line was cut off\n {\n name: \"private key\",\n re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g,\n replace: () => REDACTED,\n },\n // credentials inside URLs: scheme://user:pass@host\n {\n name: \"URL credentials\",\n re: /(:\\/\\/)([^/\\s:@]+):([^/\\s@]+)@/g,\n replace: (_m, scheme, user, pass) => (!isSecretValue(pass) || pass === user ? null : `${scheme}${REDACTED}@`),\n },\n // Authorization headers\n {\n name: \"authorization header\",\n re: /\\b(bearer|basic)(\\s+)(?=[A-Za-z0-9._~+/=-]*\\d)([A-Za-z0-9._~+/=-]{16,})/gi,\n replace: (_m, scheme, space) => `${scheme}${space}${REDACTED}`,\n },\n // name=value, name: value, \"name\": \"value\" where the name is a secret word\n {\n name: \"secret-named value\",\n re: new RegExp(String.raw`(${SECRET_NAME}[\"']?\\s*[:=]\\s*[\"']?)(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, prefix, value) => (isSecretValue(value) ? `${prefix}${REDACTED}` : null),\n },\n // command-line flags: --token VALUE, --api-key=VALUE\n {\n name: \"secret flag\",\n // a flag starts after whitespace/start, and its name is bounded: keeps long dash/hyphen runs linear\n re: new RegExp(String.raw`((?<![\\w-])--?[\\w-]{0,64}?${SECRET_WORD}[ =])(${VALUE}{${minValue},})`, \"gi\"),\n replace: (_m, flag, value) => (isSecretValue(value) ? `${flag}${REDACTED}` : null),\n },\n { name: \"vendor API key\", re: VENDOR, replace: (m) => (isPlaceholder(m) ? null : REDACTED) },\n // JWTs\n { name: \"JWT\", re: /\\beyJ[\\w-]{8,}\\.[\\w-]{8,}\\.[\\w-]{8,}/g, replace: () => REDACTED },\n ];\n if (mode === \"precise\") return precise;\n return [\n ...precise,\n // base64 blobs with mixed case and digits (the lookbehind starts a try only at a run's start: linear on huge blobs)\n {\n name: \"base64 blob\",\n re: /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*\\d)(?=[A-Za-z0-9+/]*[A-Z])(?=[A-Za-z0-9+/]*[a-z])[A-Za-z0-9+/]{40,}={0,2}/g,\n replace: () => REDACTED,\n },\n // any other long token mixing letters and digits (hashes, ids: over-redaction is fine in logs)\n { name: \"long token\", re: /(?<![\\w-])(?=[\\w-]*\\d)(?=[\\w-]*[A-Za-z])[\\w-]{32,}(?![\\w-])/g, replace: () => REDACTED },\n ];\n}\n\nlet knownSecrets: string[] | null = null;\n\n/** Exact values of secret-named variables on this machine (env + ~/.claude/settings.json env), longest first. */\nfunction loadKnownSecrets(): string[] {\n const values = new Set<string>();\n for (const [k, v] of Object.entries(process.env)) if (v && SECRET_ENV_NAME.test(k)) values.add(v);\n try {\n const env = JSON.parse(readFileSync(join(homedir(), \".claude\", \"settings.json\"), \"utf8\"))?.env ?? {};\n for (const [k, v] of Object.entries(env)) if (SECRET_ENV_NAME.test(k)) values.add(String(v));\n } catch {\n // no settings file: env vars only\n }\n // Values written into notes must look like secrets: long, no whitespace, not a path or URL\n return [...values]\n .filter((v) => v.length >= 12 && !/\\s/.test(v) && !v.startsWith(\"/\") && !v.includes(\"://\") && !isPlaceholder(v))\n .sort((a, b) => b.length - a.length);\n}\n\n/** Test seam: re-read env and settings on the next call. */\nexport function resetRedactCache(): void {\n knownSecrets = null;\n}\n\nexport function redact(text: string, mode: RedactMode = \"precise\"): RedactResult {\n if (!text) return { text, count: 0 };\n knownSecrets ??= loadKnownSecrets();\n let count = 0;\n let out = text;\n for (const value of knownSecrets) {\n const parts = out.split(value);\n if (parts.length > 1) {\n count += parts.length - 1;\n out = parts.join(REDACTED);\n }\n }\n for (const rule of rules(mode)) {\n out = out.replace(rule.re, (match: string, ...args: unknown[]) => {\n const groups = args.slice(0, -2).map((g) => (typeof g === \"string\" ? g : \"\"));\n const replaced = rule.replace(match, ...groups);\n if (replaced === null || replaced === match) return match;\n count++;\n return replaced;\n });\n }\n return { text: out, count };\n}\n\nexport interface SecretLocation {\n line: number;\n kind: string;\n}\n\n/** Where precise-set secrets sit in a text (for lint_vault): 1-based line and kind, never the value. */\nexport function findSecrets(text: string): SecretLocation[] {\n knownSecrets ??= loadKnownSecrets();\n const hits: { index: number; kind: string }[] = [];\n for (const value of knownSecrets) {\n for (let i = text.indexOf(value); i !== -1; i = text.indexOf(value, i + value.length)) {\n hits.push({ index: i, kind: \"secret env value\" });\n }\n }\n for (const rule of rules(\"precise\")) {\n for (const m of text.matchAll(rule.re)) {\n const replaced = rule.replace(m[0], ...m.slice(1).map((g) => g ?? \"\"));\n if (replaced !== null && replaced !== m[0]) hits.push({ index: m.index ?? 0, kind: rule.name });\n }\n }\n const seen = new Set<string>();\n const out: SecretLocation[] = [];\n for (const h of hits.sort((a, b) => a.index - b.index)) {\n const line = text.slice(0, h.index).split(\"\\n\").length;\n const key = `${line}:${h.kind}`;\n if (!seen.has(key)) {\n seen.add(key);\n out.push({ line, kind: h.kind });\n }\n }\n return out;\n}\n\n/** Redact every string inside a JSON-like value (for log payloads and state files). */\nexport function redactDeep<T>(value: T, mode: RedactMode = \"broad\"): { value: T; count: number } {\n let count = 0;\n const walk = (v: unknown): unknown => {\n if (typeof v === \"string\") {\n const r = redact(v, mode);\n count += r.count;\n return r.text;\n }\n if (Array.isArray(v)) return v.map(walk);\n if (v && typeof v === \"object\") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walkField(k, x)]));\n return v;\n };\n // a bare value under a secret-named key (frontmatter `api_key: ...`) needs the key as its context\n const walkField = (k: string, x: unknown): unknown => {\n const prefix = `${k}=`;\n if (typeof x !== \"string\") return walk(x);\n const r = redact(prefix + x, mode);\n if (!r.text.startsWith(prefix)) return walk(x);\n count += r.count;\n return r.text.slice(prefix.length);\n };\n return { value: walk(value) as T, count };\n}\n\n/** Suffix for a tool result: says how many secrets were masked, never which. \"\" when none. */\nexport function redactedNote(count: number): string {\n return count > 0 ? ` (${count} secret${count === 1 ? \"\" : \"s\"} redacted)` : \"\";\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,YAAY;AAed,IAAM,WAAW;AAOxB,IAAM,kBAAkB;AAExB,IAAM,cAAc,OAAO;AAE3B,IAAM,cAAc,OAAO,6BAA6B,WAAW;AAEnE,IAAM,QAAQ,OAAO;AAGrB,IAAM,cACJ;AACF,IAAM,gBAAgB,CAAC,UAAkB,YAAY,KAAK,KAAK,KAAK,iBAAiB,KAAK,KAAK;AAI/F,IAAM,cAAc,CAAC,UAAkB,KAAK,KAAK,KAAK,KAAK,kBAAkB,KAAK,KAAK;AAIvF,IAAM,YAAY,CAAC,QAAgB,MAAc,QAAgB,OAAO,MAAM,MAAM,MAAM,IAAI,OAAO,IAAI,IAAI,GAAG;AAChH,IAAM,SAAS,IAAI;AAAA,EACjB,OAAO,aACL;AAAA,IACE,UAAU,OAAO,uBAAuB,OAAO,YAAY,CAAC;AAAA,IAC5D,UAAU,UAAU,OAAO,SAAS,CAAC;AAAA,IACrC,UAAU,cAAc,OAAO,SAAS,EAAE;AAAA,IAC1C,UAAU,eAAe,OAAO,SAAS,EAAE;AAAA,IAC3C,UAAU,UAAU,OAAO,YAAY,EAAE;AAAA,IACzC,UAAU,eAAe,OAAO,YAAY,EAAE;AAAA,IAC9C,UAAU,OAAO,OAAO,SAAS,EAAE;AAAA,IACnC,UAAU,OAAO,OAAO,SAAS,EAAE;AAAA,IACnC,UAAU,QAAQ,OAAO,SAAS,EAAE;AAAA,IACpC,UAAU,WAAW,OAAO,YAAY,EAAE;AAAA;AAAA,IAC1C,UAAU,OAAO,YAAY,OAAO,YAAY,EAAE;AAAA;AAAA,IAClD,OAAO;AAAA,IACP,OAAO;AAAA,EACT,EAAE,KAAK,GAAG,IACV;AAAA,EACF;AACF;AAQA,SAAS,MAAM,MAA0B;AACvC,QAAM,WAAW,SAAS,YAAY,IAAI;AAC1C,QAAM,gBAAgB,CAAC,UAAkB,CAAC,cAAc,KAAK,MAAM,SAAS,WAAW,YAAY,KAAK;AACxG,QAAM,UAAkB;AAAA;AAAA,IAEtB;AAAA,MACE,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,MAAM;AAAA,IACjB;AAAA;AAAA,IAEA;AAAA,MACE,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,CAAC,IAAI,QAAQ,MAAM,SAAU,CAAC,cAAc,IAAI,KAAK,SAAS,OAAO,OAAO,GAAG,MAAM,GAAG,QAAQ;AAAA,IAC3G;AAAA;AAAA,IAEA;AAAA,MACE,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,CAAC,IAAI,QAAQ,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ;AAAA,IAC9D;AAAA;AAAA,IAEA;AAAA,MACE,MAAM;AAAA,MACN,IAAI,IAAI,OAAO,OAAO,OAAO,WAAW,yBAAyB,KAAK,IAAI,QAAQ,OAAO,IAAI;AAAA,MAC7F,SAAS,CAAC,IAAI,QAAQ,UAAW,cAAc,KAAK,IAAI,GAAG,MAAM,GAAG,QAAQ,KAAK;AAAA,IACnF;AAAA;AAAA,IAEA;AAAA,MACE,MAAM;AAAA;AAAA,MAEN,IAAI,IAAI,OAAO,OAAO,gCAAgC,WAAW,SAAS,KAAK,IAAI,QAAQ,OAAO,IAAI;AAAA,MACtG,SAAS,CAAC,IAAI,MAAM,UAAW,cAAc,KAAK,IAAI,GAAG,IAAI,GAAG,QAAQ,KAAK;AAAA,IAC/E;AAAA,IACA,EAAE,MAAM,kBAAkB,IAAI,QAAQ,SAAS,CAAC,MAAO,cAAc,CAAC,IAAI,OAAO,SAAU;AAAA;AAAA,IAE3F,EAAE,MAAM,OAAO,IAAI,yCAAyC,SAAS,MAAM,SAAS;AAAA,EACtF;AACA,MAAI,SAAS,UAAW,QAAO;AAC/B,SAAO;AAAA,IACL,GAAG;AAAA;AAAA,IAEH;AAAA,MACE,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,SAAS,MAAM;AAAA,IACjB;AAAA;AAAA,IAEA,EAAE,MAAM,cAAc,IAAI,gEAAgE,SAAS,MAAM,SAAS;AAAA,EACpH;AACF;AAEA,IAAI,eAAgC;AAGpC,SAAS,mBAA6B;AACpC,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG,EAAG,KAAI,KAAK,gBAAgB,KAAK,CAAC,EAAG,QAAO,IAAI,CAAC;AAChG,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,aAAa,KAAK,QAAQ,GAAG,WAAW,eAAe,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC;AACnG,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,EAAG,KAAI,gBAAgB,KAAK,CAAC,EAAG,QAAO,IAAI,OAAO,CAAC,CAAC;AAAA,EAC7F,QAAQ;AAAA,EAER;AAEA,SAAO,CAAC,GAAG,MAAM,EACd,OAAO,CAAC,MAAM,EAAE,UAAU,MAAM,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,EAAE,WAAW,GAAG,KAAK,CAAC,EAAE,SAAS,KAAK,KAAK,CAAC,cAAc,CAAC,CAAC,EAC9G,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;AACvC;AAGO,SAAS,mBAAyB;AACvC,iBAAe;AACjB;AAEO,SAAS,OAAO,MAAc,OAAmB,WAAyB;AAC/E,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,OAAO,EAAE;AACnC,mBAAiB,iBAAiB;AAClC,MAAI,QAAQ;AACZ,MAAI,MAAM;AACV,aAAW,SAAS,cAAc;AAChC,UAAM,QAAQ,IAAI,MAAM,KAAK;AAC7B,QAAI,MAAM,SAAS,GAAG;AACpB,eAAS,MAAM,SAAS;AACxB,YAAM,MAAM,KAAK,QAAQ;AAAA,IAC3B;AAAA,EACF;AACA,aAAW,QAAQ,MAAM,IAAI,GAAG;AAC9B,UAAM,IAAI,QAAQ,KAAK,IAAI,CAAC,UAAkB,SAAoB;AAChE,YAAM,SAAS,KAAK,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,MAAO,OAAO,MAAM,WAAW,IAAI,EAAG;AAC5E,YAAM,WAAW,KAAK,QAAQ,OAAO,GAAG,MAAM;AAC9C,UAAI,aAAa,QAAQ,aAAa,MAAO,QAAO;AACpD;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACA,SAAO,EAAE,MAAM,KAAK,MAAM;AAC5B;AAQO,SAAS,YAAY,MAAgC;AAC1D,mBAAiB,iBAAiB;AAClC,QAAM,OAA0C,CAAC;AACjD,aAAW,SAAS,cAAc;AAChC,aAAS,IAAI,KAAK,QAAQ,KAAK,GAAG,MAAM,IAAI,IAAI,KAAK,QAAQ,OAAO,IAAI,MAAM,MAAM,GAAG;AACrF,WAAK,KAAK,EAAE,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAAA,IAClD;AAAA,EACF;AACA,aAAW,QAAQ,MAAM,SAAS,GAAG;AACnC,eAAW,KAAK,KAAK,SAAS,KAAK,EAAE,GAAG;AACtC,YAAM,WAAW,KAAK,QAAQ,EAAE,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC;AACrE,UAAI,aAAa,QAAQ,aAAa,EAAE,CAAC,EAAG,MAAK,KAAK,EAAE,OAAO,EAAE,SAAS,GAAG,MAAM,KAAK,KAAK,CAAC;AAAA,IAChG;AAAA,EACF;AACA,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,MAAwB,CAAC;AAC/B,aAAW,KAAK,KAAK,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,GAAG;AACtD,UAAM,OAAO,KAAK,MAAM,GAAG,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE;AAChD,UAAM,MAAM,GAAG,IAAI,IAAI,EAAE,IAAI;AAC7B,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAClB,WAAK,IAAI,GAAG;AACZ,UAAI,KAAK,EAAE,MAAM,MAAM,EAAE,KAAK,CAAC;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,WAAc,OAAU,OAAmB,SAAsC;AAC/F,MAAI,QAAQ;AACZ,QAAM,OAAO,CAAC,MAAwB;AACpC,QAAI,OAAO,MAAM,UAAU;AACzB,YAAM,IAAI,OAAO,GAAG,IAAI;AACxB,eAAS,EAAE;AACX,aAAO,EAAE;AAAA,IACX;AACA,QAAI,MAAM,QAAQ,CAAC,EAAG,QAAO,EAAE,IAAI,IAAI;AACvC,QAAI,KAAK,OAAO,MAAM,SAAU,QAAO,OAAO,YAAY,OAAO,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC;AACjH,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,CAAC,GAAW,MAAwB;AACpD,UAAM,SAAS,GAAG,CAAC;AACnB,QAAI,OAAO,MAAM,SAAU,QAAO,KAAK,CAAC;AACxC,UAAM,IAAI,OAAO,SAAS,GAAG,IAAI;AACjC,QAAI,CAAC,EAAE,KAAK,WAAW,MAAM,EAAG,QAAO,KAAK,CAAC;AAC7C,aAAS,EAAE;AACX,WAAO,EAAE,KAAK,MAAM,OAAO,MAAM;AAAA,EACnC;AACA,SAAO,EAAE,OAAO,KAAK,KAAK,GAAQ,MAAM;AAC1C;AAGO,SAAS,aAAa,OAAuB;AAClD,SAAO,QAAQ,IAAI,KAAK,KAAK,UAAU,UAAU,IAAI,KAAK,GAAG,eAAe;AAC9E;","names":[]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/core/claude-run.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
var MAX_CLAUDE = 5;
|
|
7
|
+
function claudeConcurrency(env = process.env) {
|
|
8
|
+
const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);
|
|
9
|
+
return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;
|
|
10
|
+
}
|
|
11
|
+
var slotsDir = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), ".semantic-memory", "claude-slots");
|
|
12
|
+
var alive = (pid) => {
|
|
13
|
+
try {
|
|
14
|
+
process.kill(pid, 0);
|
|
15
|
+
return true;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return e.code === "EPERM";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
async function acquireSlot(opts = {}) {
|
|
21
|
+
const env = opts.env ?? process.env;
|
|
22
|
+
const dir = slotsDir(env);
|
|
23
|
+
const max = claudeConcurrency(env);
|
|
24
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 6e4);
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
for (; ; ) {
|
|
27
|
+
for (let i = 0; i < max; i++) {
|
|
28
|
+
const path = join(dir, `slot-${i}`);
|
|
29
|
+
try {
|
|
30
|
+
const fd = openSync(path, "wx");
|
|
31
|
+
writeSync(fd, `${process.pid}
|
|
32
|
+
`);
|
|
33
|
+
closeSync(fd);
|
|
34
|
+
let released = false;
|
|
35
|
+
return () => {
|
|
36
|
+
if (released) return;
|
|
37
|
+
released = true;
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(path);
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e.code !== "EEXIST") throw e;
|
|
45
|
+
try {
|
|
46
|
+
const pid = Number(readFileSync(path, "utf8").trim());
|
|
47
|
+
if (!pid || !alive(pid)) unlinkSync(path);
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 18e5} ms (${max} in use)`);
|
|
53
|
+
await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var claudeBin = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_BIN || "claude";
|
|
57
|
+
var skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || "sonnet";
|
|
58
|
+
function fencedArgs(o, env = process.env) {
|
|
59
|
+
const args = [
|
|
60
|
+
"-p",
|
|
61
|
+
"--tools",
|
|
62
|
+
o.tools.join(","),
|
|
63
|
+
"--disallowedTools",
|
|
64
|
+
"Task,Agent",
|
|
65
|
+
"--strict-mcp-config",
|
|
66
|
+
"--settings",
|
|
67
|
+
JSON.stringify({ disableAllHooks: true }),
|
|
68
|
+
"--setting-sources",
|
|
69
|
+
"project,local",
|
|
70
|
+
"--permission-mode",
|
|
71
|
+
"dontAsk",
|
|
72
|
+
"--no-session-persistence",
|
|
73
|
+
"--max-turns",
|
|
74
|
+
String(o.maxTurns),
|
|
75
|
+
"--model",
|
|
76
|
+
o.model ?? skillLoopModel(env),
|
|
77
|
+
"--output-format",
|
|
78
|
+
o.output ?? "json"
|
|
79
|
+
];
|
|
80
|
+
if (o.allowedTools?.length) args.push("--allowedTools", o.allowedTools.join(","));
|
|
81
|
+
if ((o.output ?? "json") === "stream-json") args.push("--verbose");
|
|
82
|
+
for (const d of o.addDirs ?? []) args.push("--add-dir", d);
|
|
83
|
+
if (o.appendSystemPrompt) args.push("--append-system-prompt", o.appendSystemPrompt);
|
|
84
|
+
return args;
|
|
85
|
+
}
|
|
86
|
+
async function runClaude(o) {
|
|
87
|
+
const env = { ...o.env ?? process.env, SEMANTIC_MEMORY_REVIEWER: "1" };
|
|
88
|
+
const fail = (error) => ({ ok: false, result: "", events: [], costUsd: 0, turns: 0, error });
|
|
89
|
+
let release;
|
|
90
|
+
try {
|
|
91
|
+
release = await acquireSlot({ env });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return fail(e.message);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return await new Promise((done) => {
|
|
97
|
+
const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
98
|
+
let stdout = "";
|
|
99
|
+
let stderr = "";
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
child.kill("SIGKILL");
|
|
102
|
+
done(fail(`timed out after ${o.timeoutMs ?? 6e5} ms`));
|
|
103
|
+
}, o.timeoutMs ?? 6e5);
|
|
104
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
105
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
106
|
+
child.on("error", (e) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
done(fail(`could not start ${claudeBin(env)}: ${e.message}`));
|
|
109
|
+
});
|
|
110
|
+
child.on("close", (code) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
done(parseClaudeOutput(stdout, o.output ?? "json", code, stderr));
|
|
113
|
+
});
|
|
114
|
+
child.stdin.on("error", () => {
|
|
115
|
+
});
|
|
116
|
+
child.stdin.end(o.prompt);
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
release();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function parseClaudeOutput(stdout, output, code, stderr = "") {
|
|
123
|
+
const events = [];
|
|
124
|
+
let final = null;
|
|
125
|
+
if (output === "json") {
|
|
126
|
+
try {
|
|
127
|
+
final = JSON.parse(stdout.trim());
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
for (const line of stdout.split("\n")) {
|
|
132
|
+
if (!line.trim()) continue;
|
|
133
|
+
try {
|
|
134
|
+
const e = JSON.parse(line);
|
|
135
|
+
events.push(e);
|
|
136
|
+
if (e.type === "result") final = e;
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!final) return { ok: false, result: "", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };
|
|
142
|
+
return {
|
|
143
|
+
ok: !final.is_error && final.subtype !== "error_during_execution",
|
|
144
|
+
result: typeof final.result === "string" ? final.result : "",
|
|
145
|
+
events,
|
|
146
|
+
costUsd: Number(final.total_cost_usd) || 0,
|
|
147
|
+
turns: Number(final.num_turns) || 0,
|
|
148
|
+
error: final.is_error ? String(final.result ?? final.subtype ?? "error") : final.subtype === "error_max_turns" ? "max turns reached" : void 0
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
var skillsCalled = (events) => events.filter((e) => e.type === "assistant").flatMap((e) => Array.isArray(e.message?.content) ? e.message.content : []).filter((c) => c.type === "tool_use" && c.name === "Skill" && typeof c.input?.skill === "string").map((c) => c.input.skill);
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
runClaude,
|
|
155
|
+
skillsCalled
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=chunk-IEZVFKUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/claude-run.ts"],"sourcesContent":["/**\n * Every `claude` process the skill loop starts goes through here (v1.7): the reviewer, eval runs, graders,\n * description tuning. Two guarantees:\n *\n * - **At most 5 at once, machine-wide.** A slot file under ~/.semantic-memory/claude-slots/ is held for the\n * life of each run (SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY, 1–5, higher clamps to 5). A slot whose\n * holder died is reclaimed.\n * - **Fenced.** No hooks, no MCP servers (claude.ai connectors included), no user settings, only the tools\n * asked for, never Agent/Task (no nested subagents), nothing outside the working directory (dontAsk),\n * no session file, and SEMANTIC_MEMORY_REVIEWER=1 so any semantic-memory hook that still runs exits at once.\n * Verified flag by flag on Claude Code 2.1.310 (.planning/v17-skill-loop-and-jev-mode/notes.md, Phase 2).\n */\nimport { spawn } from \"node:child_process\";\nimport { openSync, writeSync, closeSync, readFileSync, unlinkSync, mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport const MAX_CLAUDE = 5;\n\nexport function claudeConcurrency(env = process.env): number {\n const n = Number(env.SEMANTIC_MEMORY_SKILL_LOOP_CONCURRENCY);\n return Number.isFinite(n) && n >= 1 ? Math.min(MAX_CLAUDE, Math.floor(n)) : MAX_CLAUDE;\n}\n\nexport const slotsDir = (env = process.env) =>\n env.SEMANTIC_MEMORY_CLAUDE_SLOTS_DIR || join(env.HOME || homedir(), \".semantic-memory\", \"claude-slots\");\n\nconst alive = (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === \"EPERM\";\n }\n};\n\n/** Take one of the machine-wide slots, waiting for one to free up. Resolves to its release function. */\nexport async function acquireSlot(opts: { env?: NodeJS.ProcessEnv; timeoutMs?: number; pollMs?: number } = {}): Promise<() => void> {\n const env = opts.env ?? process.env;\n const dir = slotsDir(env);\n const max = claudeConcurrency(env);\n const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);\n mkdirSync(dir, { recursive: true });\n for (;;) {\n for (let i = 0; i < max; i++) {\n const path = join(dir, `slot-${i}`);\n try {\n const fd = openSync(path, \"wx\");\n writeSync(fd, `${process.pid}\\n`);\n closeSync(fd);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n try {\n unlinkSync(path);\n } catch {}\n };\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== \"EEXIST\") throw e;\n // held: reclaim it when its holder is gone (crashed worker, killed run)\n try {\n const pid = Number(readFileSync(path, \"utf8\").trim());\n if (!pid || !alive(pid)) unlinkSync(path);\n } catch {}\n }\n }\n if (Date.now() > deadline) throw new Error(`no claude slot free after ${opts.timeoutMs ?? 1_800_000} ms (${max} in use)`);\n await new Promise((r) => setTimeout(r, opts.pollMs ?? 250));\n }\n}\n\nexport interface ClaudeRunOptions {\n prompt: string;\n cwd: string;\n /** built-in tools the run may use; [] = none */\n tools: string[];\n /** with dontAsk, only these permission rules run un-prompted, e.g. \"Edit\", \"Bash(npm test:*)\"; the rest is denied */\n allowedTools?: string[];\n maxTurns: number;\n model?: string;\n /** \"json\": one result object; \"stream-json\": every event (to see which tools were called) */\n output?: \"json\" | \"stream-json\";\n timeoutMs?: number;\n /** extra directories the run may read */\n addDirs?: string[];\n appendSystemPrompt?: string;\n env?: NodeJS.ProcessEnv;\n}\n\nexport interface ClaudeRunResult {\n ok: boolean;\n /** final assistant text */\n result: string;\n /** stream-json events (empty for json output) */\n events: any[];\n costUsd: number;\n turns: number;\n error?: string;\n}\n\nexport const claudeBin = (env = process.env) => env.SEMANTIC_MEMORY_CLAUDE_BIN || \"claude\";\nexport const skillLoopModel = (env = process.env) => env.SEMANTIC_MEMORY_SKILL_LOOP_MODEL || \"sonnet\";\n\n/** The fenced argument list (exported for tests: these flags are the security boundary). */\nexport function fencedArgs(o: Pick<ClaudeRunOptions, \"tools\" | \"allowedTools\" | \"maxTurns\" | \"model\" | \"output\" | \"addDirs\" | \"appendSystemPrompt\">, env = process.env): string[] {\n const args = [\n \"-p\",\n \"--tools\", o.tools.join(\",\"),\n \"--disallowedTools\", \"Task,Agent\",\n \"--strict-mcp-config\",\n \"--settings\", JSON.stringify({ disableAllHooks: true }),\n \"--setting-sources\", \"project,local\",\n \"--permission-mode\", \"dontAsk\",\n \"--no-session-persistence\",\n \"--max-turns\", String(o.maxTurns),\n \"--model\", o.model ?? skillLoopModel(env),\n \"--output-format\", o.output ?? \"json\",\n ];\n if (o.allowedTools?.length) args.push(\"--allowedTools\", o.allowedTools.join(\",\"));\n if ((o.output ?? \"json\") === \"stream-json\") args.push(\"--verbose\");\n for (const d of o.addDirs ?? []) args.push(\"--add-dir\", d);\n if (o.appendSystemPrompt) args.push(\"--append-system-prompt\", o.appendSystemPrompt);\n return args;\n}\n\n/** Run one fenced `claude -p` inside a slot. Never throws: failures come back as `ok: false` with the reason. */\nexport async function runClaude(o: ClaudeRunOptions): Promise<ClaudeRunResult> {\n const env = { ...(o.env ?? process.env), SEMANTIC_MEMORY_REVIEWER: \"1\" };\n const fail = (error: string): ClaudeRunResult => ({ ok: false, result: \"\", events: [], costUsd: 0, turns: 0, error });\n let release: () => void;\n try {\n release = await acquireSlot({ env });\n } catch (e) {\n return fail((e as Error).message);\n }\n try {\n return await new Promise<ClaudeRunResult>((done) => {\n const child = spawn(claudeBin(env), fencedArgs(o, env), { cwd: o.cwd, env, stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n let stdout = \"\";\n let stderr = \"\";\n const timer = setTimeout(() => {\n child.kill(\"SIGKILL\");\n done(fail(`timed out after ${o.timeoutMs ?? 600_000} ms`));\n }, o.timeoutMs ?? 600_000);\n child.stdout.on(\"data\", (d) => (stdout += d));\n child.stderr.on(\"data\", (d) => (stderr += d));\n child.on(\"error\", (e) => {\n clearTimeout(timer);\n done(fail(`could not start ${claudeBin(env)}: ${e.message}`));\n });\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n done(parseClaudeOutput(stdout, o.output ?? \"json\", code, stderr));\n });\n child.stdin.on(\"error\", () => {});\n child.stdin.end(o.prompt);\n });\n } finally {\n release();\n }\n}\n\nexport function parseClaudeOutput(stdout: string, output: \"json\" | \"stream-json\", code: number | null, stderr = \"\"): ClaudeRunResult {\n const events: any[] = [];\n let final: any = null;\n if (output === \"json\") {\n try {\n final = JSON.parse(stdout.trim());\n } catch {}\n } else {\n for (const line of stdout.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const e = JSON.parse(line);\n events.push(e);\n if (e.type === \"result\") final = e;\n } catch {}\n }\n }\n if (!final) return { ok: false, result: \"\", events, costUsd: 0, turns: 0, error: `no result from claude (exit ${code}): ${stderr.trim().slice(0, 300)}` };\n return {\n ok: !final.is_error && final.subtype !== \"error_during_execution\",\n result: typeof final.result === \"string\" ? final.result : \"\",\n events,\n costUsd: Number(final.total_cost_usd) || 0,\n turns: Number(final.num_turns) || 0,\n error: final.is_error ? String(final.result ?? final.subtype ?? \"error\") : final.subtype === \"error_max_turns\" ? \"max turns reached\" : undefined,\n };\n}\n\n/** Names of the skills a stream-json run invoked through the Skill tool, in order. */\nexport const skillsCalled = (events: any[]): string[] =>\n events\n .filter((e) => e.type === \"assistant\")\n .flatMap((e) => (Array.isArray(e.message?.content) ? e.message.content : []))\n .filter((c: any) => c.type === \"tool_use\" && c.name === \"Skill\" && typeof c.input?.skill === \"string\")\n .map((c: any) => c.input.skill);\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/core/matter.ts
|
|
2
|
+
import grayMatter from "gray-matter";
|
|
3
|
+
var matter = Object.assign((raw) => grayMatter(raw, {}), { stringify: grayMatter.stringify });
|
|
4
|
+
var matter_default = matter;
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
matter_default
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=chunk-J7LYDRML.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/matter.ts"],"sourcesContent":["import grayMatter from \"gray-matter\";\n\n// Called without options, gray-matter caches every parse by file content: a parse that threw comes back\n// unparsed (and without throwing) on the next call, and every caller shares one mutable `data` object.\n// Passing options skips the cache. Import this instead of gray-matter.\nconst matter = Object.assign((raw: string) => grayMatter(raw, {}), { stringify: grayMatter.stringify });\n\nexport default matter;\n"],"mappings":";AAAA,OAAO,gBAAgB;AAKvB,IAAM,SAAS,OAAO,OAAO,CAAC,QAAgB,WAAW,KAAK,CAAC,CAAC,GAAG,EAAE,WAAW,WAAW,UAAU,CAAC;AAEtG,IAAO,iBAAQ;","names":[]}
|