@zosmaai/pi-llm-wiki 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +254 -34
- package/extensions/llm-wiki/lib/source-extractors.ts +369 -0
- package/extensions/llm-wiki/lib/source-packet.ts +156 -300
- package/package.json +25 -3
- package/.coderabbit.yaml +0 -43
- package/.github/codeql/codeql-config.yml +0 -12
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/codeql.yml +0 -39
- package/.github/workflows/release.yml +0 -86
- package/AGENTS.md +0 -57
- package/CONTRIBUTING.md +0 -43
- package/biome.json +0 -30
- package/scripts/release.js +0 -72
- package/test/llm-wiki.test.ts +0 -654
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -16
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { extname, join } from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import { appendEvent } from "./metadata.js";
|
|
5
|
+
import { type ExtractedContent, extractUrlContent, fileExtractorFor } from "./source-extractors.js";
|
|
5
6
|
import { type VaultPaths, exec, fmtDate, nextSourceId, readText, writeJson } from "./utils.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -22,295 +23,206 @@ export interface CaptureResult {
|
|
|
22
23
|
extracted: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function markitdownTimeoutMs(): number {
|
|
29
|
-
return positiveIntegerFromEnv("WIKI_MARKITDOWN_TIMEOUT_MS", DEFAULT_MARKITDOWN_TIMEOUT_MS);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function positiveIntegerFromEnv(name: string, fallback: number): number {
|
|
33
|
-
const raw = process.env[name];
|
|
34
|
-
if (!raw) return fallback;
|
|
35
|
-
const parsed = Number.parseInt(raw, 10);
|
|
36
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isPdfUrl(url: string): boolean {
|
|
40
|
-
try {
|
|
41
|
-
return new URL(url).pathname.toLowerCase().endsWith(".pdf");
|
|
42
|
-
} catch {
|
|
43
|
-
return url.toLowerCase().split(/[?#]/, 1)[0].endsWith(".pdf");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function looksLikePdf(content: string): boolean {
|
|
48
|
-
return content.trimStart().startsWith("%PDF-");
|
|
26
|
+
interface SourcePacket {
|
|
27
|
+
sourceId: string;
|
|
28
|
+
packetPath: string;
|
|
49
29
|
}
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
interface CaptureSource {
|
|
32
|
+
needsOriginalDir: boolean;
|
|
33
|
+
fallbackText: string;
|
|
34
|
+
preserveOriginal?(packetPath: string): Promise<void>;
|
|
35
|
+
extract(): Promise<ExtractedContent> | ExtractedContent;
|
|
36
|
+
manifest(content: ExtractedContent): Record<string, unknown>;
|
|
37
|
+
event(content: ExtractedContent): Record<string, unknown>;
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
const URL_ORIGINAL_EXTENSIONS = new Set([".html", ".htm", ".md", ".pdf", ".txt", ".xml", ".json"]);
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const parsed = new URL(url);
|
|
60
|
-
const ext = extname(parsed.pathname).toLowerCase();
|
|
61
|
-
if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
|
|
62
|
-
} catch {
|
|
63
|
-
const path = url.split(/[?#]/, 1)[0] ?? "";
|
|
64
|
-
const ext = extname(path).toLowerCase();
|
|
65
|
-
if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return "source.html";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function preserveUrlOriginal(
|
|
42
|
+
/** Capture a URL into a source packet. */
|
|
43
|
+
export async function captureUrl(
|
|
72
44
|
pi: ExtensionAPI,
|
|
73
|
-
|
|
45
|
+
paths: VaultPaths,
|
|
74
46
|
url: string,
|
|
75
47
|
signal?: AbortSignal,
|
|
76
|
-
): Promise<
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await exec(pi, "curl", ["-sL", "--max-time", "30", "-o", originalPath, url], {
|
|
80
|
-
signal,
|
|
81
|
-
timeout: 35_000,
|
|
82
|
-
});
|
|
83
|
-
} catch {
|
|
84
|
-
// Preserve best-effort extraction behavior even when the original artifact cannot be saved.
|
|
85
|
-
}
|
|
48
|
+
): Promise<CaptureResult> {
|
|
49
|
+
return captureSource(paths, urlCaptureSource(pi, url, signal));
|
|
86
50
|
}
|
|
87
51
|
|
|
88
|
-
/** Capture a
|
|
89
|
-
export async function
|
|
52
|
+
/** Capture a local file into a source packet. */
|
|
53
|
+
export async function captureFile(
|
|
90
54
|
pi: ExtensionAPI,
|
|
91
55
|
paths: VaultPaths,
|
|
92
|
-
|
|
56
|
+
filePath: string,
|
|
93
57
|
signal?: AbortSignal,
|
|
94
58
|
): Promise<CaptureResult> {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
mkdirSync(packetPath, { recursive: true });
|
|
98
|
-
mkdirSync(join(packetPath, "original"), { recursive: true });
|
|
99
|
-
mkdirSync(join(packetPath, "attachments"), { recursive: true });
|
|
59
|
+
return captureSource(paths, fileCaptureSource(pi, filePath, signal));
|
|
60
|
+
}
|
|
100
61
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
let title = url;
|
|
106
|
-
const isPdf = isPdfUrl(url);
|
|
107
|
-
|
|
108
|
-
// Try MarkItDown first.
|
|
109
|
-
const markitdown = await exec(
|
|
110
|
-
pi,
|
|
111
|
-
"sh",
|
|
112
|
-
["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
|
|
113
|
-
{ signal },
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
if (markitdown.stdout.trim() === "yes") {
|
|
117
|
-
try {
|
|
118
|
-
const mdResult = await exec(
|
|
119
|
-
pi,
|
|
120
|
-
"sh",
|
|
121
|
-
["-c", `uvx --from 'markitdown[pdf]' markitdown "${url}" 2>/dev/null || echo ""`],
|
|
122
|
-
{ signal, timeout: markitdownTimeoutMs() },
|
|
123
|
-
);
|
|
124
|
-
if (mdResult.stdout.trim()) {
|
|
125
|
-
extracted = mdResult.stdout;
|
|
126
|
-
// Try to extract title from first h1
|
|
127
|
-
const h1Match = extracted.match(/^#\s+(.+)$/m);
|
|
128
|
-
if (h1Match) title = h1Match[1].trim();
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// markitdown failed, fall through
|
|
132
|
-
}
|
|
133
|
-
}
|
|
62
|
+
/** Capture pasted text into a source packet. */
|
|
63
|
+
export function captureText(paths: VaultPaths, text: string, title?: string): CaptureResult {
|
|
64
|
+
return captureSourceSync(paths, textCaptureSource(text, title));
|
|
65
|
+
}
|
|
134
66
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
const curlResult = await exec(
|
|
143
|
-
pi,
|
|
144
|
-
"curl",
|
|
145
|
-
["-sL", "--max-time", String(DEFAULT_CURL_TIMEOUT_SECONDS), url],
|
|
146
|
-
{
|
|
147
|
-
signal,
|
|
148
|
-
timeout: (DEFAULT_CURL_TIMEOUT_SECONDS + 5) * 1_000,
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
if (curlResult.stdout) {
|
|
152
|
-
if (looksLikePdf(curlResult.stdout)) {
|
|
153
|
-
extracted = pdfExtractionFailureMessage(url);
|
|
154
|
-
} else {
|
|
155
|
-
extracted = curlResult.stdout;
|
|
156
|
-
// Try to extract title from HTML
|
|
157
|
-
const titleMatch = extracted.match(/<title>([^<]*)<\/title>/i);
|
|
158
|
-
if (titleMatch) title = titleMatch[1].trim();
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// curl failed too
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
67
|
+
async function captureSource(paths: VaultPaths, source: CaptureSource): Promise<CaptureResult> {
|
|
68
|
+
const packet = createSourcePacket(paths, source.needsOriginalDir);
|
|
69
|
+
await source.preserveOriginal?.(packet.packetPath);
|
|
70
|
+
const content = await source.extract();
|
|
71
|
+
return finalizeCapture(paths, packet, source, content);
|
|
72
|
+
}
|
|
166
73
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
);
|
|
74
|
+
function captureSourceSync(paths: VaultPaths, source: CaptureSource): CaptureResult {
|
|
75
|
+
const packet = createSourcePacket(paths, source.needsOriginalDir);
|
|
76
|
+
const content = source.extract() as ExtractedContent;
|
|
77
|
+
return finalizeCapture(paths, packet, source, content);
|
|
78
|
+
}
|
|
173
79
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
url,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
80
|
+
function urlCaptureSource(pi: ExtensionAPI, url: string, signal?: AbortSignal): CaptureSource {
|
|
81
|
+
return {
|
|
82
|
+
needsOriginalDir: true,
|
|
83
|
+
fallbackText: contentExtractionFailureMessage(url),
|
|
84
|
+
preserveOriginal: (packetPath) => preserveUrlOriginal(pi, packetPath, url, signal),
|
|
85
|
+
extract: () => extractUrlContent(pi, url, signal),
|
|
86
|
+
manifest: (content) => ({
|
|
87
|
+
title: content.title || url,
|
|
88
|
+
url,
|
|
89
|
+
format: "web",
|
|
90
|
+
}),
|
|
91
|
+
event: () => ({ url, format: "web" }),
|
|
182
92
|
};
|
|
183
|
-
writeJson(join(packetPath, "manifest.json"), manifest);
|
|
184
|
-
|
|
185
|
-
// Create skeleton source page in wiki
|
|
186
|
-
const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
|
|
187
|
-
const sourcePageContent = buildSourcePageSkeleton(manifest, extracted);
|
|
188
|
-
writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
|
|
189
|
-
|
|
190
|
-
// Log event
|
|
191
|
-
appendEvent(paths, { kind: "capture", source_id: sourceId, url, format: "web" });
|
|
192
|
-
|
|
193
|
-
return { sourceId, packetPath, sourcePagePath, extracted };
|
|
194
93
|
}
|
|
195
94
|
|
|
196
|
-
|
|
197
|
-
export async function captureFile(
|
|
95
|
+
function fileCaptureSource(
|
|
198
96
|
pi: ExtensionAPI,
|
|
199
|
-
paths: VaultPaths,
|
|
200
97
|
filePath: string,
|
|
201
98
|
signal?: AbortSignal,
|
|
202
|
-
):
|
|
99
|
+
): CaptureSource {
|
|
100
|
+
const fileName = filePath.split("/").pop() || "unknown";
|
|
101
|
+
const extractor = fileExtractorFor(filePath);
|
|
102
|
+
const content = extractor.shouldReadText ? readText(filePath) : "";
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
needsOriginalDir: true,
|
|
106
|
+
fallbackText: "",
|
|
107
|
+
preserveOriginal: (packetPath) =>
|
|
108
|
+
preserveFileOriginal(pi, packetPath, filePath, fileName, content, signal),
|
|
109
|
+
extract: async () => ({
|
|
110
|
+
extracted: await extractor.extract({ pi, filePath, content, signal }),
|
|
111
|
+
}),
|
|
112
|
+
manifest: () => ({
|
|
113
|
+
title: fileName,
|
|
114
|
+
file_path: filePath,
|
|
115
|
+
format: extractor.format,
|
|
116
|
+
}),
|
|
117
|
+
event: () => ({ file_path: filePath, format: extractor.format }),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function textCaptureSource(text: string, title?: string): CaptureSource {
|
|
122
|
+
return {
|
|
123
|
+
needsOriginalDir: false,
|
|
124
|
+
fallbackText: "",
|
|
125
|
+
extract: () => ({ extracted: text }),
|
|
126
|
+
manifest: () => ({
|
|
127
|
+
title: title || `Pasted text — ${fmtDate()}`,
|
|
128
|
+
format: "text",
|
|
129
|
+
}),
|
|
130
|
+
event: () => ({ format: "text" }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createSourcePacket(paths: VaultPaths, needsOriginalDir: boolean): SourcePacket {
|
|
203
135
|
const sourceId = nextSourceId(paths);
|
|
204
136
|
const packetPath = join(paths.rawSources, sourceId);
|
|
205
137
|
mkdirSync(packetPath, { recursive: true });
|
|
206
|
-
mkdirSync(join(packetPath, "original"), { recursive: true });
|
|
207
138
|
mkdirSync(join(packetPath, "attachments"), { recursive: true });
|
|
139
|
+
if (needsOriginalDir) mkdirSync(join(packetPath, "original"), { recursive: true });
|
|
140
|
+
return { sourceId, packetPath };
|
|
141
|
+
}
|
|
208
142
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Convert XML to markdown
|
|
218
|
-
if (isXml && content) {
|
|
219
|
-
extracted = xmlToMarkdown(content);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Try MarkItDown for PDFs.
|
|
223
|
-
if (isPdf) {
|
|
224
|
-
const markitdown = await exec(
|
|
225
|
-
pi,
|
|
226
|
-
"sh",
|
|
227
|
-
["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
|
|
228
|
-
{ signal },
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
if (markitdown.stdout.trim() === "yes") {
|
|
232
|
-
try {
|
|
233
|
-
const mdResult = await exec(
|
|
234
|
-
pi,
|
|
235
|
-
"sh",
|
|
236
|
-
["-c", `uvx --from 'markitdown[pdf]' markitdown "${filePath}" 2>/dev/null || echo ""`],
|
|
237
|
-
{ signal, timeout: markitdownTimeoutMs() },
|
|
238
|
-
);
|
|
239
|
-
if (mdResult.stdout.trim()) extracted = mdResult.stdout;
|
|
240
|
-
} catch {
|
|
241
|
-
extracted = pdfExtractionFailureMessage(filePath);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
if (!extracted) extracted = pdfExtractionFailureMessage(filePath);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Copy original to packet
|
|
248
|
-
try {
|
|
249
|
-
await exec(pi, "cp", [filePath, join(packetPath, "original", fileName)], { signal });
|
|
250
|
-
} catch {
|
|
251
|
-
// If cp fails, just write the content
|
|
252
|
-
writeFileSync(join(packetPath, "original", fileName), content, "utf-8");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Write extracted text
|
|
256
|
-
writeFileSync(join(packetPath, "extracted.md"), extracted, "utf-8");
|
|
257
|
-
|
|
258
|
-
// Write manifest
|
|
143
|
+
function finalizeCapture(
|
|
144
|
+
paths: VaultPaths,
|
|
145
|
+
packet: SourcePacket,
|
|
146
|
+
source: CaptureSource,
|
|
147
|
+
content: ExtractedContent,
|
|
148
|
+
): CaptureResult {
|
|
149
|
+
const extracted = content.extracted || source.fallbackText;
|
|
259
150
|
const manifest = {
|
|
260
|
-
id: sourceId,
|
|
261
|
-
title: fileName,
|
|
262
|
-
file_path: filePath,
|
|
151
|
+
id: packet.sourceId,
|
|
263
152
|
captured: fmtDate(),
|
|
264
|
-
format: guessFormat(filePath),
|
|
265
153
|
packet_version: "1.0",
|
|
154
|
+
...source.manifest({ ...content, extracted }),
|
|
266
155
|
};
|
|
267
|
-
writeJson(join(packetPath, "manifest.json"), manifest);
|
|
268
156
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
157
|
+
writeFileSync(join(packet.packetPath, "extracted.md"), extracted, "utf-8");
|
|
158
|
+
writeJson(join(packet.packetPath, "manifest.json"), manifest);
|
|
159
|
+
|
|
160
|
+
const sourcePagePath = join(paths.wiki, "sources", `${packet.sourceId}.md`);
|
|
161
|
+
writeFileSync(sourcePagePath, buildSourcePageSkeleton(manifest, extracted), "utf-8");
|
|
273
162
|
|
|
274
|
-
// Log event
|
|
275
163
|
appendEvent(paths, {
|
|
276
164
|
kind: "capture",
|
|
277
|
-
source_id: sourceId,
|
|
278
|
-
|
|
279
|
-
format: manifest.format,
|
|
165
|
+
source_id: packet.sourceId,
|
|
166
|
+
...source.event({ ...content, extracted }),
|
|
280
167
|
});
|
|
281
168
|
|
|
282
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
sourceId: packet.sourceId,
|
|
171
|
+
packetPath: packet.packetPath,
|
|
172
|
+
sourcePagePath,
|
|
173
|
+
extracted,
|
|
174
|
+
};
|
|
283
175
|
}
|
|
284
176
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
177
|
+
async function preserveFileOriginal(
|
|
178
|
+
pi: ExtensionAPI,
|
|
179
|
+
packetPath: string,
|
|
180
|
+
filePath: string,
|
|
181
|
+
fileName: string,
|
|
182
|
+
fallbackContent: string,
|
|
183
|
+
signal?: AbortSignal,
|
|
184
|
+
): Promise<void> {
|
|
185
|
+
try {
|
|
186
|
+
await exec(pi, "cp", [filePath, join(packetPath, "original", fileName)], { signal });
|
|
187
|
+
} catch {
|
|
188
|
+
// If cp fails, preserve whatever text content was available.
|
|
189
|
+
writeFileSync(join(packetPath, "original", fileName), fallbackContent, "utf-8");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
294
192
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
193
|
+
async function preserveUrlOriginal(
|
|
194
|
+
pi: ExtensionAPI,
|
|
195
|
+
packetPath: string,
|
|
196
|
+
url: string,
|
|
197
|
+
signal?: AbortSignal,
|
|
198
|
+
): Promise<void> {
|
|
199
|
+
const originalPath = join(packetPath, "original", originalFileNameForUrl(url));
|
|
200
|
+
try {
|
|
201
|
+
await exec(pi, "curl", ["-sL", "--max-time", "30", "-o", originalPath, url], {
|
|
202
|
+
signal,
|
|
203
|
+
timeout: 35_000,
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
// Preserve best-effort extraction behavior even when the original artifact cannot be saved.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
304
209
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
210
|
+
function originalFileNameForUrl(url: string): string {
|
|
211
|
+
try {
|
|
212
|
+
const parsed = new URL(url);
|
|
213
|
+
const ext = extname(parsed.pathname).toLowerCase();
|
|
214
|
+
if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
|
|
215
|
+
} catch {
|
|
216
|
+
const path = url.split(/[?#]/, 1)[0] ?? "";
|
|
217
|
+
const ext = extname(path).toLowerCase();
|
|
218
|
+
if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
|
|
219
|
+
}
|
|
309
220
|
|
|
310
|
-
|
|
311
|
-
|
|
221
|
+
return "source.html";
|
|
222
|
+
}
|
|
312
223
|
|
|
313
|
-
|
|
224
|
+
function contentExtractionFailureMessage(source: string): string {
|
|
225
|
+
return `_Content could not be extracted from ${source}_\n`;
|
|
314
226
|
}
|
|
315
227
|
|
|
316
228
|
/** Build a skeleton source page from manifest and extracted text. */
|
|
@@ -370,59 +282,3 @@ status: skeleton
|
|
|
370
282
|
- **Manifest:** [raw/sources/${id}/manifest.json](../raw/sources/${id}/manifest.json)
|
|
371
283
|
`;
|
|
372
284
|
}
|
|
373
|
-
|
|
374
|
-
/** Basic XML to markdown conversion: strip tags while preserving text structure. */
|
|
375
|
-
function xmlToMarkdown(xml: string): string {
|
|
376
|
-
// Extract title from first <title> or root element
|
|
377
|
-
let title = "";
|
|
378
|
-
const titleMatch = xml.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
379
|
-
if (titleMatch) title = titleMatch[1].trim();
|
|
380
|
-
|
|
381
|
-
// Strip XML declaration and doctype
|
|
382
|
-
let text = xml.replace(/<\?xml[^>]*\?>\s*/gi, "");
|
|
383
|
-
text = text.replace(/<!DOCTYPE[^>]*>\s*/gi, "");
|
|
384
|
-
|
|
385
|
-
// Replace block-level tags with newlines
|
|
386
|
-
text = text.replace(/<\/(p|div|section|article|li|h\d|tr|blockquote|pre)>/gi, "\n");
|
|
387
|
-
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
388
|
-
|
|
389
|
-
// Strip remaining tags — match < followed by tag name characters to >
|
|
390
|
-
// Using a loop to handle malformed/broken tags that lack a closing >
|
|
391
|
-
let prev = "";
|
|
392
|
-
while (prev !== text) {
|
|
393
|
-
prev = text;
|
|
394
|
-
text = text.replace(/<[a-zA-Z\/!?][^>]*>/g, "");
|
|
395
|
-
}
|
|
396
|
-
// Remove any stray < that didn't form a complete tag
|
|
397
|
-
text = text.replace(/</g, "");
|
|
398
|
-
|
|
399
|
-
// Decode XML entities in a single pass to avoid double-unescaping
|
|
400
|
-
text = text.replace(/&(?:amp|lt|gt|quot|#\d+);/gi, (entity) => {
|
|
401
|
-
const map: Record<string, string> = { "&": "&", "<": "<", ">": ">", """: '"' };
|
|
402
|
-
const lower = entity.toLowerCase();
|
|
403
|
-
if (map[lower]) return map[lower];
|
|
404
|
-
if (lower.startsWith("&#")) return String.fromCodePoint(Number.parseInt(entity.slice(2, -1)));
|
|
405
|
-
return entity;
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
// Clean up excessive blank lines
|
|
409
|
-
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
410
|
-
|
|
411
|
-
if (!text) return xml; // fallback: return raw if stripping produced nothing
|
|
412
|
-
|
|
413
|
-
const lines = [];
|
|
414
|
-
if (title) lines.push(`# ${title}\n`);
|
|
415
|
-
lines.push(text);
|
|
416
|
-
return lines.join("\n\n");
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function guessFormat(filePath: string): string {
|
|
420
|
-
const lower = filePath.toLowerCase();
|
|
421
|
-
if (lower.endsWith(".pdf")) return "pdf";
|
|
422
|
-
if (lower.endsWith(".md")) return "markdown";
|
|
423
|
-
if (lower.endsWith(".txt")) return "text";
|
|
424
|
-
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
|
|
425
|
-
if (lower.endsWith(".xml")) return "xml";
|
|
426
|
-
if (lower.endsWith(".docx")) return "docx";
|
|
427
|
-
return "file";
|
|
428
|
-
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "LLM Wiki for Pi —
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"pi",
|
|
6
7
|
"pi-package",
|
|
7
|
-
"pi-skill",
|
|
8
8
|
"pi-extension",
|
|
9
|
+
"pi-skill",
|
|
9
10
|
"llm-wiki",
|
|
10
11
|
"karpathy",
|
|
11
12
|
"knowledge-base",
|
|
12
13
|
"obsidian",
|
|
13
14
|
"second-brain",
|
|
14
15
|
"wiki",
|
|
16
|
+
"memory",
|
|
17
|
+
"markdown",
|
|
18
|
+
"pkm",
|
|
19
|
+
"personal-knowledge-management",
|
|
20
|
+
"research",
|
|
21
|
+
"vault",
|
|
22
|
+
"zettelkasten",
|
|
23
|
+
"rag",
|
|
15
24
|
"zosmaai"
|
|
16
25
|
],
|
|
17
26
|
"license": "MIT",
|
|
@@ -24,6 +33,19 @@
|
|
|
24
33
|
"url": "https://github.com/zosmaai/pi-llm-wiki/issues"
|
|
25
34
|
},
|
|
26
35
|
"homepage": "https://github.com/zosmaai/pi-llm-wiki#readme",
|
|
36
|
+
"files": [
|
|
37
|
+
"extensions",
|
|
38
|
+
"skills",
|
|
39
|
+
"prompts",
|
|
40
|
+
"docs",
|
|
41
|
+
"assets",
|
|
42
|
+
"README.md",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
27
49
|
"scripts": {
|
|
28
50
|
"test": "vitest run",
|
|
29
51
|
"test:watch": "vitest",
|
package/.coderabbit.yaml
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# CodeRabbit AI Code Review Configuration
|
|
2
|
-
# Docs: https://docs.coderabbit.ai/guides/configure-coderabbit
|
|
3
|
-
|
|
4
|
-
language: en-US
|
|
5
|
-
early_access: false
|
|
6
|
-
reviews:
|
|
7
|
-
profile: assertive
|
|
8
|
-
request_changes_workflow: true
|
|
9
|
-
high_level_summary: true
|
|
10
|
-
poem: false
|
|
11
|
-
review_status: true
|
|
12
|
-
collapse_walkthrough: false
|
|
13
|
-
auto_review:
|
|
14
|
-
enabled: true
|
|
15
|
-
ignore_title_keywords:
|
|
16
|
-
- "chore"
|
|
17
|
-
- "WIP"
|
|
18
|
-
- "wip"
|
|
19
|
-
drafts: false
|
|
20
|
-
base_branches:
|
|
21
|
-
- main
|
|
22
|
-
path_instructions:
|
|
23
|
-
- path: "extensions/**/*.ts"
|
|
24
|
-
instructions: |
|
|
25
|
-
Review TypeScript extension files for pi coding agent. Check that:
|
|
26
|
-
1. Tools use pi.registerTool() with TypeBox schemas
|
|
27
|
-
2. Error handling uses try/catch with proper messages
|
|
28
|
-
3. Unused parameters use underscore prefix or are removed
|
|
29
|
-
4. File exports a default function receiving ExtensionAPI
|
|
30
|
-
- path: "skills/**/SKILL.md"
|
|
31
|
-
instructions: |
|
|
32
|
-
Review SKILL.md for pi. Verify:
|
|
33
|
-
1. Frontmatter follows Agent Skills spec (name, description)
|
|
34
|
-
2. Workflows are clearly documented with steps
|
|
35
|
-
3. Golden rules and conventions are explicit
|
|
36
|
-
- path: "prompts/**/*.md"
|
|
37
|
-
instructions: |
|
|
38
|
-
Review prompt templates. Verify:
|
|
39
|
-
1. Frontmatter has description, args, section, topLevelCli
|
|
40
|
-
2. Instructions are actionable and clear
|
|
41
|
-
|
|
42
|
-
chat:
|
|
43
|
-
auto_reply: true
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
name: "CodeQL Config"
|
|
2
|
-
|
|
3
|
-
queries:
|
|
4
|
-
- uses: security-and-quality
|
|
5
|
-
|
|
6
|
-
# Suppress false positives for temporary file creation.
|
|
7
|
-
# This is a single-user CLI tool — TOCTOU race conditions on temp directories
|
|
8
|
-
# are not a valid threat model. The alerts only fire because tests pass
|
|
9
|
-
# os.tmpdir() paths into production capture functions.
|
|
10
|
-
query-filters:
|
|
11
|
-
- exclude:
|
|
12
|
-
id: js/insecure-temporary-file
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
concurrency:
|
|
10
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
-
cancel-in-progress: true
|
|
12
|
-
|
|
13
|
-
jobs:
|
|
14
|
-
quality:
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
strategy:
|
|
17
|
-
matrix:
|
|
18
|
-
node-version: [20, 22, 23, 24, 25]
|
|
19
|
-
|
|
20
|
-
steps:
|
|
21
|
-
- uses: actions/checkout@v4
|
|
22
|
-
|
|
23
|
-
- uses: actions/setup-node@v4
|
|
24
|
-
with:
|
|
25
|
-
node-version: ${{ matrix.node-version }}
|
|
26
|
-
cache: npm
|
|
27
|
-
|
|
28
|
-
- run: npm ci
|
|
29
|
-
|
|
30
|
-
- name: Type Check
|
|
31
|
-
run: npm run typecheck
|
|
32
|
-
|
|
33
|
-
- name: Lint
|
|
34
|
-
run: npm run lint
|
|
35
|
-
|
|
36
|
-
- name: Run Tests with Coverage
|
|
37
|
-
run: npm run test:coverage
|
|
38
|
-
|
|
39
|
-
- name: Upload Coverage to Codecov
|
|
40
|
-
if: ${{ matrix.node-version == 22 && github.event_name == 'push' }}
|
|
41
|
-
uses: codecov/codecov-action@v5
|
|
42
|
-
with:
|
|
43
|
-
files: ./coverage/lcov.info
|
|
44
|
-
fail_ci_if_error: false
|
|
45
|
-
verbose: true
|