@zosmaai/pi-llm-wiki 0.2.2 → 0.3.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.
|
@@ -206,12 +206,20 @@ export async function captureFile(
|
|
|
206
206
|
mkdirSync(join(packetPath, "original"), { recursive: true });
|
|
207
207
|
mkdirSync(join(packetPath, "attachments"), { recursive: true });
|
|
208
208
|
|
|
209
|
-
const
|
|
209
|
+
const lowerPath = filePath.toLowerCase();
|
|
210
|
+
const isPdf = lowerPath.endsWith(".pdf");
|
|
211
|
+
const isXml = lowerPath.endsWith(".xml");
|
|
210
212
|
const content = isPdf ? "" : readText(filePath);
|
|
211
213
|
const fileName = filePath.split("/").pop() || "unknown";
|
|
212
214
|
|
|
213
|
-
// Try MarkItDown for PDFs.
|
|
214
215
|
let extracted = content;
|
|
216
|
+
|
|
217
|
+
// Convert XML to markdown
|
|
218
|
+
if (isXml && content) {
|
|
219
|
+
extracted = xmlToMarkdown(content);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Try MarkItDown for PDFs.
|
|
215
223
|
if (isPdf) {
|
|
216
224
|
const markitdown = await exec(
|
|
217
225
|
pi,
|
|
@@ -363,12 +371,58 @@ status: skeleton
|
|
|
363
371
|
`;
|
|
364
372
|
}
|
|
365
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
|
+
|
|
366
419
|
function guessFormat(filePath: string): string {
|
|
367
420
|
const lower = filePath.toLowerCase();
|
|
368
421
|
if (lower.endsWith(".pdf")) return "pdf";
|
|
369
422
|
if (lower.endsWith(".md")) return "markdown";
|
|
370
423
|
if (lower.endsWith(".txt")) return "text";
|
|
371
424
|
if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
|
|
425
|
+
if (lower.endsWith(".xml")) return "xml";
|
|
372
426
|
if (lower.endsWith(".docx")) return "docx";
|
|
373
427
|
return "file";
|
|
374
428
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "LLM Wiki for Pi — self-maintaining knowledge base following Karpathy's pattern. Obsidian-friendly, auto-updating, personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/test/llm-wiki.test.ts
CHANGED
|
@@ -388,6 +388,56 @@ describe("source packet capture", () => {
|
|
|
388
388
|
// Original file should be preserved
|
|
389
389
|
expect(existsSync(join(result.packetPath, "original", "notes.md"))).toBe(true);
|
|
390
390
|
});
|
|
391
|
+
|
|
392
|
+
it("should convert XML files to readable markdown in extracted.md", async () => {
|
|
393
|
+
const paths = makePaths();
|
|
394
|
+
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
395
|
+
<document>
|
|
396
|
+
<title>Project Report</title>
|
|
397
|
+
<section>
|
|
398
|
+
<heading>Findings</heading>
|
|
399
|
+
<p>The analysis revealed several key insights.</p>
|
|
400
|
+
<list>
|
|
401
|
+
<item>First finding</item>
|
|
402
|
+
<item>Second finding</item>
|
|
403
|
+
</list>
|
|
404
|
+
</section>
|
|
405
|
+
</document>`;
|
|
406
|
+
const xmlPath = join(tempDir, "report.xml");
|
|
407
|
+
writeFileSync(xmlPath, xmlContent, "utf-8");
|
|
408
|
+
|
|
409
|
+
const pi = mockPi();
|
|
410
|
+
const result = await captureFile(pi as never, paths, xmlPath);
|
|
411
|
+
|
|
412
|
+
const extracted = readFile(join(result.packetPath, "extracted.md"));
|
|
413
|
+
// Should have extracted title
|
|
414
|
+
expect(extracted).toContain("Project Report");
|
|
415
|
+
// Should have extracted text content
|
|
416
|
+
expect(extracted).toContain("The analysis revealed several key insights.");
|
|
417
|
+
expect(extracted).toContain("First finding");
|
|
418
|
+
expect(extracted).toContain("Second finding");
|
|
419
|
+
// Should NOT contain raw XML tags
|
|
420
|
+
expect(extracted).not.toContain("<?xml");
|
|
421
|
+
expect(extracted).not.toContain("<document>");
|
|
422
|
+
expect(extracted).not.toContain("</document>");
|
|
423
|
+
|
|
424
|
+
// Original file should be preserved
|
|
425
|
+
expect(existsSync(join(result.packetPath, "original", "report.xml"))).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should fall back to raw XML content when tag stripping produces nothing", async () => {
|
|
429
|
+
const paths = makePaths();
|
|
430
|
+
const xmlContent = `<?xml version="1.0"?><data><![CDATA[Hello]]></data>`;
|
|
431
|
+
const xmlPath = join(tempDir, "minimal.xml");
|
|
432
|
+
writeFileSync(xmlPath, xmlContent, "utf-8");
|
|
433
|
+
|
|
434
|
+
const pi = mockPi();
|
|
435
|
+
const result = await captureFile(pi as never, paths, xmlPath);
|
|
436
|
+
|
|
437
|
+
const extracted = readFile(join(result.packetPath, "extracted.md"));
|
|
438
|
+
// Should have the text content at minimum
|
|
439
|
+
expect(extracted).toContain("Hello");
|
|
440
|
+
});
|
|
391
441
|
});
|
|
392
442
|
|
|
393
443
|
describe("wiki directory structure", () => {
|