@theglitchking/semantic-memory 1.2.2 → 1.5.1
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 +4 -4
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +154 -0
- package/ROADMAP.md +5 -2
- package/commands/healthcheck.md +13 -11
- package/dist/{chunk-UQSASB7Q.js → chunk-2EELGTLX.js} +32 -3
- package/dist/chunk-2EELGTLX.js.map +1 -0
- package/dist/chunk-5ZKNO6ZD.js +96 -0
- package/dist/chunk-5ZKNO6ZD.js.map +1 -0
- package/dist/chunk-7M3JZOYH.js +105 -0
- package/dist/chunk-7M3JZOYH.js.map +1 -0
- package/dist/chunk-A3KYHKB5.js +176 -0
- package/dist/chunk-A3KYHKB5.js.map +1 -0
- package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
- package/dist/chunk-CSXZ7QUU.js.map +1 -0
- package/dist/chunk-FM26NFTR.js +272 -0
- package/dist/chunk-FM26NFTR.js.map +1 -0
- package/dist/chunk-NTYYS53O.js +91 -0
- package/dist/chunk-NTYYS53O.js.map +1 -0
- package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
- package/dist/chunk-OWZWMB4P.js.map +1 -0
- package/dist/chunk-S4KW7VH4.js +203 -0
- package/dist/chunk-S4KW7VH4.js.map +1 -0
- package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
- package/dist/chunk-V45NJIHI.js.map +1 -0
- package/dist/chunk-WSU4QIPB.js +311 -0
- package/dist/chunk-WSU4QIPB.js.map +1 -0
- package/dist/cli/index.js +265 -65
- package/dist/cli/index.js.map +1 -1
- package/dist/core/index.d.ts +39 -2
- package/dist/core/index.js +17 -11
- package/dist/decay-ELJUKVQG.js +14 -0
- package/dist/dossier-2UG724LB.js +39 -0
- package/dist/healthcheck-IMB47LZE.js +19 -0
- package/dist/indexer-YW4O3VVM.js +8 -0
- package/dist/lexicon-5PXQEMUF.js +26 -0
- package/dist/lexicon-5PXQEMUF.js.map +1 -0
- package/dist/lint-XUQ7JDYX.js +16 -0
- package/dist/lint-XUQ7JDYX.js.map +1 -0
- package/dist/mcp/server.js +478 -40
- package/dist/mcp/server.js.map +1 -1
- package/dist/profile-LXUEF7TX.js +20 -0
- package/dist/profile-LXUEF7TX.js.map +1 -0
- package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
- package/dist/schema-ATFCLC6Y.js.map +1 -0
- package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
- package/dist/telemetry-GB2TEHVE.js +21 -0
- package/dist/telemetry-GB2TEHVE.js.map +1 -0
- package/hooks/vault-context.js +270 -11
- package/package.json +16 -1
- package/skills/vault-first/SKILL.md +18 -0
- package/dist/chunk-CAN2GWUG.js +0 -88
- package/dist/chunk-CAN2GWUG.js.map +0 -1
- package/dist/chunk-CYWPDFNI.js.map +0 -1
- package/dist/chunk-E2Z4J4MK.js.map +0 -1
- package/dist/chunk-HODDRXJK.js.map +0 -1
- package/dist/chunk-UQSASB7Q.js.map +0 -1
- package/dist/healthcheck-E4GI5RMZ.js +0 -14
- package/dist/indexer-KD6UTXZB.js +0 -8
- package/dist/lint-GMXQF4RW.js +0 -11
- /package/dist/{healthcheck-E4GI5RMZ.js.map → decay-ELJUKVQG.js.map} +0 -0
- /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
- /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
- /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
- /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendToSection,
|
|
3
|
+
replaceSectionBody
|
|
4
|
+
} from "./chunk-WSU4QIPB.js";
|
|
5
|
+
|
|
6
|
+
// src/core/profile.ts
|
|
7
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import matter from "gray-matter";
|
|
11
|
+
var SPEAKER_PROFILE_PATH = "profile/speaker.md";
|
|
12
|
+
var PROFILE_SECTIONS = [
|
|
13
|
+
"Severity calibration",
|
|
14
|
+
"Chronic omissions",
|
|
15
|
+
"Verbosity preference",
|
|
16
|
+
"Shorthand & terms"
|
|
17
|
+
];
|
|
18
|
+
function speakerProfileTemplate() {
|
|
19
|
+
return `# Speaker profile
|
|
20
|
+
|
|
21
|
+
_How this human communicates \u2014 learned over time, corrected in place. One human, one profile._
|
|
22
|
+
|
|
23
|
+
## Severity calibration
|
|
24
|
+
|
|
25
|
+
_What their words map to. e.g. "annoying" = low, "broken" = ship-blocking, "on fire" = drop everything._
|
|
26
|
+
|
|
27
|
+
## Chronic omissions
|
|
28
|
+
|
|
29
|
+
_What they habitually leave unsaid \u2014 env, repro steps, which service \u2014 so you ask up front._
|
|
30
|
+
|
|
31
|
+
## Verbosity preference
|
|
32
|
+
|
|
33
|
+
_How much detail they want back: terse answer first, or full reasoning? Code or prose?_
|
|
34
|
+
|
|
35
|
+
## Shorthand & terms
|
|
36
|
+
|
|
37
|
+
_Non-entity shorthand: "the usual", "ship it", "the thing from yesterday" \u2014 and what each has meant._
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
function isPlaceholderLine(line) {
|
|
41
|
+
const t = line.trim();
|
|
42
|
+
return t === "" || /^_.*_$/.test(t);
|
|
43
|
+
}
|
|
44
|
+
async function readSpeakerProfile(vaultPath, maxLines = 30) {
|
|
45
|
+
const abs = join(vaultPath, SPEAKER_PROFILE_PATH);
|
|
46
|
+
if (!existsSync(abs)) return null;
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(abs, "utf-8");
|
|
49
|
+
const { content } = matter(raw);
|
|
50
|
+
const body = content.trim();
|
|
51
|
+
const kept = [];
|
|
52
|
+
let lastWasHeading = false;
|
|
53
|
+
for (const line of body.split("\n")) {
|
|
54
|
+
const isHeading = /^#{1,6}\s/.test(line);
|
|
55
|
+
if (isHeading) {
|
|
56
|
+
kept.push(line.replace(/^#+\s*/, "").trim());
|
|
57
|
+
lastWasHeading = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isPlaceholderLine(line)) continue;
|
|
61
|
+
kept.push(line.trim());
|
|
62
|
+
lastWasHeading = false;
|
|
63
|
+
}
|
|
64
|
+
void lastWasHeading;
|
|
65
|
+
const head = kept.slice(0, maxLines).join("\n").trim();
|
|
66
|
+
return { path: SPEAKER_PROFILE_PATH, body, head };
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function initSpeakerProfile(vaultPath, today) {
|
|
72
|
+
const abs = join(vaultPath, SPEAKER_PROFILE_PATH);
|
|
73
|
+
if (existsSync(abs)) return { path: SPEAKER_PROFILE_PATH, created: false };
|
|
74
|
+
const date = today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
75
|
+
const fm = {
|
|
76
|
+
title: "Speaker profile",
|
|
77
|
+
type: "profile",
|
|
78
|
+
status: "active",
|
|
79
|
+
last_verified: date,
|
|
80
|
+
confidence: "medium",
|
|
81
|
+
evergreen: true
|
|
82
|
+
};
|
|
83
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
84
|
+
await writeFile(abs, matter.stringify(speakerProfileTemplate(), fm), "utf-8");
|
|
85
|
+
return { path: SPEAKER_PROFILE_PATH, created: true };
|
|
86
|
+
}
|
|
87
|
+
async function updateProfileSection(vaultPath, section, text, mode = "append") {
|
|
88
|
+
await initSpeakerProfile(vaultPath);
|
|
89
|
+
const abs = join(vaultPath, SPEAKER_PROFILE_PATH);
|
|
90
|
+
const raw = await readFile(abs, "utf-8");
|
|
91
|
+
const parsed = matter(raw);
|
|
92
|
+
const patchedBody = mode === "replace" ? replaceSectionBody(parsed.content, section, text.trim()) : appendToSection(parsed.content, section, `- ${text.trim()}`);
|
|
93
|
+
await writeFile(abs, matter.stringify(patchedBody, parsed.data), "utf-8");
|
|
94
|
+
return { path: SPEAKER_PROFILE_PATH };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
SPEAKER_PROFILE_PATH,
|
|
99
|
+
PROFILE_SECTIONS,
|
|
100
|
+
speakerProfileTemplate,
|
|
101
|
+
readSpeakerProfile,
|
|
102
|
+
initSpeakerProfile,
|
|
103
|
+
updateProfileSection
|
|
104
|
+
};
|
|
105
|
+
//# sourceMappingURL=chunk-7M3JZOYH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/profile.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport matter from \"gray-matter\";\nimport { replaceSectionBody, appendToSection } from \"./dossier.js\";\n\n/**\n * Speaker profile (v1.5 Phase 11) — the model of THIS human.\n *\n * The difference between an expert who knows the system and one who knows the\n * system *and knows you*: how this person calibrates severity (\"annoying\" vs\n * \"broken\" vs \"on fire\"), what they chronically leave unsaid, how much detail they\n * want back, and the shorthand they use that isn't an entity. One note, one human\n * (multi-speaker is an explicit non-goal). Injected at SessionStart alongside the\n * session digest; updated via capture cues when the user corrects an interpretation.\n */\n\nexport const SPEAKER_PROFILE_PATH = \"profile/speaker.md\";\n\n/** The fixed sections a speaker profile carries. */\nexport const PROFILE_SECTIONS = [\n \"Severity calibration\",\n \"Chronic omissions\",\n \"Verbosity preference\",\n \"Shorthand & terms\",\n] as const;\n\nexport type ProfileSection = (typeof PROFILE_SECTIONS)[number];\n\nexport interface SpeakerProfile {\n path: string;\n /** The whole note body (frontmatter stripped), trimmed. */\n body: string;\n /** A capped, injection-ready rendering (headings + non-placeholder lines). */\n head: string;\n}\n\nexport function speakerProfileTemplate(): string {\n return `# Speaker profile\n\n_How this human communicates — learned over time, corrected in place. One human, one profile._\n\n## Severity calibration\n\n_What their words map to. e.g. \"annoying\" = low, \"broken\" = ship-blocking, \"on fire\" = drop everything._\n\n## Chronic omissions\n\n_What they habitually leave unsaid — env, repro steps, which service — so you ask up front._\n\n## Verbosity preference\n\n_How much detail they want back: terse answer first, or full reasoning? Code or prose?_\n\n## Shorthand & terms\n\n_Non-entity shorthand: \"the usual\", \"ship it\", \"the thing from yesterday\" — and what each has meant._\n`;\n}\n\n/** True when a line is only a template placeholder (italic prompt) or blank. */\nfunction isPlaceholderLine(line: string): boolean {\n const t = line.trim();\n return t === \"\" || /^_.*_$/.test(t);\n}\n\n/**\n * Read the speaker profile, returning a capped injection head that drops the\n * template placeholders (so an unfilled profile injects nothing but its headings,\n * and a filled one injects only the real content). Returns null when absent.\n */\nexport async function readSpeakerProfile(vaultPath: string, maxLines = 30): Promise<SpeakerProfile | null> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { content } = matter(raw);\n const body = content.trim();\n // Build the head: keep headings and real (non-placeholder) content lines.\n const kept: string[] = [];\n let lastWasHeading = false;\n for (const line of body.split(\"\\n\")) {\n const isHeading = /^#{1,6}\\s/.test(line);\n if (isHeading) {\n // Drop a heading immediately followed by nothing real (avoid trailing empty sections at render).\n kept.push(line.replace(/^#+\\s*/, \"\").trim());\n lastWasHeading = true;\n continue;\n }\n if (isPlaceholderLine(line)) continue;\n kept.push(line.trim());\n lastWasHeading = false;\n }\n void lastWasHeading;\n const head = kept.slice(0, maxLines).join(\"\\n\").trim();\n return { path: SPEAKER_PROFILE_PATH, body, head };\n } catch {\n return null;\n }\n}\n\nexport interface InitProfileResult {\n path: string;\n created: boolean;\n}\n\n/** Scaffold the speaker profile note. No-op (created:false) if it already exists. */\nexport async function initSpeakerProfile(vaultPath: string, today?: string): Promise<InitProfileResult> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (existsSync(abs)) return { path: SPEAKER_PROFILE_PATH, created: false };\n const date = today ?? new Date().toISOString().slice(0, 10);\n const fm = {\n title: \"Speaker profile\",\n type: \"profile\",\n status: \"active\",\n last_verified: date,\n confidence: \"medium\",\n evergreen: true,\n };\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(speakerProfileTemplate(), fm), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH, created: true };\n}\n\n/**\n * Update one section of the speaker profile. `mode: \"replace\"` swaps the section\n * body; `mode: \"append\"` adds a line (accretion). Scaffolds the note first if\n * absent. Returns the resolved path.\n */\nexport async function updateProfileSection(\n vaultPath: string,\n section: string,\n text: string,\n mode: \"replace\" | \"append\" = \"append\"\n): Promise<{ path: string }> {\n await initSpeakerProfile(vaultPath);\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n const raw = await readFile(abs, \"utf-8\");\n const parsed = matter(raw);\n const patchedBody = mode === \"replace\"\n ? replaceSectionBody(parsed.content, section, text.trim())\n : appendToSection(parsed.content, section, `- ${text.trim()}`);\n await writeFile(abs, matter.stringify(patchedBody, parsed.data), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH };\n}\n"],"mappings":";;;;;;AAAA,SAAS,UAAU,WAAW,aAAa;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,MAAM,eAAe;AAC9B,OAAO,YAAY;AAcZ,IAAM,uBAAuB;AAG7B,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,yBAAiC;AAC/C,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBT;AAGA,SAAS,kBAAkB,MAAuB;AAChD,QAAM,IAAI,KAAK,KAAK;AACpB,SAAO,MAAM,MAAM,SAAS,KAAK,CAAC;AACpC;AAOA,eAAsB,mBAAmB,WAAmB,WAAW,IAAoC;AACzG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,UAAM,EAAE,QAAQ,IAAI,OAAO,GAAG;AAC9B,UAAM,OAAO,QAAQ,KAAK;AAE1B,UAAM,OAAiB,CAAC;AACxB,QAAI,iBAAiB;AACrB,eAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,YAAM,YAAY,YAAY,KAAK,IAAI;AACvC,UAAI,WAAW;AAEb,aAAK,KAAK,KAAK,QAAQ,UAAU,EAAE,EAAE,KAAK,CAAC;AAC3C,yBAAiB;AACjB;AAAA,MACF;AACA,UAAI,kBAAkB,IAAI,EAAG;AAC7B,WAAK,KAAK,KAAK,KAAK,CAAC;AACrB,uBAAiB;AAAA,IACnB;AACA,SAAK;AACL,UAAM,OAAO,KAAK,MAAM,GAAG,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK;AACrD,WAAO,EAAE,MAAM,sBAAsB,MAAM,KAAK;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,mBAAmB,WAAmB,OAA4C;AACtG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,WAAW,GAAG,EAAG,QAAO,EAAE,MAAM,sBAAsB,SAAS,MAAM;AACzE,QAAM,OAAO,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC1D,QAAM,KAAK;AAAA,IACT,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,WAAW;AAAA,EACb;AACA,QAAM,MAAM,QAAQ,GAAG,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,QAAM,UAAU,KAAK,OAAO,UAAU,uBAAuB,GAAG,EAAE,GAAG,OAAO;AAC5E,SAAO,EAAE,MAAM,sBAAsB,SAAS,KAAK;AACrD;AAOA,eAAsB,qBACpB,WACA,SACA,MACA,OAA6B,UACF;AAC3B,QAAM,mBAAmB,SAAS;AAClC,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,QAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,QAAM,SAAS,OAAO,GAAG;AACzB,QAAM,cAAc,SAAS,YACzB,mBAAmB,OAAO,SAAS,SAAS,KAAK,KAAK,CAAC,IACvD,gBAAgB,OAAO,SAAS,SAAS,KAAK,KAAK,KAAK,CAAC,EAAE;AAC/D,QAAM,UAAU,KAAK,OAAO,UAAU,aAAa,OAAO,IAAI,GAAG,OAAO;AACxE,SAAO,EAAE,MAAM,qBAAqB;AACtC;","names":[]}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listDossiers
|
|
3
|
+
} from "./chunk-WSU4QIPB.js";
|
|
4
|
+
import {
|
|
5
|
+
deriveSessionDir
|
|
6
|
+
} from "./chunk-S4KW7VH4.js";
|
|
7
|
+
|
|
8
|
+
// src/core/lexicon.ts
|
|
9
|
+
import { readFile, writeFile, mkdir, unlink } from "fs/promises";
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { join, dirname } from "path";
|
|
12
|
+
import { glob } from "glob";
|
|
13
|
+
import matter from "gray-matter";
|
|
14
|
+
var CACHE_FILE = "lexicon-cache.json";
|
|
15
|
+
var LEXICON_DIR = "lexicon";
|
|
16
|
+
function normalizePhrase(s) {
|
|
17
|
+
return s.toLowerCase().trim().replace(/\s+/g, " ");
|
|
18
|
+
}
|
|
19
|
+
function slugify(s) {
|
|
20
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "alias";
|
|
21
|
+
}
|
|
22
|
+
function confidenceFor(evidenceCount) {
|
|
23
|
+
return evidenceCount >= 3 ? "high" : evidenceCount >= 2 ? "medium" : "low";
|
|
24
|
+
}
|
|
25
|
+
async function compileLexicon(vaultPath) {
|
|
26
|
+
const files = await glob(`${LEXICON_DIR}/**/*.md`, { cwd: vaultPath });
|
|
27
|
+
const entries = [];
|
|
28
|
+
for (const rel of files) {
|
|
29
|
+
try {
|
|
30
|
+
const { data } = matter(await readFile(join(vaultPath, rel), "utf-8"));
|
|
31
|
+
if (data.type !== "alias" || typeof data.canonical !== "string") continue;
|
|
32
|
+
const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];
|
|
33
|
+
entries.push({
|
|
34
|
+
canonical: data.canonical,
|
|
35
|
+
phrases,
|
|
36
|
+
confidence: ["low", "medium", "high"].includes(data.confidence) ? data.confidence : confidenceFor(1),
|
|
37
|
+
evidence_count: typeof data.evidence_count === "number" ? data.evidence_count : phrases.length ? 1 : 0,
|
|
38
|
+
last_used: typeof data.last_used === "string" ? data.last_used : void 0,
|
|
39
|
+
source: data.source === "authored" ? "authored" : "learned",
|
|
40
|
+
path: rel
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const dossiers = await listDossiers(vaultPath);
|
|
47
|
+
for (const d of dossiers) {
|
|
48
|
+
if (d.aliases.length === 0) continue;
|
|
49
|
+
if (entries.some((e) => e.canonical === d.path)) continue;
|
|
50
|
+
entries.push({
|
|
51
|
+
canonical: d.path,
|
|
52
|
+
phrases: d.aliases,
|
|
53
|
+
confidence: "high",
|
|
54
|
+
evidence_count: d.aliases.length,
|
|
55
|
+
source: "authored",
|
|
56
|
+
path: d.path
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const dir = deriveSessionDir(vaultPath);
|
|
63
|
+
await mkdir(dir, { recursive: true });
|
|
64
|
+
await writeFile(join(dir, CACHE_FILE), JSON.stringify({ compiledAt: (/* @__PURE__ */ new Date()).toISOString(), aliases: entries }, null, 2), "utf-8");
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
async function loadLexiconCache(vaultPath) {
|
|
70
|
+
const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);
|
|
71
|
+
if (existsSync(cachePath)) {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(await readFile(cachePath, "utf-8"));
|
|
74
|
+
if (Array.isArray(data?.aliases)) return data.aliases;
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return compileLexicon(vaultPath);
|
|
79
|
+
}
|
|
80
|
+
function lookupPhrase(aliases, phrase) {
|
|
81
|
+
const n = normalizePhrase(phrase);
|
|
82
|
+
return aliases.filter((a) => a.phrases.some((p) => normalizePhrase(p) === n));
|
|
83
|
+
}
|
|
84
|
+
function expandQuery(aliases, query) {
|
|
85
|
+
const nq = normalizePhrase(query);
|
|
86
|
+
const matched = aliases.filter((a) => a.phrases.some((p) => p && nq.includes(normalizePhrase(p))));
|
|
87
|
+
if (matched.length === 0) return { expanded: query, matched: [] };
|
|
88
|
+
const additions = [...new Set(matched.map((a) => a.canonical))].join(" ");
|
|
89
|
+
return { expanded: `${query} ${additions}`, matched };
|
|
90
|
+
}
|
|
91
|
+
async function writeAliasNote(vaultPath, rel, entry) {
|
|
92
|
+
const fm = {
|
|
93
|
+
type: "alias",
|
|
94
|
+
canonical: entry.canonical,
|
|
95
|
+
phrases: entry.phrases,
|
|
96
|
+
confidence: entry.confidence,
|
|
97
|
+
evidence_count: entry.evidence_count,
|
|
98
|
+
last_used: entry.last_used,
|
|
99
|
+
source: entry.source
|
|
100
|
+
};
|
|
101
|
+
const body = `# Alias: ${entry.canonical}
|
|
102
|
+
|
|
103
|
+
Human phrases that refer to \`${entry.canonical}\`.
|
|
104
|
+
`;
|
|
105
|
+
const abs = join(vaultPath, rel);
|
|
106
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
107
|
+
await writeFile(abs, matter.stringify(body, fm), "utf-8");
|
|
108
|
+
}
|
|
109
|
+
async function addAlias(vaultPath, input) {
|
|
110
|
+
const existing = await compileLexicon(vaultPath);
|
|
111
|
+
const match = existing.find((a) => a.canonical === input.canonical);
|
|
112
|
+
const today = input.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
113
|
+
const cleanPhrases = input.phrases.map((p) => p.trim()).filter(Boolean);
|
|
114
|
+
if (match) {
|
|
115
|
+
const phrases = [.../* @__PURE__ */ new Set([...match.phrases, ...cleanPhrases])];
|
|
116
|
+
const evidence_count2 = match.evidence_count + 1;
|
|
117
|
+
const confidence2 = confidenceFor(evidence_count2);
|
|
118
|
+
await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count: evidence_count2, confidence: confidence2, last_used: today });
|
|
119
|
+
await compileLexicon(vaultPath);
|
|
120
|
+
return { path: match.path, created: false, evidence_count: evidence_count2, confidence: confidence2 };
|
|
121
|
+
}
|
|
122
|
+
const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);
|
|
123
|
+
const evidence_count = 1;
|
|
124
|
+
const confidence = confidenceFor(evidence_count);
|
|
125
|
+
await writeAliasNote(vaultPath, rel, {
|
|
126
|
+
canonical: input.canonical,
|
|
127
|
+
phrases: cleanPhrases,
|
|
128
|
+
confidence,
|
|
129
|
+
evidence_count,
|
|
130
|
+
last_used: today,
|
|
131
|
+
source: input.source ?? "learned"
|
|
132
|
+
});
|
|
133
|
+
await compileLexicon(vaultPath);
|
|
134
|
+
return { path: rel, created: true, evidence_count, confidence };
|
|
135
|
+
}
|
|
136
|
+
function findAliasConflicts(aliases) {
|
|
137
|
+
const byPhrase = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const a of aliases) {
|
|
139
|
+
for (const p of a.phrases) {
|
|
140
|
+
const n = normalizePhrase(p);
|
|
141
|
+
if (!n) continue;
|
|
142
|
+
if (!byPhrase.has(n)) byPhrase.set(n, /* @__PURE__ */ new Set());
|
|
143
|
+
byPhrase.get(n).add(a.canonical);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const conflicts = [];
|
|
147
|
+
for (const [phrase, canonicals] of byPhrase) {
|
|
148
|
+
if (canonicals.size > 1) conflicts.push({ phrase, canonicals: [...canonicals].sort() });
|
|
149
|
+
}
|
|
150
|
+
return conflicts.sort((a, b) => a.phrase.localeCompare(b.phrase));
|
|
151
|
+
}
|
|
152
|
+
async function removeAlias(vaultPath, canonical) {
|
|
153
|
+
const existing = await compileLexicon(vaultPath);
|
|
154
|
+
const match = existing.find((a) => a.canonical === canonical);
|
|
155
|
+
if (!match) return false;
|
|
156
|
+
try {
|
|
157
|
+
await unlink(join(vaultPath, match.path));
|
|
158
|
+
} catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
await compileLexicon(vaultPath);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export {
|
|
166
|
+
normalizePhrase,
|
|
167
|
+
slugify,
|
|
168
|
+
compileLexicon,
|
|
169
|
+
loadLexiconCache,
|
|
170
|
+
lookupPhrase,
|
|
171
|
+
expandQuery,
|
|
172
|
+
addAlias,
|
|
173
|
+
findAliasConflicts,
|
|
174
|
+
removeAlias
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=chunk-A3KYHKB5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/lexicon.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"gray-matter\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { listDossiers } from \"./dossier.js\";\n\n/**\n * Lexicon corpus (v1.4 Phase 3) — the learned human→artifact vocabulary.\n *\n * The heart of the human→LLM bridge: maps the human's own phrasing (\"the flaky\n * thing\", \"the deploy dance\") to concrete artifacts (paths / code symbols).\n * Canonical storage is markdown notes of type `alias` under <vault>/lexicon/;\n * a derived JSON cache under .claude/.semantic-memory/ is what the hot-path hook\n * reads for deterministic query expansion.\n */\n\nexport type Confidence = \"low\" | \"medium\" | \"high\";\n\nexport interface AliasEntry {\n /** The concrete target: a note path or a code symbol/path. */\n canonical: string;\n /** The human's actual words that refer to it (verbatim). */\n phrases: string[];\n confidence: Confidence;\n evidence_count: number;\n last_used?: string;\n source: \"learned\" | \"authored\";\n /** The lexicon note path this entry came from. */\n path: string;\n}\n\nconst CACHE_FILE = \"lexicon-cache.json\";\nconst LEXICON_DIR = \"lexicon\";\n\nexport function normalizePhrase(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\nexport function slugify(s: string): string {\n return s.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 60) || \"alias\";\n}\n\nfunction confidenceFor(evidenceCount: number): Confidence {\n return evidenceCount >= 3 ? \"high\" : evidenceCount >= 2 ? \"medium\" : \"low\";\n}\n\n/** Read every lexicon note into entries and write the derived cache. */\nexport async function compileLexicon(vaultPath: string): Promise<AliasEntry[]> {\n const files = await glob(`${LEXICON_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: AliasEntry[] = [];\n for (const rel of files) {\n try {\n const { data } = matter(await readFile(join(vaultPath, rel), \"utf-8\"));\n if (data.type !== \"alias\" || typeof data.canonical !== \"string\") continue;\n const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];\n entries.push({\n canonical: data.canonical,\n phrases,\n confidence: ([\"low\", \"medium\", \"high\"] as const).includes(data.confidence) ? data.confidence : confidenceFor(1),\n evidence_count: typeof data.evidence_count === \"number\" ? data.evidence_count : phrases.length ? 1 : 0,\n last_used: typeof data.last_used === \"string\" ? data.last_used : undefined,\n source: data.source === \"authored\" ? \"authored\" : \"learned\",\n path: rel,\n });\n } catch {\n /* skip unreadable note */\n }\n }\n // Fold in dossier aliases (v1.5 Phase 8): a dossier's `aliases:` are the human's\n // phrases for that entity, so they become query-expansion bindings pointing at the\n // dossier note. Authored, not learned — the human named the entity deliberately.\n // A lexicon note for the same canonical wins (it carries real evidence_count).\n try {\n const dossiers = await listDossiers(vaultPath);\n for (const d of dossiers) {\n if (d.aliases.length === 0) continue;\n if (entries.some((e) => e.canonical === d.path)) continue;\n entries.push({\n canonical: d.path,\n phrases: d.aliases,\n confidence: \"high\",\n evidence_count: d.aliases.length,\n source: \"authored\",\n path: d.path,\n });\n }\n } catch {\n /* dossiers optional — lexicon compiles fine without them */\n }\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, CACHE_FILE), JSON.stringify({ compiledAt: new Date().toISOString(), aliases: entries }, null, 2), \"utf-8\");\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 loadLexiconCache(vaultPath: string): Promise<AliasEntry[]> {\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?.aliases)) return data.aliases as AliasEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileLexicon(vaultPath);\n}\n\nexport function lookupPhrase(aliases: AliasEntry[], phrase: string): AliasEntry[] {\n const n = normalizePhrase(phrase);\n return aliases.filter((a) => a.phrases.some((p) => normalizePhrase(p) === n));\n}\n\n/**\n * Expand a query by appending the canonical targets of any alias phrases that\n * appear in it. Deterministic, no LLM — this is the Tier-1 hook-time expansion.\n */\nexport function expandQuery(aliases: AliasEntry[], query: string): { expanded: string; matched: AliasEntry[] } {\n const nq = normalizePhrase(query);\n const matched = aliases.filter((a) => a.phrases.some((p) => p && nq.includes(normalizePhrase(p))));\n if (matched.length === 0) return { expanded: query, matched: [] };\n const additions = [...new Set(matched.map((a) => a.canonical))].join(\" \");\n return { expanded: `${query} ${additions}`, matched };\n}\n\nasync function writeAliasNote(vaultPath: string, rel: string, entry: Omit<AliasEntry, \"path\">): Promise<void> {\n const fm = {\n type: \"alias\",\n canonical: entry.canonical,\n phrases: entry.phrases,\n confidence: entry.confidence,\n evidence_count: entry.evidence_count,\n last_used: entry.last_used,\n source: entry.source,\n };\n const body = `# Alias: ${entry.canonical}\\n\\nHuman phrases that refer to \\`${entry.canonical}\\`.\\n`;\n const abs = join(vaultPath, rel);\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n}\n\n/**\n * Upsert an alias. New canonical → new note (evidence 1). Existing canonical →\n * merge phrases, bump evidence_count, recompute confidence. Recompiles the cache.\n */\nexport async function addAlias(\n vaultPath: string,\n input: { canonical: string; phrases: string[]; source?: \"learned\" | \"authored\"; today?: string }\n): Promise<{ path: string; created: boolean; evidence_count: number; confidence: Confidence }> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === input.canonical);\n const today = input.today ?? new Date().toISOString().slice(0, 10);\n const cleanPhrases = input.phrases.map((p) => p.trim()).filter(Boolean);\n\n if (match) {\n const phrases = [...new Set([...match.phrases, ...cleanPhrases])];\n const evidence_count = match.evidence_count + 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count, confidence, last_used: today });\n await compileLexicon(vaultPath);\n return { path: match.path, created: false, evidence_count, confidence };\n }\n\n const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);\n const evidence_count = 1;\n const confidence = confidenceFor(evidence_count);\n await writeAliasNote(vaultPath, rel, {\n canonical: input.canonical,\n phrases: cleanPhrases,\n confidence,\n evidence_count,\n last_used: today,\n source: input.source ?? \"learned\",\n });\n await compileLexicon(vaultPath);\n return { path: rel, created: true, evidence_count, confidence };\n}\n\nexport interface AliasConflict {\n phrase: string;\n canonicals: string[];\n}\n\n/**\n * Find conflicting bindings: the same (normalized) phrase mapping to more than one\n * canonical target. A wrong alias silently misroutes every future query with that\n * phrase, so conflicts are surfaced (never auto-resolved) for a human to fix.\n */\nexport function findAliasConflicts(aliases: AliasEntry[]): AliasConflict[] {\n const byPhrase = new Map<string, Set<string>>();\n for (const a of aliases) {\n for (const p of a.phrases) {\n const n = normalizePhrase(p);\n if (!n) continue;\n if (!byPhrase.has(n)) byPhrase.set(n, new Set());\n byPhrase.get(n)!.add(a.canonical);\n }\n }\n const conflicts: AliasConflict[] = [];\n for (const [phrase, canonicals] of byPhrase) {\n if (canonicals.size > 1) conflicts.push({ phrase, canonicals: [...canonicals].sort() });\n }\n return conflicts.sort((a, b) => a.phrase.localeCompare(b.phrase));\n}\n\n/** Remove an alias note by canonical. Returns whether one was removed. */\nexport async function removeAlias(vaultPath: string, canonical: string): Promise<boolean> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === canonical);\n if (!match) return false;\n try {\n await unlink(join(vaultPath, match.path));\n } catch {\n return false;\n }\n await compileLexicon(vaultPath);\n return true;\n}\n"],"mappings":"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","names":["evidence_count","confidence"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadSchema,
|
|
3
3
|
validateNote
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OWZWMB4P.js";
|
|
5
5
|
|
|
6
6
|
// src/core/crud.ts
|
|
7
7
|
import { readFile, writeFile, unlink, rename, mkdir } from "fs/promises";
|
|
@@ -707,6 +707,7 @@ function buildSynthesizeChangeSet(input) {
|
|
|
707
707
|
...input.decision_maker && { decision_maker: input.decision_maker },
|
|
708
708
|
...input.decided_on && { decided_on: input.decided_on },
|
|
709
709
|
...input.severity && { severity: input.severity },
|
|
710
|
+
...input.symptoms && input.symptoms.length > 0 && { symptoms: input.symptoms },
|
|
710
711
|
...isProposal && { proposed_target: canonicalPath },
|
|
711
712
|
...input.extra_frontmatter ?? {}
|
|
712
713
|
};
|
|
@@ -917,203 +918,6 @@ async function inspectAgentsContract(projectRoot) {
|
|
|
917
918
|
return { path, exists: true, has_managed_block: true, local_notes_chars: localTail.length };
|
|
918
919
|
}
|
|
919
920
|
|
|
920
|
-
// src/core/session.ts
|
|
921
|
-
import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile6, rm } from "fs/promises";
|
|
922
|
-
import { existsSync as existsSync5 } from "fs";
|
|
923
|
-
import { join as join6 } from "path";
|
|
924
|
-
import { spawn } from "child_process";
|
|
925
|
-
var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
926
|
-
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
927
|
-
var TAIL_BYTES = 4096;
|
|
928
|
-
var SessionManager = class {
|
|
929
|
-
constructor(sessionDir) {
|
|
930
|
-
this.sessionDir = sessionDir;
|
|
931
|
-
}
|
|
932
|
-
sessionDir;
|
|
933
|
-
get sessionPath() {
|
|
934
|
-
return join6(this.sessionDir, "session.json");
|
|
935
|
-
}
|
|
936
|
-
async start(task, opts = {}) {
|
|
937
|
-
if (!task || task.trim().length === 0) {
|
|
938
|
-
return { ok: false, error: "session_start requires a non-empty task." };
|
|
939
|
-
}
|
|
940
|
-
await mkdir3(this.sessionDir, { recursive: true });
|
|
941
|
-
const existing = await this.read();
|
|
942
|
-
if (existing && !existing.closed_at) {
|
|
943
|
-
const reuseSameTask = opts.reuseSameTask !== false;
|
|
944
|
-
if (reuseSameTask && existing.task === task) {
|
|
945
|
-
return { ok: true, id: existing.id, reused: true };
|
|
946
|
-
}
|
|
947
|
-
return {
|
|
948
|
-
ok: false,
|
|
949
|
-
error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
953
|
-
const id = `ses_${now.replace(/[:.]/g, "-")}`;
|
|
954
|
-
const state = {
|
|
955
|
-
id,
|
|
956
|
-
task: task.trim(),
|
|
957
|
-
started_at: now,
|
|
958
|
-
last_activity_at: now,
|
|
959
|
-
verifications: [],
|
|
960
|
-
notes_touched: []
|
|
961
|
-
};
|
|
962
|
-
await writeFile6(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
|
|
963
|
-
return { ok: true, id };
|
|
964
|
-
}
|
|
965
|
-
async status() {
|
|
966
|
-
const state = await this.read();
|
|
967
|
-
if (!state || state.closed_at) return { state: "no_session" };
|
|
968
|
-
const age = Date.now() - new Date(state.last_activity_at).getTime();
|
|
969
|
-
const label = age > STALE_THRESHOLD_MS ? "stale" : "active";
|
|
970
|
-
return { ...state, state: label };
|
|
971
|
-
}
|
|
972
|
-
async run(cmd, timeoutMs) {
|
|
973
|
-
const state = await this.read();
|
|
974
|
-
if (!state || state.closed_at) {
|
|
975
|
-
return { ok: false, error: "No active session. Run session_start first." };
|
|
976
|
-
}
|
|
977
|
-
if (!cmd || cmd.trim().length === 0) {
|
|
978
|
-
return { ok: false, error: "session_run requires a non-empty command." };
|
|
979
|
-
}
|
|
980
|
-
const start = Date.now();
|
|
981
|
-
const record = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
982
|
-
record.duration_ms = Date.now() - start;
|
|
983
|
-
state.verifications.push(record);
|
|
984
|
-
state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
985
|
-
await writeFile6(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
|
|
986
|
-
return { ok: true, record };
|
|
987
|
-
}
|
|
988
|
-
async finish(input) {
|
|
989
|
-
const state = await this.read();
|
|
990
|
-
if (!state || state.closed_at) {
|
|
991
|
-
return { ok: false, error: "No active session to finish." };
|
|
992
|
-
}
|
|
993
|
-
const explicitWaiver = input.verified === false;
|
|
994
|
-
if (!explicitWaiver && state.verifications.length === 0) {
|
|
995
|
-
return {
|
|
996
|
-
ok: false,
|
|
997
|
-
error: "session_finish refused: no verification commands recorded. Run session_run with a real verification command (tests, lint, etc.) first, OR pass verified=false with a reason to waive verification (e.g. for doc-only edits)."
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {
|
|
1001
|
-
return {
|
|
1002
|
-
ok: false,
|
|
1003
|
-
error: "session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task."
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
if (!input.summary || input.summary.trim().length === 0) {
|
|
1007
|
-
return { ok: false, error: "session_finish requires a non-empty `summary`." };
|
|
1008
|
-
}
|
|
1009
|
-
const closed = {
|
|
1010
|
-
...state,
|
|
1011
|
-
closed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1012
|
-
closed_summary: input.summary.trim(),
|
|
1013
|
-
...explicitWaiver && {
|
|
1014
|
-
closed_verified_waived: true,
|
|
1015
|
-
closed_waiver_reason: input.reason.trim()
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
await rm(this.sessionPath, { force: true });
|
|
1019
|
-
return { ok: true, closed };
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Append paths to notes_touched on the active session, deduplicating. No-op when no
|
|
1023
|
-
* session is active. Used by patch tools to record what the session edited.
|
|
1024
|
-
*/
|
|
1025
|
-
async recordNotesTouched(paths) {
|
|
1026
|
-
if (paths.length === 0) return;
|
|
1027
|
-
const state = await this.read();
|
|
1028
|
-
if (!state || state.closed_at) return;
|
|
1029
|
-
const set = new Set(state.notes_touched);
|
|
1030
|
-
for (const p of paths) set.add(p);
|
|
1031
|
-
state.notes_touched = Array.from(set);
|
|
1032
|
-
state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1033
|
-
await writeFile6(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* Read the active session WITHOUT computing the stale label. Returns null when no
|
|
1037
|
-
* session is active or when the file is unparseable.
|
|
1038
|
-
*/
|
|
1039
|
-
async readActive() {
|
|
1040
|
-
const state = await this.read();
|
|
1041
|
-
if (!state || state.closed_at) return null;
|
|
1042
|
-
return state;
|
|
1043
|
-
}
|
|
1044
|
-
async read() {
|
|
1045
|
-
if (!existsSync5(this.sessionPath)) return null;
|
|
1046
|
-
try {
|
|
1047
|
-
const raw = await readFile6(this.sessionPath, "utf-8");
|
|
1048
|
-
const parsed = JSON.parse(raw);
|
|
1049
|
-
if (typeof parsed?.id !== "string") return null;
|
|
1050
|
-
return parsed;
|
|
1051
|
-
} catch {
|
|
1052
|
-
return null;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
spawnAndCapture(cmd, timeoutMs) {
|
|
1056
|
-
return new Promise((resolve) => {
|
|
1057
|
-
const proc = spawn(cmd, { shell: true });
|
|
1058
|
-
let buf = "";
|
|
1059
|
-
let killedByTimeout = false;
|
|
1060
|
-
const timer = setTimeout(() => {
|
|
1061
|
-
killedByTimeout = true;
|
|
1062
|
-
proc.kill("SIGTERM");
|
|
1063
|
-
setTimeout(() => {
|
|
1064
|
-
try {
|
|
1065
|
-
proc.kill("SIGKILL");
|
|
1066
|
-
} catch {
|
|
1067
|
-
}
|
|
1068
|
-
}, 1e3).unref();
|
|
1069
|
-
}, timeoutMs);
|
|
1070
|
-
const onData = (chunk) => {
|
|
1071
|
-
buf += chunk.toString("utf-8");
|
|
1072
|
-
if (buf.length > TAIL_BYTES * 4) {
|
|
1073
|
-
buf = buf.slice(buf.length - TAIL_BYTES * 2);
|
|
1074
|
-
}
|
|
1075
|
-
};
|
|
1076
|
-
proc.stdout?.on("data", onData);
|
|
1077
|
-
proc.stderr?.on("data", onData);
|
|
1078
|
-
proc.on("close", (code, signal) => {
|
|
1079
|
-
clearTimeout(timer);
|
|
1080
|
-
const tail = buf.slice(-TAIL_BYTES);
|
|
1081
|
-
resolve({
|
|
1082
|
-
cmd,
|
|
1083
|
-
exit: killedByTimeout ? null : code,
|
|
1084
|
-
signal: killedByTimeout ? "SIGTERM (timeout)" : signal ?? null,
|
|
1085
|
-
duration_ms: 0,
|
|
1086
|
-
tail,
|
|
1087
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1088
|
-
});
|
|
1089
|
-
});
|
|
1090
|
-
proc.on("error", (err) => {
|
|
1091
|
-
clearTimeout(timer);
|
|
1092
|
-
const tail = buf + `
|
|
1093
|
-
[spawn error: ${err.message}]`;
|
|
1094
|
-
resolve({
|
|
1095
|
-
cmd,
|
|
1096
|
-
exit: null,
|
|
1097
|
-
signal: null,
|
|
1098
|
-
duration_ms: 0,
|
|
1099
|
-
tail: tail.slice(-TAIL_BYTES),
|
|
1100
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1101
|
-
});
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
};
|
|
1106
|
-
function deriveProjectRoot(notesPath) {
|
|
1107
|
-
const norm = notesPath.replace(/[\\/]+$/, "");
|
|
1108
|
-
if (norm.endsWith("/.claude/.vault") || norm.endsWith("\\.claude\\.vault")) {
|
|
1109
|
-
return norm.slice(0, -".claude/.vault".length).replace(/[\\/]+$/, "");
|
|
1110
|
-
}
|
|
1111
|
-
return norm;
|
|
1112
|
-
}
|
|
1113
|
-
function deriveSessionDir(notesPath) {
|
|
1114
|
-
return join6(deriveProjectRoot(notesPath), ".claude", ".semantic-memory");
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
921
|
export {
|
|
1118
922
|
NoteCrud,
|
|
1119
923
|
FrontmatterManager,
|
|
@@ -1128,9 +932,6 @@ export {
|
|
|
1128
932
|
buildSynthesizeChangeSet,
|
|
1129
933
|
buildPromoteChangeSet,
|
|
1130
934
|
regenerateAgentsContract,
|
|
1131
|
-
inspectAgentsContract
|
|
1132
|
-
SessionManager,
|
|
1133
|
-
deriveProjectRoot,
|
|
1134
|
-
deriveSessionDir
|
|
935
|
+
inspectAgentsContract
|
|
1135
936
|
};
|
|
1136
|
-
//# sourceMappingURL=chunk-
|
|
937
|
+
//# sourceMappingURL=chunk-CSXZ7QUU.js.map
|