@zosmaai/pi-llm-wiki 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import { appendEvent } from "./metadata.js";
5
+ import { type VaultPaths, exec, fmtDate, nextSourceId, readText, writeJson } from "./utils.js";
6
+
7
+ /**
8
+ * Source packet capture and management.
9
+ *
10
+ * Each source is stored as a structured packet:
11
+ * raw/sources/SRC-YYYY-MM-DD-NNN/
12
+ * manifest.json — capture metadata
13
+ * original/ — original artifact (if file/URL)
14
+ * extracted.md — normalized markdown text
15
+ * attachments/ — downloaded images, PDFs, etc.
16
+ */
17
+
18
+ export interface CaptureResult {
19
+ sourceId: string;
20
+ packetPath: string;
21
+ sourcePagePath: string;
22
+ extracted: string;
23
+ }
24
+
25
+ /** Capture a URL into a source packet. */
26
+ export async function captureUrl(
27
+ pi: ExtensionAPI,
28
+ paths: VaultPaths,
29
+ url: string,
30
+ signal?: AbortSignal,
31
+ ): Promise<CaptureResult> {
32
+ const sourceId = nextSourceId(paths);
33
+ const packetPath = join(paths.rawSources, sourceId);
34
+ mkdirSync(packetPath, { recursive: true });
35
+ mkdirSync(join(packetPath, "original"), { recursive: true });
36
+ mkdirSync(join(packetPath, "attachments"), { recursive: true });
37
+
38
+ // Try to fetch and extract content
39
+ let extracted = "";
40
+ let title = url;
41
+
42
+ // Try markitdown first
43
+ const markitdown = await exec(
44
+ pi,
45
+ "sh",
46
+ ["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
47
+ { signal },
48
+ );
49
+
50
+ if (markitdown.stdout.trim() === "yes") {
51
+ try {
52
+ const mdResult = await exec(
53
+ pi,
54
+ "sh",
55
+ ["-c", `uvx --from 'markitdown[pdf]' markitdown "${url}" 2>/dev/null || echo ""`],
56
+ { signal, timeout: 30_000 },
57
+ );
58
+ if (mdResult.stdout.trim()) {
59
+ extracted = mdResult.stdout;
60
+ // Try to extract title from first h1
61
+ const h1Match = extracted.match(/^#\s+(.+)$/m);
62
+ if (h1Match) title = h1Match[1].trim();
63
+ }
64
+ } catch {
65
+ // markitdown failed, fall through
66
+ }
67
+ }
68
+
69
+ // Fallback: try fetch_content equivalent via curl
70
+ if (!extracted) {
71
+ try {
72
+ const curlResult = await exec(pi, "curl", ["-sL", "--max-time", "30", url], {
73
+ signal,
74
+ timeout: 35_000,
75
+ });
76
+ if (curlResult.stdout) {
77
+ extracted = curlResult.stdout;
78
+ // Try to extract title from HTML
79
+ const titleMatch = extracted.match(/<title>([^<]*)<\/title>/i);
80
+ if (titleMatch) title = titleMatch[1].trim();
81
+ }
82
+ } catch {
83
+ // curl failed too
84
+ }
85
+ }
86
+
87
+ // Write extracted text
88
+ writeFileSync(
89
+ join(packetPath, "extracted.md"),
90
+ extracted || `_Content could not be extracted from ${url}_\n`,
91
+ "utf-8",
92
+ );
93
+
94
+ // Write manifest
95
+ const manifest = {
96
+ id: sourceId,
97
+ title,
98
+ url,
99
+ captured: fmtDate(),
100
+ format: "web",
101
+ packet_version: "1.0",
102
+ };
103
+ writeJson(join(packetPath, "manifest.json"), manifest);
104
+
105
+ // Create skeleton source page in wiki
106
+ const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
107
+ const sourcePageContent = buildSourcePageSkeleton(manifest, extracted);
108
+ writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
109
+
110
+ // Log event
111
+ appendEvent(paths, { kind: "capture", source_id: sourceId, url, format: "web" });
112
+
113
+ return { sourceId, packetPath, sourcePagePath, extracted };
114
+ }
115
+
116
+ /** Capture a local file into a source packet. */
117
+ export async function captureFile(
118
+ pi: ExtensionAPI,
119
+ paths: VaultPaths,
120
+ filePath: string,
121
+ signal?: AbortSignal,
122
+ ): Promise<CaptureResult> {
123
+ const sourceId = nextSourceId(paths);
124
+ const packetPath = join(paths.rawSources, sourceId);
125
+ mkdirSync(packetPath, { recursive: true });
126
+ mkdirSync(join(packetPath, "original"), { recursive: true });
127
+ mkdirSync(join(packetPath, "attachments"), { recursive: true });
128
+
129
+ const content = readText(filePath);
130
+ const fileName = filePath.split("/").pop() || "unknown";
131
+
132
+ // Try markitdown for PDFs
133
+ let extracted = content;
134
+ if (filePath.toLowerCase().endsWith(".pdf")) {
135
+ const markitdown = await exec(
136
+ pi,
137
+ "sh",
138
+ ["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
139
+ { signal },
140
+ );
141
+
142
+ if (markitdown.stdout.trim() === "yes") {
143
+ try {
144
+ const mdResult = await exec(
145
+ pi,
146
+ "sh",
147
+ ["-c", `uvx --from 'markitdown[pdf]' markitdown "${filePath}" 2>/dev/null || echo ""`],
148
+ { signal, timeout: 30_000 },
149
+ );
150
+ if (mdResult.stdout.trim()) extracted = mdResult.stdout;
151
+ } catch {
152
+ // fallback to original
153
+ }
154
+ }
155
+ }
156
+
157
+ // Copy original to packet
158
+ try {
159
+ await exec(pi, "cp", [filePath, join(packetPath, "original", fileName)], { signal });
160
+ } catch {
161
+ // If cp fails, just write the content
162
+ writeFileSync(join(packetPath, "original", fileName), content, "utf-8");
163
+ }
164
+
165
+ // Write extracted text
166
+ writeFileSync(join(packetPath, "extracted.md"), extracted, "utf-8");
167
+
168
+ // Write manifest
169
+ const manifest = {
170
+ id: sourceId,
171
+ title: fileName,
172
+ file_path: filePath,
173
+ captured: fmtDate(),
174
+ format: guessFormat(filePath),
175
+ packet_version: "1.0",
176
+ };
177
+ writeJson(join(packetPath, "manifest.json"), manifest);
178
+
179
+ // Create skeleton source page
180
+ const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
181
+ const sourcePageContent = buildSourcePageSkeleton(manifest, extracted);
182
+ writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
183
+
184
+ // Log event
185
+ appendEvent(paths, {
186
+ kind: "capture",
187
+ source_id: sourceId,
188
+ file_path: filePath,
189
+ format: manifest.format,
190
+ });
191
+
192
+ return { sourceId, packetPath, sourcePagePath, extracted };
193
+ }
194
+
195
+ /** Capture pasted text into a source packet. */
196
+ export function captureText(paths: VaultPaths, text: string, title?: string): CaptureResult {
197
+ const sourceId = nextSourceId(paths);
198
+ const packetPath = join(paths.rawSources, sourceId);
199
+ mkdirSync(packetPath, { recursive: true });
200
+ mkdirSync(join(packetPath, "attachments"), { recursive: true });
201
+
202
+ // Write extracted text
203
+ writeFileSync(join(packetPath, "extracted.md"), text, "utf-8");
204
+
205
+ // Write manifest
206
+ const manifest = {
207
+ id: sourceId,
208
+ title: title || `Pasted text — ${fmtDate()}`,
209
+ captured: fmtDate(),
210
+ format: "text",
211
+ packet_version: "1.0",
212
+ };
213
+ writeJson(join(packetPath, "manifest.json"), manifest);
214
+
215
+ // Create skeleton source page
216
+ const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
217
+ const sourcePageContent = buildSourcePageSkeleton(manifest, text);
218
+ writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
219
+
220
+ // Log event
221
+ appendEvent(paths, { kind: "capture", source_id: sourceId, format: "text" });
222
+
223
+ return { sourceId, packetPath, sourcePagePath, extracted: text };
224
+ }
225
+
226
+ /** Build a skeleton source page from manifest and extracted text. */
227
+ function buildSourcePageSkeleton(manifest: Record<string, unknown>, extracted: string): string {
228
+ const id = String(manifest.id);
229
+ const title = String(manifest.title || id);
230
+ const url = manifest.url ? `\n> _Original: ${manifest.url}_` : "";
231
+ const format = String(manifest.format || "unknown");
232
+ const captured = String(manifest.captured || fmtDate());
233
+
234
+ // Generate a brief auto-summary (first 500 chars)
235
+ const preview = extracted
236
+ .replace(/[#*_`]/g, "")
237
+ .replace(/\s+/g, " ")
238
+ .trim()
239
+ .slice(0, 500);
240
+
241
+ return `---
242
+ type: source
243
+ format: ${format}
244
+ source_id: ${id}
245
+ raw_path: raw/sources/${id}/extracted.md
246
+ captured: ${captured}
247
+ status: skeleton
248
+ ---
249
+
250
+ # ${title}${url}
251
+
252
+ ## Summary
253
+
254
+ [LLM: Replace with 2-3 paragraph summary of key content]
255
+
256
+ > _Auto-preview: ${preview}${extracted.length > 500 ? "..." : ""}_
257
+
258
+ ## Key Takeaways
259
+
260
+ - [LLM: Most important point]
261
+ - [LLM: Second important point]
262
+ - [LLM: Third important point]
263
+
264
+ ## Entities Mentioned
265
+
266
+ - [[entity-name]]
267
+
268
+ ## Concepts Mentioned
269
+
270
+ - [[concept-name]]
271
+
272
+ ## Notable Quotes
273
+
274
+ > [LLM: Important quote] — attribution
275
+
276
+ ## Source Packet
277
+
278
+ - **ID:** \`[[sources/${id}]]\`
279
+ - **Extracted:** [raw/sources/${id}/extracted.md](../raw/sources/${id}/extracted.md)
280
+ - **Manifest:** [raw/sources/${id}/manifest.json](../raw/sources/${id}/manifest.json)
281
+ `;
282
+ }
283
+
284
+ function guessFormat(filePath: string): string {
285
+ const lower = filePath.toLowerCase();
286
+ if (lower.endsWith(".pdf")) return "pdf";
287
+ if (lower.endsWith(".md")) return "markdown";
288
+ if (lower.endsWith(".txt")) return "text";
289
+ if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
290
+ if (lower.endsWith(".docx")) return "docx";
291
+ return "file";
292
+ }