@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 isPdf = filePath.toLowerCase().endsWith(".pdf");
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> = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"' };
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.2.2",
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",
@@ -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", () => {