@tekmidian/pai 0.9.13 → 0.9.15
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/dist/{auto-route-DL4ViDKC.mjs → auto-route-D54Kv7yI.mjs} +2 -2
- package/dist/{auto-route-DL4ViDKC.mjs.map → auto-route-D54Kv7yI.mjs.map} +1 -1
- package/dist/cli/index.mjs +1365 -378
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{clusters-CRlPBpq8.mjs → clusters-BwmdKB-V.mjs} +1 -1
- package/dist/{clusters-CRlPBpq8.mjs.map → clusters-BwmdKB-V.mjs.map} +1 -1
- package/dist/{config-BuhHWyOK.mjs → config-DqBY3aT0.mjs} +1 -1
- package/dist/{config-BuhHWyOK.mjs.map → config-DqBY3aT0.mjs.map} +1 -1
- package/dist/daemon/index.mjs +12 -8
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-B-vBWU1i.mjs +1426 -0
- package/dist/daemon-B-vBWU1i.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{detector-AzVtGLtL.mjs → detector-Q_lVXtj-.mjs} +1 -1
- package/dist/{detector-AzVtGLtL.mjs.map → detector-Q_lVXtj-.mjs.map} +1 -1
- package/dist/{factory-BDBIfTDC.mjs → factory-BufouUQ1.mjs} +3 -3
- package/dist/{factory-BDBIfTDC.mjs.map → factory-BufouUQ1.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +39 -0
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/stop-hook.mjs +36 -0
- package/dist/hooks/stop-hook.mjs.map +2 -2
- package/dist/index.mjs +2 -2
- package/dist/indexer-D7MvSQPY.mjs +1 -0
- package/dist/{indexer-backend-DXkDh-kc.mjs → indexer-backend-BrrCnasS.mjs} +1 -1
- package/dist/{indexer-backend-DXkDh-kc.mjs.map → indexer-backend-BrrCnasS.mjs.map} +1 -1
- package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-C3pjwy2m.mjs} +1 -1
- package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-C3pjwy2m.mjs.map} +1 -1
- package/dist/{kg-entity-D5v7RCDi.mjs → kg-entity-DVzTsy6G.mjs} +1 -1
- package/dist/{kg-entity-D5v7RCDi.mjs.map → kg-entity-DVzTsy6G.mjs.map} +1 -1
- package/dist/kg-extraction-r5KkTmWY.mjs +524 -0
- package/dist/kg-extraction-r5KkTmWY.mjs.map +1 -0
- package/dist/{latent-ideas-DvWBRHsy.mjs → latent-ideas-dWVuI_dg.mjs} +2 -2
- package/dist/{latent-ideas-DvWBRHsy.mjs.map → latent-ideas-dWVuI_dg.mjs.map} +1 -1
- package/dist/{migrate-jokLenje.mjs → migrate-fLD6rAdO.mjs} +2 -2
- package/dist/{migrate-jokLenje.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
- package/dist/{neighborhood-u8ytjmWq.mjs → neighborhood-DixmpfPT.mjs} +1 -1
- package/dist/{neighborhood-u8ytjmWq.mjs.map → neighborhood-DixmpfPT.mjs.map} +1 -1
- package/dist/{note-context-CG2_e-0W.mjs → note-context-BY7rfGdl.mjs} +1 -1
- package/dist/{note-context-CG2_e-0W.mjs.map → note-context-BY7rfGdl.mjs.map} +1 -1
- package/dist/{postgres-BGERehmX.mjs → postgres-Cp25xZes.mjs} +1 -1
- package/dist/{postgres-BGERehmX.mjs.map → postgres-Cp25xZes.mjs.map} +1 -1
- package/dist/{query-feedback-CQSumXDy.mjs → query-feedback-C0hKUHVz.mjs} +1 -1
- package/dist/{query-feedback-CQSumXDy.mjs.map → query-feedback-C0hKUHVz.mjs.map} +1 -1
- package/dist/{sqlite-xUe94oUZ.mjs → sqlite-B3JHfJc2.mjs} +1 -1
- package/dist/{sqlite-xUe94oUZ.mjs.map → sqlite-B3JHfJc2.mjs.map} +1 -1
- package/dist/{state-BIlxNRUn.mjs → state-BHZAhLou.mjs} +1 -1
- package/dist/{state-BIlxNRUn.mjs.map → state-BHZAhLou.mjs.map} +1 -1
- package/dist/{themes-9jxFn3Rf.mjs → themes-DO8mUVwD.mjs} +1 -1
- package/dist/{themes-9jxFn3Rf.mjs.map → themes-DO8mUVwD.mjs.map} +1 -1
- package/dist/{tools-CF0lB5lW.mjs → tools-C1JNgya7.mjs} +14 -14
- package/dist/{tools-CF0lB5lW.mjs.map → tools-C1JNgya7.mjs.map} +1 -1
- package/dist/{trace-C2XrzssW.mjs → trace-TYCLMCkY.mjs} +1 -1
- package/dist/{trace-C2XrzssW.mjs.map → trace-TYCLMCkY.mjs.map} +1 -1
- package/dist/{utils-QSfKagcj.mjs → utils-BAxjW3j8.mjs} +2 -2
- package/dist/{utils-QSfKagcj.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
- package/dist/{vault-indexer-iHRjcy6e.mjs → vault-indexer-DsPC6wQf.mjs} +1 -1
- package/dist/{vault-indexer-iHRjcy6e.mjs.map → vault-indexer-DsPC6wQf.mjs.map} +1 -1
- package/dist/{daemon-w2fE3UMz.mjs → work-queue-worker-94INtUiz.mjs} +54 -1398
- package/dist/work-queue-worker-94INtUiz.mjs.map +1 -0
- package/dist/{zettelkasten-BdaMzTGQ.mjs → zettelkasten-B-94vghH.mjs} +3 -3
- package/dist/{zettelkasten-BdaMzTGQ.mjs.map → zettelkasten-B-94vghH.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +48 -0
- package/src/hooks/ts/stop/stop-hook.ts +40 -0
- package/dist/daemon-w2fE3UMz.mjs.map +0 -1
- package/dist/kg-extraction-Uvr9fTlz.mjs +0 -246
- package/dist/kg-extraction-Uvr9fTlz.mjs.map +0 -1
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-D5v7RCDi.mjs";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/daemon/templates/triple-extraction-prompt.ts
|
|
7
|
-
/**
|
|
8
|
-
* triple-extraction-prompt.ts — Prompt template for KG triple extraction.
|
|
9
|
-
*
|
|
10
|
-
* Used by the session-summary-worker to extract structured facts from
|
|
11
|
-
* a completed session summary and store them in the temporal knowledge graph.
|
|
12
|
-
*/
|
|
13
|
-
function buildTripleExtractionPrompt(params) {
|
|
14
|
-
return `Extract structured entities and relations from this coding session.
|
|
15
|
-
|
|
16
|
-
Output a single JSON object with two arrays: "entities" and "relations".
|
|
17
|
-
|
|
18
|
-
Entity types: project | person | concept | tool | file | version | decision | technology | organization
|
|
19
|
-
|
|
20
|
-
Rules:
|
|
21
|
-
- Be SPECIFIC: entity names must be concrete (e.g., "FSRS", "Glidr", "Matthias")
|
|
22
|
-
- Use snake_case relation verb phrases (e.g., "uses_algorithm", "decided_to", "shipped_version")
|
|
23
|
-
- Skip opinions, speculation, and "we should" statements
|
|
24
|
-
- Skip entities obvious from project metadata unless they have a meaningful relation
|
|
25
|
-
- Maximum 15 relations per session — pick the most important
|
|
26
|
-
- Each entity should have a brief description (1 sentence, what it is in this context)
|
|
27
|
-
- Each relation must reference entity names that appear in the entities array
|
|
28
|
-
|
|
29
|
-
Example output:
|
|
30
|
-
{
|
|
31
|
-
"entities": [
|
|
32
|
-
{"name": "Glidr", "type": "project", "description": "Flashcard app using FSRS spaced repetition"},
|
|
33
|
-
{"name": "FSRS", "type": "concept", "description": "Free Spaced Repetition Scheduler algorithm"},
|
|
34
|
-
{"name": "Matthias", "type": "person", "description": "Developer of Glidr and Quassl"},
|
|
35
|
-
{"name": "Quassl", "type": "project", "description": "iOS app being rewritten in Flutter"},
|
|
36
|
-
{"name": "Flutter", "type": "technology", "description": "Cross-platform mobile framework"}
|
|
37
|
-
],
|
|
38
|
-
"relations": [
|
|
39
|
-
{"source": "Glidr", "relation": "uses_algorithm", "target": "FSRS"},
|
|
40
|
-
{"source": "Glidr", "relation": "shipped_version", "target": "1.0.5"},
|
|
41
|
-
{"source": "Matthias", "relation": "decided_to_rewrite", "target": "Quassl"},
|
|
42
|
-
{"source": "Quassl", "relation": "migrating_to", "target": "Flutter"}
|
|
43
|
-
]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
PROJECT: ${params.projectSlug}
|
|
47
|
-
|
|
48
|
-
SESSION CONTENT:
|
|
49
|
-
${params.sessionContent}
|
|
50
|
-
|
|
51
|
-
GIT COMMITS:
|
|
52
|
-
${params.gitLog}
|
|
53
|
-
|
|
54
|
-
JSON object (entities + relations):`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
//#endregion
|
|
58
|
-
//#region src/memory/kg-extraction.ts
|
|
59
|
-
/**
|
|
60
|
-
* kg-extraction.ts — Shared KG triple extraction logic.
|
|
61
|
-
*
|
|
62
|
-
* Extracted from session-summary-worker.ts so both the worker and the
|
|
63
|
-
* CLI backfill (`pai kg backfill`) can use the same code path.
|
|
64
|
-
*
|
|
65
|
-
* Provides:
|
|
66
|
-
* - findClaudeBinary() — locate the claude CLI
|
|
67
|
-
* - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)
|
|
68
|
-
* - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres
|
|
69
|
-
*/
|
|
70
|
-
/**
|
|
71
|
-
* Find the `claude` CLI binary. Checks common installation locations first
|
|
72
|
-
* (launchd PATH is minimal so bare "claude" often won't resolve).
|
|
73
|
-
*/
|
|
74
|
-
function findClaudeBinary() {
|
|
75
|
-
const candidates = [
|
|
76
|
-
join(homedir(), ".local", "bin", "claude"),
|
|
77
|
-
join(homedir(), ".claude", "local", "claude"),
|
|
78
|
-
"/usr/local/bin/claude",
|
|
79
|
-
"/opt/homebrew/bin/claude"
|
|
80
|
-
];
|
|
81
|
-
for (const candidate of candidates) try {
|
|
82
|
-
if (existsSync(candidate)) return candidate;
|
|
83
|
-
} catch {}
|
|
84
|
-
return "claude";
|
|
85
|
-
}
|
|
86
|
-
const CLAUDE_TIMEOUT_MS = {
|
|
87
|
-
haiku: 6e4,
|
|
88
|
-
sonnet: 12e4,
|
|
89
|
-
opus: 3e5
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* Spawn the claude CLI with a prompt on stdin and return stdout.
|
|
93
|
-
*
|
|
94
|
-
* IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so
|
|
95
|
-
* the CLI uses the user's Max plan (free) instead of billing the API key.
|
|
96
|
-
*/
|
|
97
|
-
async function spawnClaude(prompt, model = "sonnet") {
|
|
98
|
-
const claudeBin = findClaudeBinary();
|
|
99
|
-
if (!claudeBin) {
|
|
100
|
-
process.stderr.write("[kg-extraction] claude CLI not found.\n");
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
const { spawn } = await import("node:child_process");
|
|
104
|
-
return new Promise((resolve) => {
|
|
105
|
-
let timer = null;
|
|
106
|
-
const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;
|
|
107
|
-
const child = spawn(claudeBin, [
|
|
108
|
-
"--model",
|
|
109
|
-
model,
|
|
110
|
-
"-p",
|
|
111
|
-
"--no-session-persistence"
|
|
112
|
-
], {
|
|
113
|
-
env: envWithoutApiKey,
|
|
114
|
-
stdio: [
|
|
115
|
-
"pipe",
|
|
116
|
-
"pipe",
|
|
117
|
-
"pipe"
|
|
118
|
-
]
|
|
119
|
-
});
|
|
120
|
-
let stdout = "";
|
|
121
|
-
let stderr = "";
|
|
122
|
-
child.stdout.on("data", (chunk) => {
|
|
123
|
-
stdout += chunk.toString();
|
|
124
|
-
});
|
|
125
|
-
child.stderr.on("data", (chunk) => {
|
|
126
|
-
stderr += chunk.toString();
|
|
127
|
-
});
|
|
128
|
-
child.on("error", (err) => {
|
|
129
|
-
if (timer) {
|
|
130
|
-
clearTimeout(timer);
|
|
131
|
-
timer = null;
|
|
132
|
-
}
|
|
133
|
-
process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\n`);
|
|
134
|
-
resolve(null);
|
|
135
|
-
});
|
|
136
|
-
child.on("close", (code) => {
|
|
137
|
-
if (timer) {
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
timer = null;
|
|
140
|
-
}
|
|
141
|
-
if (code !== 0) {
|
|
142
|
-
process.stderr.write(`[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\n`);
|
|
143
|
-
resolve(null);
|
|
144
|
-
} else resolve(stdout.trim() || null);
|
|
145
|
-
});
|
|
146
|
-
timer = setTimeout(() => {
|
|
147
|
-
process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\n`);
|
|
148
|
-
child.kill("SIGTERM");
|
|
149
|
-
resolve(null);
|
|
150
|
-
}, CLAUDE_TIMEOUT_MS[model] ?? 12e4);
|
|
151
|
-
child.stdin.write(prompt);
|
|
152
|
-
child.stdin.end();
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Extract structured KG triples from a session summary and store them in
|
|
157
|
-
* Postgres. Idempotent: if a (subject, predicate) pair already has the same
|
|
158
|
-
* object, no new row is added; if the object differs, the old triple is
|
|
159
|
-
* invalidated (valid_to = NOW()) and a new one is inserted.
|
|
160
|
-
*
|
|
161
|
-
* Best-effort: per-triple errors are caught and logged but never thrown.
|
|
162
|
-
* Returns a small stats object so callers can report progress.
|
|
163
|
-
*/
|
|
164
|
-
async function extractAndStoreTriples(pool, params) {
|
|
165
|
-
const stats = {
|
|
166
|
-
extracted: 0,
|
|
167
|
-
added: 0,
|
|
168
|
-
superseded: 0
|
|
169
|
-
};
|
|
170
|
-
const jsonOutput = await spawnClaude(buildTripleExtractionPrompt({
|
|
171
|
-
sessionContent: params.summaryText,
|
|
172
|
-
projectSlug: params.projectSlug,
|
|
173
|
-
gitLog: params.gitLog ?? ""
|
|
174
|
-
}), params.model ?? "sonnet");
|
|
175
|
-
if (!jsonOutput) return stats;
|
|
176
|
-
const cleaned = jsonOutput.replace(/^```json\s*/m, "").replace(/^```\s*/m, "").replace(/\s*```$/m, "").trim();
|
|
177
|
-
let triples;
|
|
178
|
-
try {
|
|
179
|
-
const parsed = JSON.parse(cleaned);
|
|
180
|
-
if (Array.isArray(parsed)) triples = parsed;
|
|
181
|
-
else if (parsed && typeof parsed === "object" && Array.isArray(parsed.relations)) {
|
|
182
|
-
const newFmt = parsed;
|
|
183
|
-
if (params.federationDb && Array.isArray(newFmt.entities)) {
|
|
184
|
-
const tenantId = params.tenantId ?? "default";
|
|
185
|
-
for (const entity of newFmt.entities) {
|
|
186
|
-
if (!entity.name) continue;
|
|
187
|
-
try {
|
|
188
|
-
upsertKgEntity(params.federationDb, {
|
|
189
|
-
name: entity.name,
|
|
190
|
-
type: entity.type ?? "unknown",
|
|
191
|
-
description: entity.description,
|
|
192
|
-
tenantId
|
|
193
|
-
});
|
|
194
|
-
} catch (entityErr) {
|
|
195
|
-
process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\n`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
triples = newFmt.relations.map((r) => ({
|
|
200
|
-
subject: r.source,
|
|
201
|
-
predicate: r.relation,
|
|
202
|
-
object: r.target
|
|
203
|
-
}));
|
|
204
|
-
} else {
|
|
205
|
-
process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\n`);
|
|
206
|
-
return stats;
|
|
207
|
-
}
|
|
208
|
-
} catch (e) {
|
|
209
|
-
process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\n`);
|
|
210
|
-
return stats;
|
|
211
|
-
}
|
|
212
|
-
if (!Array.isArray(triples)) return stats;
|
|
213
|
-
stats.extracted = triples.length;
|
|
214
|
-
for (const t of triples) {
|
|
215
|
-
if (!t.subject || !t.predicate || !t.object) continue;
|
|
216
|
-
try {
|
|
217
|
-
const existing = await kgQuery(pool, {
|
|
218
|
-
subject: t.subject,
|
|
219
|
-
predicate: t.predicate,
|
|
220
|
-
project_id: params.projectId ?? void 0
|
|
221
|
-
});
|
|
222
|
-
if (existing.find((e) => e.object === t.object && !e.valid_to)) continue;
|
|
223
|
-
const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);
|
|
224
|
-
if (supersedes) {
|
|
225
|
-
await kgInvalidate(pool, supersedes.id);
|
|
226
|
-
stats.superseded++;
|
|
227
|
-
}
|
|
228
|
-
await kgAdd(pool, {
|
|
229
|
-
subject: t.subject,
|
|
230
|
-
predicate: t.predicate,
|
|
231
|
-
object: t.object,
|
|
232
|
-
project_id: params.projectId ?? void 0,
|
|
233
|
-
source_session: params.sessionId,
|
|
234
|
-
confidence: "EXTRACTED"
|
|
235
|
-
});
|
|
236
|
-
stats.added++;
|
|
237
|
-
} catch (tripleErr) {
|
|
238
|
-
process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\n`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return stats;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
//#endregion
|
|
245
|
-
export { extractAndStoreTriples as t };
|
|
246
|
-
//# sourceMappingURL=kg-extraction-Uvr9fTlz.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"kg-extraction-Uvr9fTlz.mjs","names":[],"sources":["../src/daemon/templates/triple-extraction-prompt.ts","../src/memory/kg-extraction.ts"],"sourcesContent":["/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"Matthias\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"Matthias\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"Matthias\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nJSON object (entities + relations):`;\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000,\n sonnet: 120_000,\n opus: 300_000,\n};\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so\n * the CLI uses the user's Max plan (free) instead of billing the API key.\n */\nexport async function spawnClaude(\n prompt: string,\n model: \"haiku\" | \"sonnet\" | \"opus\" = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;\n const child = spawn(\n claudeBin,\n [\"--model\", model, \"-p\", \"--no-session-persistence\"],\n { env: envWithoutApiKey, stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n federationDb?: Database;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n pool: Pool,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n const cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into federation SQLite kg_entities table when db is available\n if (params.federationDb && Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n upsertKgEntity(params.federationDb, {\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\\n`);\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await kgQuery(pool, {\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await kgInvalidate(pool, supersedes.id);\n stats.superseded++;\n }\n\n await kgAdd(pool, {\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n"],"mappings":"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"}
|