pi-para 0.5.2
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/CHANGELOG.md +214 -0
- package/README.md +305 -0
- package/SETUP.md +171 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +461 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +405 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +786 -0
- package/dist/commands.js.map +1 -0
- package/dist/context.d.ts +61 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +289 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon.d.ts +49 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +226 -0
- package/dist/daemon.js.map +1 -0
- package/dist/frontmatter.d.ts +59 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +170 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/gepa/dataset.d.ts +36 -0
- package/dist/gepa/dataset.d.ts.map +1 -0
- package/dist/gepa/dataset.js +148 -0
- package/dist/gepa/dataset.js.map +1 -0
- package/dist/gepa/evaluator.d.ts +51 -0
- package/dist/gepa/evaluator.d.ts.map +1 -0
- package/dist/gepa/evaluator.js +317 -0
- package/dist/gepa/evaluator.js.map +1 -0
- package/dist/gepa/index.d.ts +66 -0
- package/dist/gepa/index.d.ts.map +1 -0
- package/dist/gepa/index.js +240 -0
- package/dist/gepa/index.js.map +1 -0
- package/dist/gepa/llm.d.ts +35 -0
- package/dist/gepa/llm.d.ts.map +1 -0
- package/dist/gepa/llm.js +268 -0
- package/dist/gepa/llm.js.map +1 -0
- package/dist/gepa/optimized/capture-prompt.txt +65 -0
- package/dist/gepa/optimizer.d.ts +29 -0
- package/dist/gepa/optimizer.d.ts.map +1 -0
- package/dist/gepa/optimizer.js +249 -0
- package/dist/gepa/optimizer.js.map +1 -0
- package/dist/gepa/targets.d.ts +39 -0
- package/dist/gepa/targets.d.ts.map +1 -0
- package/dist/gepa/targets.js +326 -0
- package/dist/gepa/targets.js.map +1 -0
- package/dist/gepa/types.d.ts +168 -0
- package/dist/gepa/types.d.ts.map +1 -0
- package/dist/gepa/types.js +124 -0
- package/dist/gepa/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +294 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +88 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +105 -0
- package/dist/ingest.js.map +1 -0
- package/dist/link-discovery.d.ts +40 -0
- package/dist/link-discovery.d.ts.map +1 -0
- package/dist/link-discovery.js +122 -0
- package/dist/link-discovery.js.map +1 -0
- package/dist/link-utils.d.ts +30 -0
- package/dist/link-utils.d.ts.map +1 -0
- package/dist/link-utils.js +145 -0
- package/dist/link-utils.js.map +1 -0
- package/dist/lint.d.ts +39 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +827 -0
- package/dist/lint.js.map +1 -0
- package/dist/maintainer.d.ts +24 -0
- package/dist/maintainer.d.ts.map +1 -0
- package/dist/maintainer.js +358 -0
- package/dist/maintainer.js.map +1 -0
- package/dist/processor.d.ts +14 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +361 -0
- package/dist/processor.js.map +1 -0
- package/dist/query.d.ts +69 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +107 -0
- package/dist/query.js.map +1 -0
- package/dist/raw.d.ts +57 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +219 -0
- package/dist/raw.js.map +1 -0
- package/dist/redact.d.ts +21 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +143 -0
- package/dist/redact.js.map +1 -0
- package/dist/scope.d.ts +54 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +161 -0
- package/dist/scope.js.map +1 -0
- package/dist/scripts/bulk-link-fix.d.ts +9 -0
- package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
- package/dist/scripts/bulk-link-fix.js +106 -0
- package/dist/scripts/bulk-link-fix.js.map +1 -0
- package/dist/session-tools.d.ts +33 -0
- package/dist/session-tools.d.ts.map +1 -0
- package/dist/session-tools.js +211 -0
- package/dist/session-tools.js.map +1 -0
- package/dist/state.d.ts +54 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +176 -0
- package/dist/state.js.map +1 -0
- package/dist/store.d.ts +56 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +227 -0
- package/dist/store.js.map +1 -0
- package/dist/summarize.d.ts +56 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +231 -0
- package/dist/summarize.js.map +1 -0
- package/dist/tag-registry.d.ts +56 -0
- package/dist/tag-registry.d.ts.map +1 -0
- package/dist/tag-registry.js +146 -0
- package/dist/tag-registry.js.map +1 -0
- package/dist/templates/prompts.d.ts +63 -0
- package/dist/templates/prompts.d.ts.map +1 -0
- package/dist/templates/prompts.js +260 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/tools.d.ts +26 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1033 -0
- package/dist/tools.js.map +1 -0
- package/dist/watcher.d.ts +25 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +84 -0
- package/dist/watcher.js.map +1 -0
- package/dist/webui/client/dist/index.html +1885 -0
- package/dist/webui/server.d.ts +31 -0
- package/dist/webui/server.d.ts.map +1 -0
- package/dist/webui/server.js +566 -0
- package/dist/webui/server.js.map +1 -0
- package/dist/wiki.d.ts +103 -0
- package/dist/wiki.d.ts.map +1 -0
- package/dist/wiki.js +428 -0
- package/dist/wiki.js.map +1 -0
- package/package.json +142 -0
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- package/scripts/gepa/dataset.py +192 -0
- package/scripts/gepa/lm_providers.py +472 -0
- package/scripts/gepa/metric.py +279 -0
- package/scripts/gepa/optimize.py +367 -0
- package/scripts/gepa/program.py +99 -0
- package/scripts/gepa/pyproject.toml +19 -0
- package/scripts/gepa/run-all.sh +53 -0
- package/scripts/gepa/uv.lock +2241 -0
- package/scripts/gepa/wiki_reader.py +113 -0
- package/skills/para/SKILL.md +77 -0
- package/skills/setup/SKILL.md +20 -0
- package/src/capture.ts +563 -0
- package/src/cli.ts +416 -0
- package/src/commands.ts +878 -0
- package/src/context.ts +390 -0
- package/src/daemon.ts +288 -0
- package/src/frontmatter.ts +219 -0
- package/src/gepa/index.ts +287 -0
- package/src/gepa/optimized/capture-prompt.txt +65 -0
- package/src/index.ts +384 -0
- package/src/ingest.ts +177 -0
- package/src/link-discovery.ts +170 -0
- package/src/link-utils.ts +169 -0
- package/src/lint.ts +1018 -0
- package/src/maintainer.ts +404 -0
- package/src/processor.ts +415 -0
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +198 -0
- package/src/python/rlm_capture.py +215 -0
- package/src/query.ts +148 -0
- package/src/raw.ts +276 -0
- package/src/redact.ts +159 -0
- package/src/scope.ts +214 -0
- package/src/scripts/bulk-link-fix.ts +126 -0
- package/src/session-tools.ts +260 -0
- package/src/state.ts +271 -0
- package/src/store.ts +288 -0
- package/src/summarize.ts +273 -0
- package/src/tag-registry.ts +157 -0
- package/src/templates/index.md +17 -0
- package/src/templates/prompts.ts +274 -0
- package/src/templates/schema.md +116 -0
- package/src/tools.ts +1293 -0
- package/src/watcher.ts +101 -0
- package/src/webui/client/dist/index.html +1885 -0
- package/src/webui/server.ts +765 -0
- package/src/wiki.ts +550 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session exploration tools for the Agent.
|
|
3
|
+
*
|
|
4
|
+
* These tools give the Agent the ability to explore a session file
|
|
5
|
+
* without loading the entire thing into the prompt — the TypeScript
|
|
6
|
+
* equivalent of RLM's REPL-based exploration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { Type, type Static } from "typebox";
|
|
11
|
+
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
12
|
+
|
|
13
|
+
// -- Session loading ---------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
interface SessionMessage {
|
|
16
|
+
role: string;
|
|
17
|
+
text: string;
|
|
18
|
+
toolCalls: string[];
|
|
19
|
+
charOffset: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LoadedSession {
|
|
23
|
+
messages: SessionMessage[];
|
|
24
|
+
serialized: string;
|
|
25
|
+
stats: {
|
|
26
|
+
totalMessages: number;
|
|
27
|
+
userMessages: number;
|
|
28
|
+
assistantMessages: number;
|
|
29
|
+
toolResults: number;
|
|
30
|
+
toolCalls: number;
|
|
31
|
+
totalChars: number;
|
|
32
|
+
compactionSummaries: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Load and pre-process a session JSONL file. */
|
|
37
|
+
export function loadSession(sessionPath: string): LoadedSession {
|
|
38
|
+
const raw = readFileSync(sessionPath, "utf-8");
|
|
39
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
40
|
+
|
|
41
|
+
const messages: SessionMessage[] = [];
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
let charOffset = 0;
|
|
44
|
+
let compactionSummaries = 0;
|
|
45
|
+
let toolCallCount = 0;
|
|
46
|
+
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
let entry: Record<string, unknown>;
|
|
49
|
+
try {
|
|
50
|
+
entry = JSON.parse(line);
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (entry.type === "compaction" && typeof entry.summary === "string") {
|
|
56
|
+
const text = `[Compaction Summary]:\n${entry.summary}`;
|
|
57
|
+
parts.push(text);
|
|
58
|
+
messages.push({ role: "compaction", text, toolCalls: [], charOffset });
|
|
59
|
+
charOffset += text.length + 2;
|
|
60
|
+
compactionSummaries++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (entry.type !== "message") continue;
|
|
65
|
+
const msg = entry.message as Record<string, unknown> | undefined;
|
|
66
|
+
if (!msg) continue;
|
|
67
|
+
const role = msg.role as string;
|
|
68
|
+
if (!["user", "assistant", "toolResult"].includes(role)) continue;
|
|
69
|
+
|
|
70
|
+
const content = msg.content;
|
|
71
|
+
let text = "";
|
|
72
|
+
const toolCalls: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (typeof content === "string") {
|
|
75
|
+
text = content;
|
|
76
|
+
} else if (Array.isArray(content)) {
|
|
77
|
+
const textParts: string[] = [];
|
|
78
|
+
for (const block of content) {
|
|
79
|
+
if (typeof block !== "object" || block === null) continue;
|
|
80
|
+
const b = block as Record<string, unknown>;
|
|
81
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
82
|
+
textParts.push(b.text);
|
|
83
|
+
} else if (b.type === "toolCall" && typeof b.name === "string") {
|
|
84
|
+
const argsStr = JSON.stringify(b.arguments ?? {});
|
|
85
|
+
toolCalls.push(
|
|
86
|
+
`${b.name}(${argsStr.length > 300 ? argsStr.slice(0, 300) + "..." : argsStr})`,
|
|
87
|
+
);
|
|
88
|
+
toolCallCount++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
text = textParts.join("\n");
|
|
92
|
+
if (toolCalls.length > 0) {
|
|
93
|
+
text += `\n[Tools]: ${toolCalls.join("; ")}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!text.trim()) continue;
|
|
98
|
+
|
|
99
|
+
// Truncate tool results
|
|
100
|
+
if (role === "toolResult" && text.length > 1000) {
|
|
101
|
+
text = text.slice(0, 1000) + "\n[truncated]";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const label = { user: "User", assistant: "Assistant", toolResult: "Tool" }[role] ?? role;
|
|
105
|
+
const formatted = `[${label}]: ${text}`;
|
|
106
|
+
parts.push(formatted);
|
|
107
|
+
messages.push({ role, text: formatted, toolCalls, charOffset });
|
|
108
|
+
charOffset += formatted.length + 2;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const serialized = parts.join("\n\n");
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
messages,
|
|
115
|
+
serialized,
|
|
116
|
+
stats: {
|
|
117
|
+
totalMessages: messages.length,
|
|
118
|
+
userMessages: messages.filter((m) => m.role === "user").length,
|
|
119
|
+
assistantMessages: messages.filter((m) => m.role === "assistant").length,
|
|
120
|
+
toolResults: messages.filter((m) => m.role === "toolResult").length,
|
|
121
|
+
toolCalls: toolCallCount,
|
|
122
|
+
totalChars: serialized.length,
|
|
123
|
+
compactionSummaries,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// -- Tool definitions --------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
const SliceParams = Type.Object({
|
|
131
|
+
start: Type.Number({ description: "Start character offset" }),
|
|
132
|
+
end: Type.Number({ description: "End character offset" }),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const SearchParams = Type.Object({
|
|
136
|
+
query: Type.String({ description: "Text to search for (case-insensitive)" }),
|
|
137
|
+
maxResults: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const StatsParams = Type.Object({});
|
|
141
|
+
|
|
142
|
+
/** Create session exploration tools bound to a loaded session. */
|
|
143
|
+
export function createSessionTools(session: LoadedSession): AgentTool[] {
|
|
144
|
+
const sessionSlice: AgentTool<typeof SliceParams> = {
|
|
145
|
+
name: "session_slice",
|
|
146
|
+
label: "Session Slice",
|
|
147
|
+
description: `Read a slice of the session text (${session.stats.totalChars} chars total). Use to explore specific sections.`,
|
|
148
|
+
parameters: SliceParams,
|
|
149
|
+
execute: async (_id, params) => {
|
|
150
|
+
const start = Math.max(0, params.start);
|
|
151
|
+
const end = Math.min(session.serialized.length, params.end);
|
|
152
|
+
const slice = session.serialized.slice(start, end);
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text",
|
|
157
|
+
text: `[chars ${start}-${end} of ${session.serialized.length}]\n\n${slice}`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
details: { start, end, length: slice.length },
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const sessionSearch: AgentTool<typeof SearchParams> = {
|
|
166
|
+
name: "session_search",
|
|
167
|
+
label: "Session Search",
|
|
168
|
+
description: "Search the session for a keyword. Returns matching sections with character offsets.",
|
|
169
|
+
parameters: SearchParams,
|
|
170
|
+
execute: async (_id, params) => {
|
|
171
|
+
const query = params.query.toLowerCase();
|
|
172
|
+
const maxResults = params.maxResults ?? 10;
|
|
173
|
+
const matches: Array<{ offset: number; context: string }> = [];
|
|
174
|
+
|
|
175
|
+
const text = session.serialized.toLowerCase();
|
|
176
|
+
let pos = 0;
|
|
177
|
+
while (matches.length < maxResults) {
|
|
178
|
+
const idx = text.indexOf(query, pos);
|
|
179
|
+
if (idx === -1) break;
|
|
180
|
+
|
|
181
|
+
// Extract context around match
|
|
182
|
+
const contextStart = Math.max(0, idx - 200);
|
|
183
|
+
const contextEnd = Math.min(session.serialized.length, idx + query.length + 200);
|
|
184
|
+
matches.push({
|
|
185
|
+
offset: idx,
|
|
186
|
+
context: session.serialized.slice(contextStart, contextEnd),
|
|
187
|
+
});
|
|
188
|
+
pos = idx + query.length;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (matches.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: `No matches for "${params.query}"` }],
|
|
194
|
+
details: { matchCount: 0 },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const resultText = matches
|
|
199
|
+
.map(
|
|
200
|
+
(m, i) =>
|
|
201
|
+
`Match ${i + 1} (offset ${m.offset}):\n${m.context}\n`,
|
|
202
|
+
)
|
|
203
|
+
.join("\n---\n");
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
content: [
|
|
207
|
+
{
|
|
208
|
+
type: "text",
|
|
209
|
+
text: `Found ${matches.length} match(es) for "${params.query}":\n\n${resultText}`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
details: { matchCount: matches.length },
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const sessionStats: AgentTool<typeof StatsParams> = {
|
|
218
|
+
name: "session_stats",
|
|
219
|
+
label: "Session Stats",
|
|
220
|
+
description: "Get session overview: message counts, size, topics.",
|
|
221
|
+
parameters: StatsParams,
|
|
222
|
+
execute: async () => {
|
|
223
|
+
const s = session.stats;
|
|
224
|
+
|
|
225
|
+
// Extract user message topics (first 100 chars of each, cap at 50)
|
|
226
|
+
const allUserMsgs = session.messages.filter((m) => m.role === "user");
|
|
227
|
+
const displayLimit = 50;
|
|
228
|
+
const userTopics = allUserMsgs
|
|
229
|
+
.slice(0, displayLimit)
|
|
230
|
+
.map((m) => m.text.replace(/^\[User\]: /, "").slice(0, 100))
|
|
231
|
+
.map((t, i) => ` ${i + 1}. ${t}`);
|
|
232
|
+
if (allUserMsgs.length > displayLimit) {
|
|
233
|
+
userTopics.push(` ... and ${allUserMsgs.length - displayLimit} more user messages. Use session_search to find specific topics.`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const sizeLabel = s.totalChars > 1_000_000 ? `${(s.totalChars / 1_000_000).toFixed(1)}M`
|
|
237
|
+
: s.totalChars > 1_000 ? `${(s.totalChars / 1_000).toFixed(0)}K`
|
|
238
|
+
: `${s.totalChars}`;
|
|
239
|
+
|
|
240
|
+
const text = [
|
|
241
|
+
`Session: ${sizeLabel} chars, ${s.totalMessages} messages`,
|
|
242
|
+
` User: ${s.userMessages}, Assistant: ${s.assistantMessages}, Tool results: ${s.toolResults}`,
|
|
243
|
+
` Tool calls: ${s.toolCalls}, Compaction summaries: ${s.compactionSummaries}`,
|
|
244
|
+
"",
|
|
245
|
+
"Strategy: Use session_search to find topics, then session_slice to read details.",
|
|
246
|
+
`Valid slice range: 0 to ${s.totalChars}`,
|
|
247
|
+
"",
|
|
248
|
+
"User messages (topics):",
|
|
249
|
+
...userTopics,
|
|
250
|
+
].join("\n");
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text }],
|
|
254
|
+
details: s,
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return [sessionSlice, sessionSearch, sessionStats];
|
|
260
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite state management for processed sessions.
|
|
3
|
+
* Tracks which sessions have been processed, their content hash,
|
|
4
|
+
* and what pages were created/updated.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
export interface PageSummary {
|
|
13
|
+
slug: string;
|
|
14
|
+
category: string;
|
|
15
|
+
scope: string[];
|
|
16
|
+
tags: string[];
|
|
17
|
+
firstParagraph: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ProcessedSession {
|
|
22
|
+
sessionPath: string;
|
|
23
|
+
contentHash: string;
|
|
24
|
+
processedAt: string;
|
|
25
|
+
scope: string;
|
|
26
|
+
pagesCreated: string[];
|
|
27
|
+
pagesUpdated: string[];
|
|
28
|
+
error: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class StateDB {
|
|
32
|
+
private db: Database.Database;
|
|
33
|
+
|
|
34
|
+
constructor(wikiDir: string) {
|
|
35
|
+
const dbPath = join(wikiDir, ".daemon.sqlite");
|
|
36
|
+
this.db = new Database(dbPath);
|
|
37
|
+
this.db.pragma("journal_mode = WAL");
|
|
38
|
+
this.migrate();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private migrate(): void {
|
|
42
|
+
this.db.exec(`
|
|
43
|
+
CREATE TABLE IF NOT EXISTS processed_sessions (
|
|
44
|
+
session_path TEXT PRIMARY KEY,
|
|
45
|
+
content_hash TEXT NOT NULL,
|
|
46
|
+
processed_at TEXT NOT NULL,
|
|
47
|
+
scope TEXT,
|
|
48
|
+
pages_created TEXT DEFAULT '[]',
|
|
49
|
+
pages_updated TEXT DEFAULT '[]',
|
|
50
|
+
error TEXT
|
|
51
|
+
);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS daemon_state (
|
|
53
|
+
key TEXT PRIMARY KEY,
|
|
54
|
+
value TEXT
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS page_summaries (
|
|
57
|
+
slug TEXT PRIMARY KEY,
|
|
58
|
+
category TEXT NOT NULL,
|
|
59
|
+
scope_json TEXT NOT NULL DEFAULT '[]',
|
|
60
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
61
|
+
first_paragraph TEXT NOT NULL DEFAULT '',
|
|
62
|
+
updated_at TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Check if a session file has already been processed with the same content. */
|
|
68
|
+
isProcessed(sessionPath: string): boolean {
|
|
69
|
+
const row = this.db
|
|
70
|
+
.prepare("SELECT content_hash FROM processed_sessions WHERE session_path = ?")
|
|
71
|
+
.get(sessionPath) as { content_hash: string } | undefined;
|
|
72
|
+
if (!row) return false;
|
|
73
|
+
|
|
74
|
+
const currentHash = hashFile(sessionPath);
|
|
75
|
+
return row.content_hash === currentHash;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Get the stored hash for a session (or null if not processed). */
|
|
79
|
+
getHash(sessionPath: string): string | null {
|
|
80
|
+
const row = this.db
|
|
81
|
+
.prepare("SELECT content_hash FROM processed_sessions WHERE session_path = ?")
|
|
82
|
+
.get(sessionPath) as { content_hash: string } | undefined;
|
|
83
|
+
return row?.content_hash ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Record a successfully processed session. */
|
|
87
|
+
recordSuccess(
|
|
88
|
+
sessionPath: string,
|
|
89
|
+
scope: string,
|
|
90
|
+
pagesCreated: string[],
|
|
91
|
+
pagesUpdated: string[],
|
|
92
|
+
): void {
|
|
93
|
+
const hash = hashFile(sessionPath);
|
|
94
|
+
this.db
|
|
95
|
+
.prepare(`
|
|
96
|
+
INSERT OR REPLACE INTO processed_sessions
|
|
97
|
+
(session_path, content_hash, processed_at, scope, pages_created, pages_updated, error)
|
|
98
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)
|
|
99
|
+
`)
|
|
100
|
+
.run(
|
|
101
|
+
sessionPath,
|
|
102
|
+
hash,
|
|
103
|
+
new Date().toISOString(),
|
|
104
|
+
scope,
|
|
105
|
+
JSON.stringify(pagesCreated),
|
|
106
|
+
JSON.stringify(pagesUpdated),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Record a failed processing attempt. */
|
|
111
|
+
recordFailure(sessionPath: string, scope: string, error: string): void {
|
|
112
|
+
const hash = hashFile(sessionPath);
|
|
113
|
+
this.db
|
|
114
|
+
.prepare(`
|
|
115
|
+
INSERT OR REPLACE INTO processed_sessions
|
|
116
|
+
(session_path, content_hash, processed_at, scope, pages_created, pages_updated, error)
|
|
117
|
+
VALUES (?, ?, ?, ?, '[]', '[]', ?)
|
|
118
|
+
`)
|
|
119
|
+
.run(sessionPath, hash, new Date().toISOString(), scope, error);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Get all failed sessions for retry. */
|
|
123
|
+
getFailed(): ProcessedSession[] {
|
|
124
|
+
const rows = this.db
|
|
125
|
+
.prepare("SELECT * FROM processed_sessions WHERE error IS NOT NULL")
|
|
126
|
+
.all() as Array<{
|
|
127
|
+
session_path: string;
|
|
128
|
+
content_hash: string;
|
|
129
|
+
processed_at: string;
|
|
130
|
+
scope: string;
|
|
131
|
+
pages_created: string;
|
|
132
|
+
pages_updated: string;
|
|
133
|
+
error: string;
|
|
134
|
+
}>;
|
|
135
|
+
return rows.map(rowToSession);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Get processing history, optionally filtered by scope. */
|
|
139
|
+
getHistory(scope?: string, limit: number = 20): ProcessedSession[] {
|
|
140
|
+
const sql = scope
|
|
141
|
+
? "SELECT * FROM processed_sessions WHERE scope = ? ORDER BY processed_at DESC LIMIT ?"
|
|
142
|
+
: "SELECT * FROM processed_sessions ORDER BY processed_at DESC LIMIT ?";
|
|
143
|
+
const params = scope ? [scope, limit] : [limit];
|
|
144
|
+
const rows = this.db.prepare(sql).all(...params) as Array<{
|
|
145
|
+
session_path: string;
|
|
146
|
+
content_hash: string;
|
|
147
|
+
processed_at: string;
|
|
148
|
+
scope: string;
|
|
149
|
+
pages_created: string;
|
|
150
|
+
pages_updated: string;
|
|
151
|
+
error: string | null;
|
|
152
|
+
}>;
|
|
153
|
+
return rows.map(rowToSession);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Get/set daemon state values. */
|
|
157
|
+
getState(key: string): string | null {
|
|
158
|
+
const row = this.db
|
|
159
|
+
.prepare("SELECT value FROM daemon_state WHERE key = ?")
|
|
160
|
+
.get(key) as { value: string } | undefined;
|
|
161
|
+
return row?.value ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setState(key: string, value: string): void {
|
|
165
|
+
this.db
|
|
166
|
+
.prepare("INSERT OR REPLACE INTO daemon_state (key, value) VALUES (?, ?)")
|
|
167
|
+
.run(key, value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Upsert a page summary in the cache. */
|
|
171
|
+
upsertPageSummary(
|
|
172
|
+
slug: string,
|
|
173
|
+
category: string,
|
|
174
|
+
scope: string[],
|
|
175
|
+
tags: string[],
|
|
176
|
+
firstParagraph: string,
|
|
177
|
+
updatedAt: string,
|
|
178
|
+
): void {
|
|
179
|
+
this.db
|
|
180
|
+
.prepare(`
|
|
181
|
+
INSERT OR REPLACE INTO page_summaries
|
|
182
|
+
(slug, category, scope_json, tags_json, first_paragraph, updated_at)
|
|
183
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
184
|
+
`)
|
|
185
|
+
.run(
|
|
186
|
+
slug,
|
|
187
|
+
category,
|
|
188
|
+
JSON.stringify(scope),
|
|
189
|
+
JSON.stringify(tags),
|
|
190
|
+
firstParagraph,
|
|
191
|
+
updatedAt,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Get all page summaries, optionally filtered by scope. */
|
|
196
|
+
getPageSummaries(scope?: { name: string; include: string[]; exclude: string[] }): PageSummary[] {
|
|
197
|
+
const rows = this.db
|
|
198
|
+
.prepare("SELECT * FROM page_summaries ORDER BY updated_at DESC")
|
|
199
|
+
.all() as Array<{
|
|
200
|
+
slug: string;
|
|
201
|
+
category: string;
|
|
202
|
+
scope_json: string;
|
|
203
|
+
tags_json: string;
|
|
204
|
+
first_paragraph: string;
|
|
205
|
+
updated_at: string;
|
|
206
|
+
}>;
|
|
207
|
+
|
|
208
|
+
const summaries = rows.map((row) => ({
|
|
209
|
+
slug: row.slug,
|
|
210
|
+
category: row.category,
|
|
211
|
+
scope: JSON.parse(row.scope_json || "[]") as string[],
|
|
212
|
+
tags: JSON.parse(row.tags_json || "[]") as string[],
|
|
213
|
+
firstParagraph: row.first_paragraph,
|
|
214
|
+
updatedAt: row.updated_at,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
if (!scope) return summaries;
|
|
218
|
+
|
|
219
|
+
// Filter by scope: include global + matching, exclude excluded
|
|
220
|
+
return summaries.filter((s) => {
|
|
221
|
+
if (
|
|
222
|
+
scope.exclude.length > 0 &&
|
|
223
|
+
s.scope.some((tag) => scope.exclude.includes(tag))
|
|
224
|
+
) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
if (s.scope.includes("global")) return true;
|
|
228
|
+
return s.scope.some((tag) => scope.include.includes(tag));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Delete a page summary from the cache. */
|
|
233
|
+
deletePageSummary(slug: string): void {
|
|
234
|
+
this.db
|
|
235
|
+
.prepare("DELETE FROM page_summaries WHERE slug = ?")
|
|
236
|
+
.run(slug);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
close(): void {
|
|
240
|
+
this.db.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hashFile(filePath: string): string {
|
|
245
|
+
try {
|
|
246
|
+
const content = readFileSync(filePath);
|
|
247
|
+
return createHash("sha256").update(content).digest("hex");
|
|
248
|
+
} catch {
|
|
249
|
+
return "missing";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function rowToSession(row: {
|
|
254
|
+
session_path: string;
|
|
255
|
+
content_hash: string;
|
|
256
|
+
processed_at: string;
|
|
257
|
+
scope: string;
|
|
258
|
+
pages_created: string;
|
|
259
|
+
pages_updated: string;
|
|
260
|
+
error: string | null;
|
|
261
|
+
}): ProcessedSession {
|
|
262
|
+
return {
|
|
263
|
+
sessionPath: row.session_path,
|
|
264
|
+
contentHash: row.content_hash,
|
|
265
|
+
processedAt: row.processed_at,
|
|
266
|
+
scope: row.scope ?? "",
|
|
267
|
+
pagesCreated: JSON.parse(row.pages_created || "[]"),
|
|
268
|
+
pagesUpdated: JSON.parse(row.pages_updated || "[]"),
|
|
269
|
+
error: row.error,
|
|
270
|
+
};
|
|
271
|
+
}
|