@zosmaai/pi-llm-wiki 0.8.1 → 0.8.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/README.md
CHANGED
|
@@ -399,9 +399,39 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, test patterns, and
|
|
|
399
399
|
|
|
400
400
|
## Contributors
|
|
401
401
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
402
|
+
Thanks to everyone who has contributed! This list is regenerated automatically by [`.github/workflows/contributors.yml`](.github/workflows/contributors.yml) — see [#60](https://github.com/zosmaai/pi-llm-wiki/issues/60) for the rationale.
|
|
403
|
+
|
|
404
|
+
<!-- readme: contributors -start -->
|
|
405
|
+
<table>
|
|
406
|
+
<tbody>
|
|
407
|
+
<tr>
|
|
408
|
+
<td align="center">
|
|
409
|
+
<a href="https://github.com/arjun-zosma">
|
|
410
|
+
<img src="https://avatars.githubusercontent.com/u/25246034?v=4" width="64;" alt="arjun-zosma"/>
|
|
411
|
+
<br />
|
|
412
|
+
<sub><b>Arjun Nayak</b></sub>
|
|
413
|
+
</a>
|
|
414
|
+
</td>
|
|
415
|
+
<td align="center">
|
|
416
|
+
<a href="https://github.com/jfraser">
|
|
417
|
+
<img src="https://avatars.githubusercontent.com/u/165964?v=4" width="64;" alt="jfraser"/>
|
|
418
|
+
<br />
|
|
419
|
+
<sub><b>James Fraser</b></sub>
|
|
420
|
+
</a>
|
|
421
|
+
</td>
|
|
422
|
+
<td align="center">
|
|
423
|
+
<a href="https://github.com/Shanvit7">
|
|
424
|
+
<img src="https://avatars.githubusercontent.com/u/64424817?v=4" width="64;" alt="Shanvit7"/>
|
|
425
|
+
<br />
|
|
426
|
+
<sub><b>Shanvit S Shetty</b></sub>
|
|
427
|
+
</a>
|
|
428
|
+
</td>
|
|
429
|
+
</tr>
|
|
430
|
+
<tbody>
|
|
431
|
+
</table>
|
|
432
|
+
<!-- readme: contributors -end -->
|
|
433
|
+
|
|
434
|
+
<sub>Full history: [contributors graph](https://github.com/zosmaai/pi-llm-wiki/graphs/contributors).</sub>
|
|
405
435
|
|
|
406
436
|
---
|
|
407
437
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
1
2
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { NodeHtmlMarkdown } from "node-html-markdown";
|
|
2
4
|
import { exec } from "./utils.js";
|
|
3
5
|
|
|
4
6
|
export type ExtractionStatus = "success" | "failed" | "unsupported";
|
|
@@ -38,6 +40,67 @@ interface UrlExtractArgs {
|
|
|
38
40
|
signal?: AbortSignal;
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Binary magic byte detection
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const BINARY_SIGNATURES: Array<{ bytes: number[]; format: string }> = [
|
|
48
|
+
// Archives & documents
|
|
49
|
+
{ bytes: [0x50, 0x4b, 0x03, 0x04], format: "zip" }, // ZIP / DOCX / XLSX / PPTX / JAR
|
|
50
|
+
{ bytes: [0x25, 0x50, 0x44, 0x46], format: "pdf" }, // %PDF
|
|
51
|
+
{ bytes: [0x37, 0x7a, 0xbc, 0xaf], format: "7z" }, // 7-Zip
|
|
52
|
+
{ bytes: [0x1f, 0x8b], format: "gzip" }, // gzip / .tar.gz
|
|
53
|
+
// Images
|
|
54
|
+
{ bytes: [0x89, 0x50, 0x4e, 0x47], format: "png" }, // PNG
|
|
55
|
+
{ bytes: [0xff, 0xd8, 0xff], format: "jpeg" }, // JPEG
|
|
56
|
+
{ bytes: [0x47, 0x49, 0x46, 0x38], format: "gif" }, // GIF8
|
|
57
|
+
{ bytes: [0x42, 0x4d], format: "bmp" }, // BMP
|
|
58
|
+
{ bytes: [0x49, 0x49, 0x2a, 0x00], format: "tiff" }, // TIFF (little-endian)
|
|
59
|
+
{ bytes: [0x4d, 0x4d, 0x00, 0x2a], format: "tiff" }, // TIFF (big-endian)
|
|
60
|
+
{ bytes: [0x52, 0x49, 0x46, 0x46], format: "riff" }, // RIFF (WAV / AVI / WebP)
|
|
61
|
+
// Executables & binaries
|
|
62
|
+
{ bytes: [0x4d, 0x5a], format: "exe" }, // Windows PE (EXE / DLL)
|
|
63
|
+
{ bytes: [0xcf, 0xfa, 0xed, 0xfe], format: "macho" }, // Mach-O 64-bit LE
|
|
64
|
+
{ bytes: [0xce, 0xfa, 0xed, 0xfe], format: "macho" }, // Mach-O 32-bit LE
|
|
65
|
+
{ bytes: [0xfe, 0xed, 0xfa, 0xcf], format: "macho" }, // Mach-O 64-bit BE
|
|
66
|
+
{ bytes: [0xfe, 0xed, 0xfa, 0xce], format: "macho" }, // Mach-O 32-bit BE
|
|
67
|
+
{ bytes: [0xca, 0xfe, 0xba, 0xbe], format: "class" }, // Java .class / Mach-O FAT
|
|
68
|
+
{ bytes: [0x7f, 0x45, 0x4c, 0x46], format: "elf" }, // ELF binary
|
|
69
|
+
{ bytes: [0x00, 0x61, 0x73, 0x6d], format: "wasm" }, // WebAssembly
|
|
70
|
+
// Data & media
|
|
71
|
+
{ bytes: [0x53, 0x51, 0x4c, 0x69], format: "sqlite" }, // SQLite
|
|
72
|
+
{ bytes: [0x49, 0x44, 0x33], format: "mp3" }, // MP3 (ID3 tag)
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Reads the first 8 bytes of `filePath` and checks them against known binary
|
|
77
|
+
* magic byte signatures. Returns the detected format name or `null` for text.
|
|
78
|
+
*/
|
|
79
|
+
export async function detectBinaryMagicBytes(filePath: string): Promise<string | null> {
|
|
80
|
+
let handle: import("node:fs/promises").FileHandle | undefined;
|
|
81
|
+
try {
|
|
82
|
+
handle = await open(filePath, "r");
|
|
83
|
+
const buf = Buffer.alloc(8);
|
|
84
|
+
const { bytesRead } = await handle.read(buf, 0, 8, 0);
|
|
85
|
+
const header = buf.subarray(0, bytesRead);
|
|
86
|
+
|
|
87
|
+
for (const { bytes, format } of BINARY_SIGNATURES) {
|
|
88
|
+
if (bytes.every((b, i) => header[i] === b)) return format;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null; // Unreadable file — let the extractor deal with it
|
|
93
|
+
} finally {
|
|
94
|
+
await handle?.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function binaryExtractionFailureMessage(format: string): string {
|
|
99
|
+
return `_Binary file could not be converted to markdown (detected format: ${format}).\nCapture a text-based version or a URL pointing to readable content instead._\n`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
41
104
|
const DEFAULT_MARKITDOWN_TIMEOUT_MS = 180_000;
|
|
42
105
|
const DEFAULT_CURL_TIMEOUT_SECONDS = 30;
|
|
43
106
|
|
|
@@ -193,10 +256,11 @@ async function extractTextUrl(
|
|
|
193
256
|
content_type: "application/pdf",
|
|
194
257
|
};
|
|
195
258
|
}
|
|
259
|
+
const normalized = htmlToMarkdown(curlExtracted);
|
|
196
260
|
return {
|
|
197
|
-
extracted:
|
|
198
|
-
title: titleFromHtml(curlExtracted),
|
|
199
|
-
extractor: "
|
|
261
|
+
extracted: normalized,
|
|
262
|
+
title: titleFromMarkdown(normalized) ?? titleFromHtml(curlExtracted),
|
|
263
|
+
extractor: "htmlToMarkdown",
|
|
200
264
|
extraction_status: "success",
|
|
201
265
|
};
|
|
202
266
|
}
|
|
@@ -279,6 +343,23 @@ function titleFromHtml(html: string): string | undefined {
|
|
|
279
343
|
return html.match(/<title>([^<]*)<\/title>/i)?.[1]?.trim();
|
|
280
344
|
}
|
|
281
345
|
|
|
346
|
+
/** Decode common HTML/XML entities. Shared by xmlToMarkdown and htmlToMarkdown. */
|
|
347
|
+
function decodeHtmlEntities(text: string): string {
|
|
348
|
+
return text.replace(/&(?:amp|lt|gt|quot|apos|#\d+);/gi, (entity) => {
|
|
349
|
+
const map: Record<string, string> = {
|
|
350
|
+
"&": "&",
|
|
351
|
+
"<": "<",
|
|
352
|
+
">": ">",
|
|
353
|
+
""": '"',
|
|
354
|
+
"'": "'",
|
|
355
|
+
};
|
|
356
|
+
const lower = entity.toLowerCase();
|
|
357
|
+
if (map[lower]) return map[lower];
|
|
358
|
+
if (lower.startsWith("&#")) return String.fromCodePoint(Number.parseInt(entity.slice(2, -1)));
|
|
359
|
+
return entity;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
282
363
|
/** Basic XML to markdown conversion: strip tags while preserving text structure. */
|
|
283
364
|
function xmlToMarkdown(xml: string): string {
|
|
284
365
|
let title = "";
|
|
@@ -297,14 +378,7 @@ function xmlToMarkdown(xml: string): string {
|
|
|
297
378
|
}
|
|
298
379
|
text = text.replace(/</g, "");
|
|
299
380
|
|
|
300
|
-
text = text
|
|
301
|
-
const map: Record<string, string> = { "&": "&", "<": "<", ">": ">", """: '"' };
|
|
302
|
-
const lower = entity.toLowerCase();
|
|
303
|
-
if (map[lower]) return map[lower];
|
|
304
|
-
if (lower.startsWith("&#")) return String.fromCodePoint(Number.parseInt(entity.slice(2, -1)));
|
|
305
|
-
return entity;
|
|
306
|
-
});
|
|
307
|
-
|
|
381
|
+
text = decodeHtmlEntities(text);
|
|
308
382
|
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
309
383
|
if (!text) return xml;
|
|
310
384
|
|
|
@@ -314,6 +388,41 @@ function xmlToMarkdown(xml: string): string {
|
|
|
314
388
|
return lines.join("\n\n");
|
|
315
389
|
}
|
|
316
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Lightweight HTML-to-markdown normalizer for the curl fallback path.
|
|
393
|
+
*
|
|
394
|
+
* Pre-strips page chrome (nav, header, footer, script, style) that
|
|
395
|
+
* node-html-markdown does not remove, then delegates full conversion —
|
|
396
|
+
* bold, italic, code blocks, tables, ordered lists, image alt text — to
|
|
397
|
+
* node-html-markdown. Prepends the <title> as a # heading when the body
|
|
398
|
+
* has no <h1> of its own.
|
|
399
|
+
*
|
|
400
|
+
* Falls back to the original HTML if conversion yields an empty string.
|
|
401
|
+
*/
|
|
402
|
+
export function htmlToMarkdown(input: string): string {
|
|
403
|
+
// 1. Extract <title> from original before stripping head
|
|
404
|
+
const title = input.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? "";
|
|
405
|
+
|
|
406
|
+
// 2. Strip <head> and noise blocks that node-html-markdown won't remove
|
|
407
|
+
let html = input.replace(/<head[\s\S]*?<\/head>/gi, "");
|
|
408
|
+
let previousHtml = "";
|
|
409
|
+
while (previousHtml !== html) {
|
|
410
|
+
previousHtml = html;
|
|
411
|
+
html = html.replace(/<(script|style|nav|header|footer|noscript)[\s\S]*?<\/\1>/gi, "");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 3. Delegate to node-html-markdown for full semantic conversion
|
|
415
|
+
const converted = NodeHtmlMarkdown.translate(html).trim();
|
|
416
|
+
if (!converted) return input;
|
|
417
|
+
|
|
418
|
+
// 4. Prepend <title> as # heading only if body has no <h1> of its own
|
|
419
|
+
const hasBodyH1 = /<h1[^>]*>[\s\S]*?<\/h1>/i.test(html);
|
|
420
|
+
const lines: string[] = [];
|
|
421
|
+
if (title && !hasBodyH1) lines.push(`# ${title}\n`);
|
|
422
|
+
lines.push(converted);
|
|
423
|
+
return lines.join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
317
426
|
function jsonToMarkdown(json: string): string {
|
|
318
427
|
let value: unknown;
|
|
319
428
|
try {
|
|
@@ -2,7 +2,13 @@ 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 {
|
|
5
|
+
import {
|
|
6
|
+
type ExtractedContent,
|
|
7
|
+
binaryExtractionFailureMessage,
|
|
8
|
+
detectBinaryMagicBytes,
|
|
9
|
+
extractUrlContent,
|
|
10
|
+
fileExtractorFor,
|
|
11
|
+
} from "./source-extractors.js";
|
|
6
12
|
import { type VaultPaths, exec, fmtDate, nextSourceId, readText, writeJson } from "./utils.js";
|
|
7
13
|
|
|
8
14
|
/**
|
|
@@ -107,6 +113,18 @@ function fileCaptureSource(
|
|
|
107
113
|
preserveOriginal: (packetPath) =>
|
|
108
114
|
preserveFileOriginal(pi, packetPath, filePath, fileName, content, signal),
|
|
109
115
|
extract: async () => {
|
|
116
|
+
// Guard: if we hit the generic catch-all extractor, check for binary magic bytes first
|
|
117
|
+
if (extractor.format === "file") {
|
|
118
|
+
const binaryFormat = await detectBinaryMagicBytes(filePath);
|
|
119
|
+
if (binaryFormat) {
|
|
120
|
+
return {
|
|
121
|
+
extracted: binaryExtractionFailureMessage(binaryFormat),
|
|
122
|
+
extractor: "magicBytes",
|
|
123
|
+
extraction_status: "unsupported" as const,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
const extractedStr = await extractor.extract({ pi, filePath, content, signal });
|
|
111
129
|
const failed = extractedStr.includes("could not be converted");
|
|
112
130
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
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
6
|
"pi",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"node": ">=18"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@modelcontextprotocol/server": "^2.0.0-alpha.2"
|
|
79
|
+
"@modelcontextprotocol/server": "^2.0.0-alpha.2",
|
|
80
|
+
"node-html-markdown": "^2.0.0"
|
|
80
81
|
},
|
|
81
82
|
"devDependencies": {
|
|
82
83
|
"@biomejs/biome": "^1.9.4",
|