pi-quiver 5.2.4 → 5.4.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/pi-quiver.ts
4
- import { realpathSync } from "node:fs";
4
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync } from "node:fs";
5
+ import { homedir as homedir2 } from "node:os";
6
+ import { join as join4, resolve as resolve3 } from "node:path";
5
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6
- import { resolve as resolve2 } from "node:path";
7
8
 
8
9
  // lib/fetch-core.ts
9
10
  import { mkdirSync, writeFileSync, createWriteStream } from "node:fs";
@@ -318,8 +319,8 @@ function binaryExtension(contentType) {
318
319
  return sub || "bin";
319
320
  }
320
321
  function writeChunk(stream, b) {
321
- return new Promise((resolve3, reject) => {
322
- stream.write(b, (err) => err ? reject(err) : resolve3());
322
+ return new Promise((resolve4, reject) => {
323
+ stream.write(b, (err) => err ? reject(err) : resolve4());
323
324
  });
324
325
  }
325
326
  async function pumpToFile(stream, reader, prefix, exhausted) {
@@ -343,7 +344,7 @@ async function pumpToFile(stream, reader, prefix, exhausted) {
343
344
  await writeChunk(stream, chunk);
344
345
  bytes += chunk.length;
345
346
  }
346
- await new Promise((resolve3, reject) => stream.end((err) => err ? reject(err) : resolve3()));
347
+ await new Promise((resolve4, reject) => stream.end((err) => err ? reject(err) : resolve4()));
347
348
  return { bytes, truncated };
348
349
  }
349
350
  async function collectBody(res, contentType, raw) {
@@ -516,93 +517,380 @@ async function fetchUrl(opts) {
516
517
  }
517
518
 
518
519
  // lib/doc-to-md-core.ts
519
- import { existsSync, mkdirSync as mkdirSync2, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
520
+ import { existsSync as existsSync2, mkdtempSync, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
520
521
  import { spawn } from "node:child_process";
521
- import { homedir, tmpdir as tmpdir2 } from "node:os";
522
- import { basename, dirname, extname, join as join2, resolve } from "node:path";
523
- import { createHash as createHash2 } from "node:crypto";
522
+ import { homedir, tmpdir as tmpdir3 } from "node:os";
523
+ import { basename, dirname, extname as extname3, join as join3, resolve as resolve2 } from "node:path";
524
524
  import { fileURLToPath, pathToFileURL } from "node:url";
525
- import { extractText, getDocumentProxy } from "unpdf";
526
- var DEFAULT_PYMUPDF_VERSION = "1.27.2.3";
527
- var WARM_TIMEOUT_DEFAULT = 12e4;
528
- var CONVERT_TIMEOUT_DEFAULT = 6e4;
529
- var SOFFICE_TIMEOUT_DEFAULT = 12e4;
530
- var STDERR_CAP = 1e6;
531
- var INLINE_MAX_BYTES2 = 32e3;
532
- var INLINE_MAX_LINES2 = 1e3;
533
- var PREVIEW_LINES2 = 60;
534
- var PREVIEW_MAX_BYTES2 = 4e3;
535
- var VERSION_RE = /^\d+(\.\d+)*$/;
536
- var PAGE_SEP = "\n\n---\n\n";
537
- var OUTPUT_MAX_BYTES = 2e7;
538
- var DEGRADED_MARKER = "[Note: degraded extraction via unpdf \u2014 structure (tables/headings) not preserved]";
539
- function parseConfig(env) {
540
- const version = env.PI_DOC_TO_MD_PYMUPDF_VERSION ?? DEFAULT_PYMUPDF_VERSION;
541
- if (!VERSION_RE.test(version)) {
542
- throw new Error(`PI_DOC_TO_MD_PYMUPDF_VERSION must be digits and dots (got "${version}")`);
525
+
526
+ // lib/doc-to-md-bundle.ts
527
+ import fs, { closeSync, existsSync, mkdirSync as mkdirSync2, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
528
+ import { tmpdir as tmpdir2 } from "node:os";
529
+ import { extname, join as join2, resolve } from "node:path";
530
+ import { randomBytes } from "node:crypto";
531
+ function ownedPattern(stem) {
532
+ const esc = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
533
+ return new RegExp(`^${esc}-(p|s)\\d+-\\d+\\.[a-z0-9]+$`);
534
+ }
535
+ var IMG_LINK_RE = /!\[[^\]]*\]\(\s*(?:<([^>]*)>|([^)]*?))\s*\)|<img\b[^>]*\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"']+))/gi;
536
+ function imageTarget(m) {
537
+ const target = (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? "").trim();
538
+ const destination = target.replace(/^<([\s\S]*)>$/, "$1").trim();
539
+ return m[2] === void 0 ? destination : destination.replace(/\s+(?:"[^"]*"|'[^']*'|\([^)]*\))$/, "");
540
+ }
541
+ function linkedFiles(md) {
542
+ const out = [];
543
+ for (const m of md.matchAll(IMG_LINK_RE)) {
544
+ const target = imageTarget(m);
545
+ if (!target.startsWith("images/")) continue;
546
+ const file = target.slice("images/".length);
547
+ if (file && file !== "." && file !== ".." && /^[^/\\]+$/.test(file)) out.push(file);
543
548
  }
544
- const num = (key, def) => {
545
- const raw = env[key];
546
- if (raw === void 0) return def;
547
- const n = Number.parseInt(raw, 10);
548
- if (!Number.isInteger(n) || n <= 0) throw new Error(`${key} must be a positive integer (got "${raw}")`);
549
- return n;
550
- };
551
- return {
552
- pymupdfVersion: version,
553
- warmTimeoutMs: num("PI_DOC_TO_MD_WARM_TIMEOUT_MS", WARM_TIMEOUT_DEFAULT),
554
- convertTimeoutMs: num("PI_DOC_TO_MD_CONVERT_TIMEOUT_MS", CONVERT_TIMEOUT_DEFAULT),
555
- sofficeTimeoutMs: num("PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", SOFFICE_TIMEOUT_DEFAULT)
556
- };
549
+ return out;
557
550
  }
558
- var SUPPORTED = { ".pdf": "pdf", ".docx": "docx", ".pptx": "pptx" };
559
- function classifyInput(filePath) {
560
- const ext = extname(filePath).toLowerCase();
561
- const t = SUPPORTED[ext];
562
- if (!t) throw new Error(`Unsupported file type "${ext || "(none)"}"; supported: .pdf, .docx, .pptx`);
563
- return t;
551
+ function openBundle(root, stem, overwrite) {
552
+ mkdirSync2(root, { recursive: true });
553
+ const mdPath = join2(root, `${stem}.md`);
554
+ const lockPath = `${mdPath}.lock`;
555
+ let fd;
556
+ try {
557
+ fd = openSync(lockPath, "wx");
558
+ } catch (e) {
559
+ if (e.code === "EEXIST") throw new Error(`Another conversion owns ${mdPath} (lock: ${lockPath}); if no conversion is running, delete the lock`);
560
+ throw new Error(`Output directory not writable: ${root} (${e.message})`);
561
+ }
562
+ closeSync(fd);
563
+ const imagesDir = join2(root, "images");
564
+ try {
565
+ if (existsSync(mdPath)) {
566
+ if (!overwrite) throw new Error(`Output exists: ${mdPath} (pass overwrite)`);
567
+ const owned = ownedPattern(stem);
568
+ const linked = new Set(linkedFiles(readFileSync(mdPath, "utf8")));
569
+ unlinkSync(mdPath);
570
+ if (existsSync(imagesDir)) {
571
+ for (const f of readdirSync(imagesDir)) if (owned.test(f) || linked.has(f)) rmSync(join2(imagesDir, f), { force: true });
572
+ }
573
+ }
574
+ const lockId = randomBytes(6).toString("hex");
575
+ const stagingDir = join2(imagesDir, `.stage-${lockId}`);
576
+ mkdirSync2(stagingDir, { recursive: true });
577
+ return { root, stem, mdPath, lockPath, imagesDir, stagingDir, lockId, manifest: /* @__PURE__ */ new Set(), sourceMap: /* @__PURE__ */ new Map() };
578
+ } catch (e) {
579
+ rmSync(lockPath, { force: true });
580
+ throw e;
581
+ }
564
582
  }
565
- function applyGate2(body) {
566
- const bytes = Buffer.byteLength(body, "utf8");
567
- const lines = body.length ? body.split("\n").length : 0;
568
- const spill = body.length > 0 && (bytes > INLINE_MAX_BYTES2 || lines > INLINE_MAX_LINES2);
569
- return { spill, bytes, lines };
583
+ function publishStaged(b) {
584
+ const out = /* @__PURE__ */ new Map();
585
+ if (!existsSync(b.stagingDir)) return out;
586
+ for (const dir of readdirSync(b.stagingDir).sort()) {
587
+ const m = dir.match(/^p(\d+)$/);
588
+ if (!m) continue;
589
+ const pageDir = join2(b.stagingDir, dir);
590
+ if (!existsSync(join2(pageDir, ".done"))) {
591
+ rmSync(pageDir, { recursive: true, force: true });
592
+ continue;
593
+ }
594
+ const page = Number(m[1]);
595
+ const files = readdirSync(pageDir).filter((f) => f !== ".done" && statSync(join2(pageDir, f)).isFile()).sort();
596
+ const names = [];
597
+ files.forEach((f, i) => {
598
+ const name = `${b.stem}-p${page}-${i + 1}${extname(f).toLowerCase()}`;
599
+ renameSync(join2(pageDir, f), join2(b.imagesDir, name));
600
+ b.manifest.add(name);
601
+ b.sourceMap.set(`${dir}/${f}`, `images/${name}`);
602
+ names.push(name);
603
+ });
604
+ rmSync(pageDir, { recursive: true, force: true });
605
+ out.set(page, names);
606
+ }
607
+ return out;
570
608
  }
571
- function withMarker(md, degraded) {
572
- return degraded ? `${DEGRADED_MARKER}
573
-
574
- ${md}` : md;
609
+ function publishSheetImages(b) {
610
+ for (const f of readdirSync(b.stagingDir).sort()) {
611
+ if (!/^s\d+-\d+\.[a-z0-9]+$/i.test(f)) continue;
612
+ const name = `${b.stem}-${f.toLowerCase()}`;
613
+ renameSync(join2(b.stagingDir, f), join2(b.imagesDir, name));
614
+ b.manifest.add(name);
615
+ b.sourceMap.set(f, `images/${name}`);
616
+ }
617
+ }
618
+ function rewriteImageLinks(md, sourceMap) {
619
+ return md.replace(IMG_LINK_RE, (whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted) => {
620
+ const target = imageTarget([whole, mdAngle, mdPlain, htmlDouble, htmlSingle, htmlUnquoted]);
621
+ const dest = sourceMap.get(target) ?? sourceMap.get(target.replace(/^\.\//, ""));
622
+ return dest ? whole.replace(mdAngle === void 0 ? target : `<${mdAngle}>`, dest) : whole;
623
+ });
575
624
  }
576
- function pagesToMarkdown(pages) {
577
- return pages.map((p) => p.trim()).filter((p) => p.length > 0).join(PAGE_SEP);
625
+ function validateImageLinks(md, manifest) {
626
+ for (const m of md.matchAll(IMG_LINK_RE)) {
627
+ const target = imageTarget(m);
628
+ if (!target.startsWith("images/") || !manifest.has(target.slice("images/".length))) throw new Error(`unexpected image reference in output: ${target}`);
629
+ }
578
630
  }
579
- function tempFilePath2(inputPath, ext) {
580
- const dir = join2(tmpdir2(), "pi-doc-to-md");
631
+ function commitBundle(b, markdown) {
632
+ const tmp = `${b.mdPath}.tmp`;
633
+ writeFileSync2(tmp, markdown, "utf8");
634
+ renameSync(tmp, b.mdPath);
635
+ try {
636
+ fs.rmSync(b.stagingDir, { recursive: true, force: true });
637
+ } catch {
638
+ }
639
+ try {
640
+ fs.rmSync(b.lockPath, { force: true });
641
+ } catch {
642
+ }
643
+ }
644
+ function abortBundle(b) {
645
+ for (const f of b.manifest) rmSync(join2(b.imagesDir, f), { force: true });
646
+ rmSync(`${b.mdPath}.tmp`, { force: true });
647
+ rmSync(b.stagingDir, { recursive: true, force: true });
648
+ rmSync(b.lockPath, { force: true });
649
+ }
650
+ function tempBundleRoot() {
651
+ const dir = join2(tmpdir2(), `pi-quiver-doc-to-md-${randomBytes(4).toString("hex")}`);
581
652
  mkdirSync2(dir, { recursive: true });
582
- const base = basename(inputPath).replace(/\.[^.]+$/, "").replace(/[^\w.-]+/g, "-");
583
- const hash = createHash2("sha1").update(resolve(inputPath)).digest("hex").slice(0, 8);
584
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
585
- return join2(dir, `${stamp}-${base}-${hash}.${ext}`);
653
+ return resolve(dir);
586
654
  }
587
- function spillToFile2(inputPath, body) {
588
- const file = tempFilePath2(inputPath, "md");
589
- writeFileSync2(file, `${body}
590
- `, "utf8");
591
- return file;
655
+
656
+ // lib/doc-to-md-handle.ts
657
+ var TITLE_MAX = 80;
658
+ var NOTE_MAX_LINES = 5;
659
+ var NOTE_MAX_CHARS = 200;
660
+ var META_MAX_CHARS = 120;
661
+ function formatSize2(bytes) {
662
+ if (bytes < 1024) return `${bytes}B`;
663
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
664
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
592
665
  }
593
- function buildPreview2(body) {
594
- let preview = body.split("\n").slice(0, PREVIEW_LINES2).join("\n");
595
- if (preview.length > PREVIEW_MAX_BYTES2) {
596
- preview = `${preview.slice(0, PREVIEW_MAX_BYTES2)}
597
- \u2026[preview truncated]`;
666
+ var trunc = (s, max) => s.length > max ? `${s.slice(0, max - 3)}...` : s;
667
+ function compactRanges(nums, maxEntries = 20) {
668
+ const sorted = [...new Set(nums)].sort((a, b) => a - b);
669
+ const parts = [];
670
+ for (let i = 0; i < sorted.length; ) {
671
+ let j = i;
672
+ while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1) j++;
673
+ parts.push(j > i ? `${sorted[i]}-${sorted[j]}` : `${sorted[i]}`);
674
+ i = j + 1;
598
675
  }
599
- return preview;
676
+ if (parts.length <= maxEntries) return parts.join(", ");
677
+ return `${parts.slice(0, maxEntries).join(", ")} (+${parts.length - maxEntries} more)`;
678
+ }
679
+ function scanOutline(md, max) {
680
+ const entries = [];
681
+ let total = 0, inFence = false;
682
+ md.split("\n").forEach((raw, i) => {
683
+ const line = raw.replace(/\r$/, "");
684
+ if (/^\s*(```|~~~)/.test(line)) {
685
+ inFence = !inFence;
686
+ return;
687
+ }
688
+ if (inFence) return;
689
+ const m = line.match(/^(#{1,6}) (.*)$/);
690
+ if (!m) return;
691
+ total++;
692
+ if (entries.length < max) entries.push({ line: i + 1, level: m[1].length, title: trunc(m[2].trim(), TITLE_MAX) });
693
+ });
694
+ return { entries, total };
695
+ }
696
+ function outlineLines(entries, total) {
697
+ if (entries.length === 0) return [];
698
+ const width = Math.max(5, Math.max(...entries.map((e) => `L${e.line}`.length)) + 2);
699
+ const out = ["Outline:", ...entries.map((e) => ` ${`L${e.line}`.padEnd(width)}${"#".repeat(e.level)} ${trunc(e.title, TITLE_MAX)}`)];
700
+ if (total > entries.length) out.push(` (+${total - entries.length} more)`);
701
+ return out;
702
+ }
703
+ function formatHandle(h) {
704
+ const lines = [`Saved-To: ${h.savedTo}`];
705
+ if (h.imagesDir && h.imageCount > 0) lines.push(`Images-Dir: ${h.imagesDir}`);
706
+ lines.push(`Type: ${h.type} Engine: ${h.engine} Tier: ${h.tier}`);
707
+ lines.push(`Page-Count: ${h.pageCount ?? "?"} Pages: ${h.pages ? compactRanges(h.pages) : "all"} Images: ${h.imageCount} Size: ${formatSize2(h.bytes)} / ${h.lines} lines`);
708
+ if (h.degraded) lines.push(`Degraded: ${h.degraded}`);
709
+ if (h.fallbackReason) lines.push(`Fallback-Reason: ${h.fallbackReason}`);
710
+ const fe = [];
711
+ if (h.failedPages.length) fe.push(`Failed-Pages: ${compactRanges(h.failedPages)}`);
712
+ if (h.emptyPages.length) fe.push(`Empty-Pages: ${compactRanges(h.emptyPages)}`);
713
+ if (fe.length) lines.push(fe.join(" "));
714
+ h.notes.slice(0, NOTE_MAX_LINES).forEach((n, i) => lines.push(`${i === 0 ? "Notes: " : " "}${trunc(n, NOTE_MAX_CHARS)}`));
715
+ lines.push(...outlineLines(h.outline, h.outlineTotal));
716
+ return lines.join("\n");
717
+ }
718
+ function formatInfoHandle(i, max) {
719
+ if (i.sheets) {
720
+ const lines2 = [`Type: ${i.type} Sheets: ${i.sheetsTotal}`];
721
+ for (const s of i.sheets.slice(0, max)) {
722
+ const dims = `rows=${s.rows ?? "?"} cols=${s.cols ?? "?"}`;
723
+ const hidden = s.hiddenRows || s.hiddenCols ? ` hiddenRows=${s.hiddenRows} hiddenCols=${s.hiddenCols}` : "";
724
+ lines2.push(` ${trunc(s.name, TITLE_MAX)} ${s.hidden ? "hidden " : ""}${dims}${hidden}`);
725
+ }
726
+ if (i.sheetsTotal > max) lines2.push(` (+${i.sheetsTotal - max} more)`);
727
+ return lines2.join("\n");
728
+ }
729
+ const lines = [`Type: ${i.type} Page-Count: ${i.pageCount ?? "?"} Backend: ${i.backend}`];
730
+ const meta = Object.entries(i.metadata).filter(([, v]) => v).map(([k, v]) => `${k[0].toUpperCase()}${k.slice(1)}: ${trunc(v, META_MAX_CHARS)}`);
731
+ if (meta.length) lines.push(meta.join(" "));
732
+ if (i.toc.length) {
733
+ lines.push("TOC:", ...i.toc.slice(0, max).map((t) => ` L${t.level} ${trunc(t.title, TITLE_MAX)} (p${t.page})`));
734
+ if (i.tocTotal > max) lines.push(` (+${i.tocTotal - max} more)`);
735
+ }
736
+ return lines.join("\n");
737
+ }
738
+
739
+ // lib/doc-to-md-options.ts
740
+ import { extname as extname2 } from "node:path";
741
+ var UsageError = class extends Error {
742
+ };
743
+ var MIN_PYMUPDF4LLM = "1.27.0";
744
+ var VERSION_RE = /^\d+(\.\d+)*$/;
745
+ var DOC_TO_MD_OPTIONS = [
746
+ { key: "path", flag: null, type: "string", default: null, settable: false, help: "Local .pdf .docx .pptx .xlsx .xls file" },
747
+ { key: "info", flag: "--info", type: "bool", default: false, settable: false, help: "Inspect only (page count, metadata, TOC or sheet inventory); no bundle" },
748
+ { key: "pages", flag: "--pages", type: "pages", default: null, settable: false, help: 'Inclusive 1-based pages, e.g. "12-15" or "3,7,10-12" (PDF/DOCX/PPTX only); default all' },
749
+ { key: "outputDir", flag: "--output-dir", type: "string", default: null, settable: false, help: "Bundle root for <stem>.md + images/; default a per-call temp dir" },
750
+ { key: "overwrite", flag: "--overwrite", type: "bool", default: false, settable: false, help: "Replace an existing completed <stem>.md bundle" },
751
+ { key: "primaryTimeoutMs", flag: "--primary-timeout", type: "int", default: 6e4, settable: true, env: "PI_DOC_TO_MD_CONVERT_TIMEOUT_MS", help: "pymupdf4llm tier; also the unpdf tier" },
752
+ { key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 3e4, settable: true, help: "PyMuPDF get_text tier; also info on PDF" },
753
+ { key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 12e4, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice" },
754
+ { key: "excelTimeoutMs", flag: "--excel-timeout", type: "int", default: 6e4, settable: true, help: "Excel child (both openpyxl loads); also info on Excel" },
755
+ { key: "warmTimeoutMs", flag: "--warm-timeout", type: "int", default: 12e4, settable: true, env: "PI_DOC_TO_MD_WARM_TIMEOUT_MS", help: "Absolute backend discovery/bootstrap deadline (first call per process)" },
756
+ { key: "pymupdfVersion", flag: "--pymupdf-version", type: "version", default: "1.27.2.3", settable: true, env: "PI_DOC_TO_MD_PYMUPDF_VERSION", help: "pymupdf4llm pin (>= 1.27.0)" },
757
+ { key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images" },
758
+ { key: "imageFormat", flag: "--image-format", type: "enum", default: "png", settable: true, enumValues: ["png", "jpg"], help: "Rendered image format (embedded images keep their native extension)" },
759
+ { key: "maxCellsPerSheet", flag: "--max-cells-per-sheet", type: "int", default: 5e4, settable: true, help: "rows x cols budget per worksheet" },
760
+ { key: "maxOutputBytes", flag: "--max-output-bytes", type: "int", default: 2e7, settable: true, help: "Child stdout cap in bytes" },
761
+ { key: "outlineMaxEntries", flag: "--outline-max-entries", type: "int", default: 40, settable: true, help: "Heading outline / TOC / sheet inventory cap in the handle" }
762
+ ];
763
+ var TUNABLE_DEFAULTS = Object.fromEntries(
764
+ DOC_TO_MD_OPTIONS.filter((d) => d.settable).map((d) => [d.key, d.default])
765
+ );
766
+ function versionAtLeast(v, min) {
767
+ const a = v.split(".").map(Number), b = min.split(".").map(Number);
768
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
769
+ const x = a[i] ?? 0, y = b[i] ?? 0;
770
+ if (x !== y) return x > y;
771
+ }
772
+ return true;
773
+ }
774
+ function coerceValue(d, raw, fromString = false) {
775
+ switch (d.type) {
776
+ case "int": {
777
+ const n = fromString && typeof raw === "string" ? Number(raw) : raw;
778
+ return typeof n === "number" && Number.isInteger(n) && n > 0 ? { ok: true, value: n } : { ok: false, reason: "must be a positive integer" };
779
+ }
780
+ case "bool":
781
+ return typeof raw === "boolean" ? { ok: true, value: raw } : { ok: false, reason: "must be true or false" };
782
+ case "enum":
783
+ return typeof raw === "string" && d.enumValues.includes(raw) ? { ok: true, value: raw } : { ok: false, reason: `must be one of ${d.enumValues.join(", ")}` };
784
+ case "version":
785
+ if (typeof raw !== "string" || !VERSION_RE.test(raw)) return { ok: false, reason: "must be digits and dots" };
786
+ if (!versionAtLeast(raw, MIN_PYMUPDF4LLM)) return { ok: false, reason: `must be >= ${MIN_PYMUPDF4LLM}` };
787
+ return { ok: true, value: raw };
788
+ case "string":
789
+ return typeof raw === "string" && raw.length > 0 ? { ok: true, value: raw } : { ok: false, reason: "must be a non-empty string" };
790
+ case "pages":
791
+ return typeof raw === "string" ? { ok: true, value: parsePages(raw) } : { ok: false, reason: "must be a string" };
792
+ }
793
+ }
794
+ function coerceDocToMdSettings(raw, warn = console.warn) {
795
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return void 0;
796
+ const out = {};
797
+ for (const [k, v] of Object.entries(raw)) {
798
+ const d = DOC_TO_MD_OPTIONS.find((o) => o.key === k);
799
+ if (!d || !d.settable) continue;
800
+ const c = coerceValue(d, v);
801
+ if (!c.ok) {
802
+ warn(`pi-quiver: quiver.docToMd.${k} ${c.reason}; ignored.`);
803
+ continue;
804
+ }
805
+ out[k] = c.value;
806
+ }
807
+ return out;
808
+ }
809
+ function parsePages(spec) {
810
+ const out = /* @__PURE__ */ new Set();
811
+ const parts = spec.split(",").map((s) => s.trim());
812
+ if (parts.length === 0 || parts.some((p) => p === "")) throw new UsageError(`invalid --pages "${spec}": expected e.g. "12-15" or "3,7,10-12"`);
813
+ for (const p of parts) {
814
+ const m = p.match(/^(\d+)(?:-(\d+))?$/);
815
+ if (!m) throw new UsageError(`invalid --pages "${spec}": bad token "${p}"`);
816
+ const a = Number(m[1]), b = m[2] === void 0 ? a : Number(m[2]);
817
+ if (a < 1 || b < a) throw new UsageError(`invalid --pages "${spec}": pages are 1-based and ranges ascend`);
818
+ for (let i = a; i <= b; i++) out.add(i);
819
+ }
820
+ return [...out].sort((x, y) => x - y);
821
+ }
822
+ function sanitizeStem(base) {
823
+ const s = base.replace(/[^A-Za-z0-9._-]+/g, "_");
824
+ return s.length ? s : "document";
825
+ }
826
+ var SUPPORTED = { ".pdf": "pdf", ".docx": "docx", ".pptx": "pptx", ".xlsx": "xlsx", ".xls": "xls" };
827
+ function classifyInput(filePath) {
828
+ const t = SUPPORTED[extname2(filePath).toLowerCase()];
829
+ if (!t) throw new Error(`Unsupported file type "${extname2(filePath) || "(none)"}"; supported: .pdf, .docx, .pptx, .xlsx, .xls`);
830
+ return t;
831
+ }
832
+ function resolveOptions(perCall, settings, env) {
833
+ const out = { path: perCall.path };
834
+ for (const d of DOC_TO_MD_OPTIONS) {
835
+ if (d.key === "path") continue;
836
+ let value = d.default;
837
+ if (d.env && env[d.env] !== void 0) {
838
+ const c = coerceValue(d, env[d.env], true);
839
+ if (!c.ok) throw new UsageError(`${d.env} ${c.reason} (got "${env[d.env]}")`);
840
+ value = c.value;
841
+ }
842
+ if (d.settable && settings[d.key] !== void 0) value = settings[d.key];
843
+ const pc = perCall[d.key];
844
+ if (pc !== void 0 && pc !== null) {
845
+ const c = coerceValue(d, pc);
846
+ if (!c.ok) throw new UsageError(`${d.flag} ${c.reason}`);
847
+ value = c.value;
848
+ }
849
+ out[d.key] = value;
850
+ }
851
+ const o = out;
852
+ if (o.info && (o.pages !== null || o.outputDir !== null || o.overwrite)) {
853
+ throw new UsageError("--info cannot be combined with --pages, --output-dir or --overwrite");
854
+ }
855
+ return o;
856
+ }
857
+ function renderHelp() {
858
+ const row = (d) => ` ${(d.flag ?? "<path>").padEnd(26)} ${d.help}${d.default !== null && d.key !== "info" && d.key !== "overwrite" ? ` (default ${d.default})` : ""}`;
859
+ return [
860
+ "Usage: pi-quiver doc-to-md [flags] <path>",
861
+ "",
862
+ "Per-call:",
863
+ ...DOC_TO_MD_OPTIONS.filter((d) => !d.settable).map(row),
864
+ "",
865
+ "Tunables (also settable under quiver.docToMd in settings.json; per-call > settings > default):",
866
+ ...DOC_TO_MD_OPTIONS.filter((d) => d.settable).map(row),
867
+ "",
868
+ "Result: a handle (Saved-To, Images-Dir, Page-Count, Outline ...). Read the Saved-To file for the Markdown.",
869
+ "Exit codes: 0 success, 1 runtime error, 2 usage error."
870
+ ].join("\n");
871
+ }
872
+
873
+ // lib/doc-to-md-core.ts
874
+ var PACKAGE_PINS = { pymupdf4llm: TUNABLE_DEFAULTS.pymupdfVersion, openpyxl: "3.1.5", xlrd: "2.0.2", pillow: "12.3.0" };
875
+ var KILL_GRACE_MS = 2e3;
876
+ var VENV_DIR_NAME = "doc-to-md-venv-v2";
877
+ var LEGACY_VENV_DIR_NAME = "pymupdf-venv";
878
+ var STDERR_CAP = 1e6;
879
+ var OUTPUT_MAX_BYTES = 2e7;
880
+ function withArgs(cfg) {
881
+ return ["--with", `pymupdf4llm==${cfg.pymupdfVersion}`, "--with", `openpyxl==${PACKAGE_PINS.openpyxl}`, "--with", `xlrd==${PACKAGE_PINS.xlrd}`, "--with", `pillow==${PACKAGE_PINS.pillow}`];
882
+ }
883
+ function pipInstallArgs(cfg) {
884
+ return ["-m", "pip", "install", `pymupdf4llm==${cfg.pymupdfVersion}`, `openpyxl==${PACKAGE_PINS.openpyxl}`, `xlrd==${PACKAGE_PINS.xlrd}`, `pillow==${PACKAGE_PINS.pillow}`];
600
885
  }
601
886
  function warmArgs(cfg) {
602
- return ["run", "--with", `pymupdf4llm==${cfg.pymupdfVersion}`, "--python", "3.14", "python", "-c", "import pymupdf4llm"];
887
+ return ["run", ...withArgs(cfg), "--python", "3.14", "python", "-c", "import pymupdf4llm, openpyxl, xlrd, PIL"];
888
+ }
889
+ function uvChildArgs(cfg, script, mode) {
890
+ return ["run", ...withArgs(cfg), "--python", "3.14", "python", script, mode];
603
891
  }
604
- function convertArgs(cfg, scriptPath2, pdfPath) {
605
- return ["run", "--with", `pymupdf4llm==${cfg.pymupdfVersion}`, "--python", "3.14", "python", scriptPath2, pdfPath];
892
+ function pythonChildArgs(script, mode) {
893
+ return [script, mode];
606
894
  }
607
895
  function soffArgs(src, profileDir, outDir) {
608
896
  return [
@@ -623,20 +911,41 @@ function soffArgs(src, profileDir, outDir) {
623
911
  src
624
912
  ];
625
913
  }
914
+ var LIVE = /* @__PURE__ */ new Set();
915
+ function killTree(child) {
916
+ if (child.pid === void 0) return;
917
+ try {
918
+ if (process.platform === "win32") spawn("taskkill", ["/T", "/F", "/PID", String(child.pid)], { stdio: "ignore" });
919
+ else process.kill(-child.pid, "SIGKILL");
920
+ } catch {
921
+ try {
922
+ child.kill("SIGKILL");
923
+ } catch {
924
+ }
925
+ }
926
+ }
927
+ process.on("exit", () => {
928
+ for (const c of LIVE) killTree(c);
929
+ });
626
930
  async function runCapped(cmd, args, opts) {
627
931
  return new Promise((resolveP) => {
628
932
  let settled = false;
629
933
  let timedOut = false;
630
934
  let capped = false;
935
+ let timer;
936
+ let graceTimer;
631
937
  const stdoutChunks = [];
632
938
  const stderrChunks = [];
633
939
  let stdoutBytes = 0;
634
940
  let stderrBytes = 0;
635
- const child = spawn(cmd, args, { env: opts.env ?? process.env });
941
+ const child = spawn(cmd, args, { env: opts.env ?? process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
942
+ LIVE.add(child);
636
943
  const finish = (code) => {
637
944
  if (settled) return;
638
945
  settled = true;
946
+ LIVE.delete(child);
639
947
  clearTimeout(timer);
948
+ clearTimeout(graceTimer);
640
949
  opts.signal?.removeEventListener("abort", onAbort);
641
950
  resolveP({
642
951
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
@@ -646,17 +955,25 @@ async function runCapped(cmd, args, opts) {
646
955
  capped
647
956
  });
648
957
  };
649
- const kill = () => child.kill("SIGKILL");
958
+ const kill = () => {
959
+ killTree(child);
960
+ graceTimer ??= setTimeout(() => finish(null), KILL_GRACE_MS);
961
+ };
650
962
  const onAbort = () => {
651
963
  kill();
652
964
  };
653
965
  if (opts.signal?.aborted) {
654
966
  kill();
655
967
  } else opts.signal?.addEventListener("abort", onAbort);
656
- const timer = setTimeout(() => {
968
+ timer = setTimeout(() => {
657
969
  timedOut = true;
658
970
  kill();
659
971
  }, opts.timeoutMs);
972
+ child.stdin.on("error", (err) => {
973
+ if (err.code !== "EPIPE") throw err;
974
+ });
975
+ child.stdin.write(opts.stdin ?? "");
976
+ child.stdin.end();
660
977
  child.stdout.on("data", (chunk) => {
661
978
  if (capped) return;
662
979
  const remaining = opts.capBytes - stdoutBytes;
@@ -696,73 +1013,29 @@ async function runCapped(cmd, args, opts) {
696
1013
  function findPackageRoot(startDir) {
697
1014
  let dir = startDir;
698
1015
  for (; ; ) {
699
- if (existsSync(join2(dir, "package.json"))) return dir;
1016
+ if (existsSync2(join3(dir, "package.json"))) return dir;
700
1017
  const parent = dirname(dir);
701
1018
  if (parent === dir) throw new Error(`package.json not found walking up from ${startDir}`);
702
1019
  dir = parent;
703
1020
  }
704
1021
  }
705
1022
  function scriptPath() {
706
- return join2(findPackageRoot(dirname(fileURLToPath(import.meta.url))), "scripts", "pdf_to_md.py");
707
- }
708
- function formatSize2(bytes) {
709
- if (bytes < 1024) {
710
- return `${bytes}B`;
711
- } else if (bytes < 1024 * 1024) {
712
- return `${(bytes / 1024).toFixed(1)}KB`;
713
- } else {
714
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
715
- }
716
- }
717
- async function convertViaUv(cfg, pdfPath, signal) {
718
- const r = await runCapped("uv", convertArgs(cfg, scriptPath(), pdfPath), {
719
- timeoutMs: cfg.convertTimeoutMs,
720
- capBytes: OUTPUT_MAX_BYTES,
721
- signal
722
- });
723
- if (r.timedOut || r.capped || r.code !== 0) {
724
- throw new Error(`code=${r.code} timedOut=${r.timedOut} capped=${r.capped}: ${r.stderr.slice(0, 500)}`);
725
- }
726
- return r.stdout;
727
- }
728
- async function convertViaPython(cfg, exe, pdfPath, signal) {
729
- const r = await runCapped(exe, pythonConvertArgs(scriptPath(), pdfPath), {
730
- timeoutMs: cfg.convertTimeoutMs,
731
- capBytes: OUTPUT_MAX_BYTES,
732
- signal
733
- });
734
- if (r.timedOut || r.capped || r.code !== 0) {
735
- throw new Error(`code=${r.code} timedOut=${r.timedOut} capped=${r.capped}: ${r.stderr.slice(0, 500)}`);
736
- }
737
- return r.stdout;
738
- }
739
- async function convertViaUnpdf(pdfPath, cfg, signal) {
740
- if (signal?.aborted) throw new Error("aborted");
741
- const buf = readFileSync(pdfPath);
742
- const work = (async () => {
743
- const pdf = await getDocumentProxy(new Uint8Array(buf));
744
- const { text } = await extractText(pdf, { mergePages: false });
745
- const md = pagesToMarkdown(text);
746
- if (Buffer.byteLength(md, "utf8") > OUTPUT_MAX_BYTES) throw new Error("unpdf output exceeded cap");
747
- return md;
748
- })();
749
- let timer;
750
- const timeout = new Promise((_, rej) => {
751
- timer = setTimeout(() => rej(new Error("unpdf conversion timed out")), cfg.convertTimeoutMs);
752
- });
753
- try {
754
- return await Promise.race([work, timeout]);
755
- } finally {
756
- clearTimeout(timer);
757
- }
1023
+ return join3(findPackageRoot(dirname(fileURLToPath(import.meta.url))), "scripts", "doc_to_md.py");
758
1024
  }
759
1025
  var PROBE_PROGRAM = `import sys
760
1026
  print("PY", sys.version_info[0], sys.version_info[1])
761
1027
  try:
762
- import pymupdf4llm
763
- print("PKG", pymupdf4llm.__version__)
1028
+ # pymupdf4llm.__version__ >= 1.27.0
1029
+ import pymupdf, pymupdf4llm
1030
+ v = tuple(int(x) for x in pymupdf4llm.__version__.split(".")[:3])
1031
+ print("PDF", "yes" if v >= (1, 27, 0) else "no")
1032
+ except Exception:
1033
+ print("PDF", "no")
1034
+ try:
1035
+ import openpyxl, xlrd, PIL
1036
+ print("XLSX", "yes")
764
1037
  except Exception:
765
- print("PKG", "none")
1038
+ print("XLSX", "no")
766
1039
  `;
767
1040
  var PROBE_TIMEOUT_MS = 5e3;
768
1041
  function probeArgs() {
@@ -770,33 +1043,39 @@ function probeArgs() {
770
1043
  }
771
1044
  var PYTHON_CANDIDATES = ["python3", "python"];
772
1045
  function parseProbeOutput(stdout) {
773
- const m = stdout.match(/^PY (\d+) (\d+)\r?\nPKG (\S+)\s*$/);
774
- if (!m) return null;
775
- return { major: Number(m[1]), minor: Number(m[2]), pkg: m[3] === "none" ? null : m[3] };
1046
+ const m = stdout.match(/^PY (\d+) (\d+)\r?\nPDF (yes|no)\r?\nXLSX (yes|no)\s*$/);
1047
+ return m ? { major: Number(m[1]), minor: Number(m[2]), pdf: m[3] === "yes", xlsx: m[4] === "yes" } : null;
776
1048
  }
777
1049
  function meetsFloor(p) {
778
1050
  return p.major > 3 || p.major === 3 && p.minor >= 12;
779
1051
  }
780
1052
  function cacheDir(platform, env, home) {
781
- if (platform === "win32") return join2(env.LOCALAPPDATA ?? join2(home, "AppData", "Local"), "pi-quiver");
782
- if (platform === "darwin") return join2(home, "Library", "Caches", "pi-quiver");
783
- return join2(env.XDG_CACHE_HOME ?? join2(home, ".cache"), "pi-quiver");
1053
+ if (platform === "win32") return join3(env.LOCALAPPDATA ?? join3(home, "AppData", "Local"), "pi-quiver");
1054
+ if (platform === "darwin") return join3(home, "Library", "Caches", "pi-quiver");
1055
+ return join3(env.XDG_CACHE_HOME ?? join3(home, ".cache"), "pi-quiver");
784
1056
  }
785
1057
  function venvPython(venvDir, platform) {
786
- return platform === "win32" ? join2(venvDir, "Scripts", "python.exe") : join2(venvDir, "bin", "python");
787
- }
788
- function pythonConvertArgs(script, pdfPath) {
789
- return [script, pdfPath];
1058
+ return platform === "win32" ? join3(venvDir, "Scripts", "python.exe") : join3(venvDir, "bin", "python");
790
1059
  }
791
1060
  var tail = (s) => s.slice(-500).trim();
792
1061
  async function resolveBackend(cfg, deps, signal) {
793
1062
  if (signal?.aborted) throw new Error("aborted");
794
- const warm = await deps.run("uv", warmArgs(cfg), { timeoutMs: cfg.warmTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
1063
+ const deadline = deps.now() + cfg.warmTimeoutMs;
1064
+ const left = () => Math.max(1, deadline - deps.now());
1065
+ const expired = (tmp) => {
1066
+ if (deadline - deps.now() > 0) return null;
1067
+ if (tmp) deps.rmrf(tmp);
1068
+ return { kind: "none", reason: `backend discovery exceeded warmTimeoutMs (${cfg.warmTimeoutMs}ms) - raise warmTimeoutMs, or install uv` };
1069
+ };
1070
+ const warm = await deps.run("uv", warmArgs(cfg), { timeoutMs: left(), capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
795
1071
  if (signal?.aborted) throw new Error("aborted");
796
- if (warm.code === 0 && !warm.timedOut) return { kind: "uv" };
1072
+ if (warm.code === 0 && !warm.timedOut) return { kind: "uv", pdf: true, xlsx: true };
797
1073
  const uvAbsent = warm.code === null && !warm.timedOut;
798
- const probe = async (exe) => {
799
- const r = await deps.run(exe, probeArgs(), { timeoutMs: PROBE_TIMEOUT_MS, capBytes: 4e3, env: deps.env, signal });
1074
+ const isDeadline = (result) => result !== null && "kind" in result;
1075
+ const probe = async (exe, tmp) => {
1076
+ const timeout = expired(tmp);
1077
+ if (timeout) return timeout;
1078
+ const r = await deps.run(exe, probeArgs(), { timeoutMs: Math.min(PROBE_TIMEOUT_MS, left()), capBytes: 4e3, env: deps.env, signal });
800
1079
  if (signal?.aborted) throw new Error("aborted");
801
1080
  if (r.code !== 0 || r.timedOut) return null;
802
1081
  return parseProbeOutput(r.stdout);
@@ -804,28 +1083,33 @@ async function resolveBackend(cfg, deps, signal) {
804
1083
  let eligible = null;
805
1084
  for (const exe of PYTHON_CANDIDATES) {
806
1085
  const p = await probe(exe);
1086
+ if (isDeadline(p)) return p;
807
1087
  if (!p || !meetsFloor(p)) continue;
808
- if (p.pkg) return { kind: "python", exe, version: p.pkg };
1088
+ if (p.pdf) return { kind: "python", exe, pdf: true, xlsx: p.xlsx };
809
1089
  eligible ??= { exe, version: `${p.major}.${p.minor}` };
810
1090
  }
811
- const venvDir = join2(deps.cacheRoot, "pymupdf-venv");
1091
+ const venvDir = join3(deps.cacheRoot, VENV_DIR_NAME);
812
1092
  const venvExe = venvPython(venvDir, deps.platform);
813
1093
  const cached = await probe(venvExe);
814
- if (cached && meetsFloor(cached) && cached.pkg) return { kind: "venv", exe: venvExe, version: cached.pkg };
1094
+ if (isDeadline(cached)) return cached;
1095
+ if (cached && meetsFloor(cached) && cached.pdf && cached.xlsx) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true };
815
1096
  if (eligible) {
816
1097
  const recheck = await probe(venvExe);
817
- if (recheck && meetsFloor(recheck) && recheck.pkg) return { kind: "venv", exe: venvExe, version: recheck.pkg };
1098
+ if (isDeadline(recheck)) return recheck;
1099
+ if (recheck && meetsFloor(recheck) && recheck.pdf && recheck.xlsx) return { kind: "venv", exe: venvExe, pdf: true, xlsx: true };
818
1100
  const tmp = `${venvDir}.tmp-${deps.pid}`;
819
- const deadline = deps.now() + cfg.warmTimeoutMs;
820
- const left = () => Math.max(1, deadline - deps.now());
821
1101
  const bootFail = (stderr) => {
822
1102
  deps.rmrf(tmp);
823
1103
  return { kind: "none", reason: `python ${eligible.version} found but venv bootstrap failed: ${tail(stderr)} - install python3-venv, or uv` };
824
1104
  };
1105
+ const mkTimeout = expired(tmp);
1106
+ if (mkTimeout) return mkTimeout;
825
1107
  const mk = await deps.run(eligible.exe, ["-m", "venv", tmp], { timeoutMs: left(), capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
826
1108
  if (signal?.aborted) throw new Error("aborted");
827
1109
  if (mk.code !== 0 || mk.timedOut) return bootFail(mk.stderr);
828
- const pip = await deps.run(venvPython(tmp, deps.platform), ["-m", "pip", "install", `pymupdf4llm==${cfg.pymupdfVersion}`], { timeoutMs: left(), capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
1110
+ const pipTimeout = expired(tmp);
1111
+ if (pipTimeout) return pipTimeout;
1112
+ const pip = await deps.run(venvPython(tmp, deps.platform), pipInstallArgs(cfg), { timeoutMs: left(), capBytes: OUTPUT_MAX_BYTES, env: deps.env, signal });
829
1113
  if (signal?.aborted) throw new Error("aborted");
830
1114
  if (pip.code !== 0 || pip.timedOut) return bootFail(pip.stderr);
831
1115
  const publish = () => {
@@ -836,14 +1120,21 @@ async function resolveBackend(cfg, deps, signal) {
836
1120
  return false;
837
1121
  }
838
1122
  };
839
- if (publish()) return { kind: "venv", exe: venvExe, version: cfg.pymupdfVersion };
840
- const winner = await probe(venvExe);
841
- if (winner && meetsFloor(winner) && winner.pkg) {
1123
+ if (publish()) {
1124
+ deps.rmrf(join3(deps.cacheRoot, LEGACY_VENV_DIR_NAME));
1125
+ return { kind: "venv", exe: venvExe, pdf: true, xlsx: true };
1126
+ }
1127
+ const winner = await probe(venvExe, tmp);
1128
+ if (isDeadline(winner)) return winner;
1129
+ if (winner && meetsFloor(winner) && winner.pdf && winner.xlsx) {
842
1130
  deps.rmrf(tmp);
843
- return { kind: "venv", exe: venvExe, version: winner.pkg };
1131
+ return { kind: "venv", exe: venvExe, pdf: true, xlsx: true };
844
1132
  }
845
1133
  deps.rmrf(venvDir);
846
- if (publish()) return { kind: "venv", exe: venvExe, version: cfg.pymupdfVersion };
1134
+ if (publish()) {
1135
+ deps.rmrf(join3(deps.cacheRoot, LEGACY_VENV_DIR_NAME));
1136
+ return { kind: "venv", exe: venvExe, pdf: true, xlsx: true };
1137
+ }
847
1138
  return bootFail("rename after competing bootstrap");
848
1139
  }
849
1140
  return uvAbsent ? { kind: "none", reason: "uv not found; no python >= 3.12 on PATH - install uv, or Python 3.12+" } : { kind: "none", reason: `uv warm-up failed: ${tail(warm.stderr)}; no python >= 3.12 on PATH` };
@@ -855,8 +1146,8 @@ function realDeps() {
855
1146
  cacheRoot: cacheDir(process.platform, process.env, homedir()),
856
1147
  platform: process.platform,
857
1148
  pid: process.pid,
858
- rename: renameSync,
859
- rmrf: (p) => rmSync(p, { recursive: true, force: true }),
1149
+ rename: renameSync2,
1150
+ rmrf: (p) => rmSync2(p, { recursive: true, force: true }),
860
1151
  now: Date.now,
861
1152
  env: process.env
862
1153
  };
@@ -871,28 +1162,28 @@ function getBackend(cfg, deps, signal) {
871
1162
  }
872
1163
  return backendPromise;
873
1164
  }
874
- async function convertOffice(cfg, src, signal, run = runCapped) {
875
- const profileDir = mkdtempSync(join2(tmpdir2(), "pi-doc-soffice-prof-"));
876
- const outDir = mkdtempSync(join2(tmpdir2(), "pi-doc-soffice-out-"));
1165
+ async function convertOffice(sofficeTimeoutMs, src, signal, run = runCapped) {
1166
+ const profileDir = mkdtempSync(join3(tmpdir3(), "pi-doc-soffice-prof-"));
1167
+ const outDir = mkdtempSync(join3(tmpdir3(), "pi-doc-soffice-out-"));
877
1168
  const cleanup = () => {
878
1169
  for (const d of [profileDir, outDir]) {
879
1170
  try {
880
- rmSync(d, { recursive: true, force: true });
1171
+ rmSync2(d, { recursive: true, force: true });
881
1172
  } catch {
882
1173
  }
883
1174
  }
884
1175
  };
885
1176
  try {
886
1177
  const env = { ...process.env, SAL_USE_VCLPLUGIN: "svp", OOO_DISABLE_RECOVERY: "1", SAL_NO_MOUSEGRABS: "1" };
887
- const r = await run("soffice", soffArgs(src, profileDir, outDir), { timeoutMs: cfg.sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
1178
+ const r = await run("soffice", soffArgs(src, profileDir, outDir), { timeoutMs: sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
888
1179
  if (signal?.aborted) throw new Error("aborted");
889
1180
  if (r.code === null && !r.timedOut) {
890
1181
  throw new Error("LibreOffice (soffice) is required to convert .docx/.pptx but was not found on PATH. Install LibreOffice or convert the file to PDF first.");
891
1182
  }
892
1183
  if (r.timedOut || r.code !== 0) throw new Error(`soffice failed (code=${r.code} timedOut=${r.timedOut}): ${r.stderr.slice(0, 500)}`);
893
1184
  const base = basename(src).replace(/\.[^.]+$/, "");
894
- const pdfPath = join2(outDir, `${base}.pdf`);
895
- const st = statSync(pdfPath, { throwIfNoEntry: false });
1185
+ const pdfPath = join3(outDir, `${base}.pdf`);
1186
+ const st = statSync2(pdfPath, { throwIfNoEntry: false });
896
1187
  if (!st || !st.isFile() || st.size === 0) {
897
1188
  throw new Error("LibreOffice (soffice) ran but produced no usable PDF for this file. Ensure LibreOffice can open the document, or convert it to PDF manually first.");
898
1189
  }
@@ -902,92 +1193,228 @@ async function convertOffice(cfg, src, signal, run = runCapped) {
902
1193
  throw e;
903
1194
  }
904
1195
  }
905
- async function runPipeline(cfg, inputPath, type, signal, seams) {
906
- const s = {
907
- backend: (c) => getBackend(c, void 0, signal),
908
- convertPymupdf: (c, b, p, sig) => b.kind === "uv" ? convertViaUv(c, p, sig) : convertViaPython(c, b.exe, p, sig),
909
- convertUnpdf: convertViaUnpdf,
910
- ...seams
911
- };
912
- let pdfPath = inputPath;
913
- let cleanup = null;
914
- if (type === "docx" || type === "pptx") {
915
- const o = await convertOffice(cfg, inputPath, signal);
916
- pdfPath = o.pdfPath;
917
- cleanup = o.cleanup;
1196
+ var DEGRADED_TEXT = "PyMuPDF text extraction - layout/tables not preserved";
1197
+ var DEGRADED_UNPDF = "unpdf text extraction - structure not preserved";
1198
+ var EXCEL_REMEDY = "Remedy: install uv, or pip install openpyxl xlrd pillow";
1199
+ function resolveUnpdfWorker(candidates, exists) {
1200
+ for (const candidate of candidates) if (exists(candidate)) return candidate;
1201
+ throw new Error("unpdf worker not found");
1202
+ }
1203
+ function unpdfWorkerPath() {
1204
+ const here = dirname(fileURLToPath(import.meta.url));
1205
+ return resolveUnpdfWorker([
1206
+ join3(findPackageRoot(here), "dist", "lib", "unpdf-worker.js"),
1207
+ join3(here, "unpdf-worker.js"),
1208
+ join3(here, "unpdf-worker.ts")
1209
+ ], existsSync2);
1210
+ }
1211
+ async function runTierReal(mode, childOptions, _b, signal, timeoutMs, backend) {
1212
+ const cfg = { pymupdfVersion: String(childOptions.pymupdfVersion), warmTimeoutMs: 0 };
1213
+ let cmd, args;
1214
+ if (mode === "pdf-text" || mode === "info" && backend.kind === "none") {
1215
+ cmd = process.execPath;
1216
+ args = [unpdfWorkerPath(), mode];
1217
+ } else if (backend.kind === "uv") {
1218
+ cmd = "uv";
1219
+ args = uvChildArgs(cfg, scriptPath(), mode);
1220
+ } else if (backend.kind === "python" || backend.kind === "venv") {
1221
+ cmd = backend.exe;
1222
+ args = pythonChildArgs(scriptPath(), mode);
1223
+ } else return { ok: false, reason: backend.reason };
1224
+ const r = await runCapped(cmd, args, { timeoutMs, capBytes: Number(childOptions.maxOutputBytes), signal, stdin: JSON.stringify(childOptions) });
1225
+ if (signal?.aborted) return { ok: false, reason: "aborted" };
1226
+ if (r.timedOut) return { ok: false, reason: `timeout after ${timeoutMs}ms` };
1227
+ if (r.capped) return { ok: false, reason: "output exceeded maxOutputBytes" };
1228
+ const detail = r.stderr.slice(-300).trim();
1229
+ let json;
1230
+ try {
1231
+ json = JSON.parse(r.stdout);
1232
+ } catch {
1233
+ return r.code === 0 ? { ok: false, reason: "invalid-json" } : { ok: false, reason: `exit ${r.code ?? -1}`, ...detail ? { detail } : {} };
918
1234
  }
1235
+ if (r.code === 3) return { ok: false, userError: json.error ?? "user error", ...json.pageCount !== void 0 ? { pageCount: json.pageCount } : {} };
1236
+ if (r.code !== 0) return { ok: false, reason: `exit ${r.code ?? -1}`, ...detail ? { detail } : {} };
1237
+ return { ok: true, json };
1238
+ }
1239
+ function detailSuffix(r) {
1240
+ return r.detail ? ` (${r.detail})` : "";
1241
+ }
1242
+ async function convertDocument(o, signal, seams) {
1243
+ const s = { backend: (c) => getBackend(c, void 0, signal), runTier: runTierReal, ...seams };
1244
+ const inputPath = resolve2(o.path);
1245
+ const st = statSync2(inputPath, { throwIfNoEntry: false });
1246
+ if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
1247
+ const type = classifyInput(inputPath);
1248
+ const isExcel = type === "xlsx" || type === "xls";
1249
+ if (isExcel && o.pages) throw new Error("--pages does not apply to spreadsheets: worksheets have no stable page numbering");
1250
+ const backend = await s.backend({ pymupdfVersion: o.pymupdfVersion, warmTimeoutMs: o.warmTimeoutMs });
1251
+ if (isExcel && (backend.kind === "none" || !backend.xlsx)) throw new Error(`Excel conversion needs a Python backend with openpyxl, xlrd and pillow (${backend.kind === "none" ? backend.reason : `${backend.kind} lacks the Excel packages`}). ${EXCEL_REMEDY}`);
1252
+ const stem = sanitizeStem(basename(inputPath, extname3(inputPath)));
1253
+ const b = openBundle(o.outputDir ? resolve2(o.outputDir) : tempBundleRoot(), stem, o.overwrite);
1254
+ let office = null;
919
1255
  try {
920
- const backend = await s.backend(cfg);
921
- if (backend.kind === "none") {
922
- return { markdown: await s.convertUnpdf(pdfPath, cfg, signal), engine: "unpdf", degraded: true, fallbackReason: backend.reason, backend: "unpdf" };
1256
+ let pdfPath = inputPath;
1257
+ if (type === "docx" || type === "pptx") {
1258
+ office = await convertOffice(o.sofficeTimeoutMs, inputPath, signal);
1259
+ pdfPath = office.pdfPath;
923
1260
  }
924
- try {
925
- const markdown = await s.convertPymupdf(cfg, backend, pdfPath, signal);
926
- return {
927
- markdown,
928
- engine: "pymupdf4llm",
929
- degraded: false,
930
- backend: backend.kind,
931
- ...backend.kind === "python" || backend.kind === "venv" ? { pymupdfVersion: backend.version } : {}
932
- };
933
- } catch (e) {
934
- const msg = e instanceof Error ? e.message : String(e);
935
- return {
936
- markdown: await s.convertUnpdf(pdfPath, cfg, signal),
937
- engine: "unpdf",
938
- degraded: true,
939
- fallbackReason: `pymupdf4llm (${backend.kind}) conversion failed: ${msg} - fell back to unpdf`,
940
- backend: "unpdf"
941
- };
1261
+ const base = { path: pdfPath, pages: o.pages, stagingDir: b.stagingDir, imageDpi: o.imageDpi, imageFormat: o.imageFormat, maxCellsPerSheet: o.maxCellsPerSheet, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion };
1262
+ let tier, engine, json, degraded = null, fallbackReason = null;
1263
+ if (isExcel) {
1264
+ const r = await s.runTier("xlsx", base, b, signal, o.excelTimeoutMs, backend);
1265
+ if (!r.ok) {
1266
+ if ("userError" in r) throw new Error(r.userError);
1267
+ const remedy = r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes" ? ". Remedy: raise excelTimeoutMs or lower maxCellsPerSheet" : "";
1268
+ throw new Error(`Excel conversion failed: ${r.reason}${detailSuffix(r)}${remedy}`);
1269
+ }
1270
+ publishSheetImages(b);
1271
+ tier = "excel";
1272
+ engine = type === "xls" ? "xlrd" : "openpyxl";
1273
+ json = r.json;
1274
+ } else if (backend.kind === "none") {
1275
+ const r = await s.runTier("pdf-text", base, b, signal, o.primaryTimeoutMs, backend);
1276
+ if (!r.ok) throw new Error("userError" in r ? r.userError : `Conversion failed: unpdf ${r.reason}${detailSuffix(r)}`);
1277
+ tier = "unpdf";
1278
+ engine = "unpdf";
1279
+ json = r.json;
1280
+ degraded = DEGRADED_UNPDF;
1281
+ } else {
1282
+ const p = await s.runTier("pdf-primary", base, b, signal, o.primaryTimeoutMs, backend);
1283
+ const kept = publishStaged(b);
1284
+ if (p.ok) {
1285
+ tier = "primary";
1286
+ engine = "pymupdf4llm";
1287
+ json = p.json;
1288
+ } else if ("userError" in p) throw new Error(p.userError);
1289
+ else {
1290
+ if (signal?.aborted) throw new Error("aborted");
1291
+ const keepPages = Object.fromEntries([...kept.entries()].map(([k, v]) => [String(k), v]));
1292
+ const f = await s.runTier("pdf-fallback", { ...base, keepPages }, b, signal, o.fallbackTimeoutMs, backend);
1293
+ publishStaged(b);
1294
+ if (!f.ok) throw new Error("userError" in f ? f.userError : `Conversion failed: primary ${p.reason}; fallback ${f.reason}${detailSuffix(f)}`);
1295
+ tier = "fallback";
1296
+ engine = "pymupdf-text";
1297
+ json = f.json;
1298
+ degraded = DEGRADED_TEXT;
1299
+ fallbackReason = `primary ${p.reason}`;
1300
+ }
942
1301
  }
1302
+ let body = rewriteImageLinks(json.markdown ?? "", b.sourceMap);
1303
+ validateImageLinks(body, b.manifest);
1304
+ const head = [];
1305
+ if (degraded) head.push(`Degraded: ${degraded}`);
1306
+ if (fallbackReason) head.push(`Fallback-Reason: ${fallbackReason}`);
1307
+ if (json.failedPages?.length) head.push(`Failed pages: ${json.failedPages.map((f) => `${f.page} (${f.error})`).join("; ")}`);
1308
+ if (json.emptyPages?.length) head.push(`Empty pages: ${json.emptyPages.join(", ")}`);
1309
+ for (const n of json.notes ?? []) head.push(`Notes: ${n}`);
1310
+ const markdown = (head.length ? `${head.join("\n")}
1311
+
1312
+ ` : "") + body;
1313
+ commitBundle(b, markdown);
1314
+ const outline = scanOutline(markdown, o.outlineMaxEntries);
1315
+ const details = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, type, engine, tier, pageCount: json.pageCount ?? null, pages: o.pages, imageCount: b.manifest.size, bytes: Buffer.byteLength(markdown, "utf8"), lines: markdown.split("\n").length, degraded, fallbackReason, failedPages: (json.failedPages ?? []).map((f) => f.page), emptyPages: json.emptyPages ?? [], notes: json.notes ?? [], outline: outline.entries, outlineTotal: outline.total };
1316
+ return { output: formatHandle(details), details };
1317
+ } catch (e) {
1318
+ abortBundle(b);
1319
+ throw e;
943
1320
  } finally {
944
- cleanup?.();
1321
+ office?.cleanup();
945
1322
  }
946
1323
  }
947
- async function convertDocument(path, cfg, signal, seams) {
948
- const inputPath = resolve(path);
949
- const st = statSync(inputPath, { throwIfNoEntry: false });
950
- if (!st || !st.isFile()) throw new Error(`Not a readable file: ${path}`);
1324
+ async function inspectDocument(o, signal, seams) {
1325
+ const s = { backend: (c) => getBackend(c, void 0, signal), runTier: runTierReal, ...seams };
1326
+ const inputPath = resolve2(o.path);
1327
+ const st = statSync2(inputPath, { throwIfNoEntry: false });
1328
+ if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
951
1329
  const type = classifyInput(inputPath);
952
- const { markdown, engine, degraded, fallbackReason, backend, pymupdfVersion } = await runPipeline(cfg, inputPath, type, signal, seams);
953
- const body = withMarker(markdown, degraded).replace(/\s+$/, "");
954
- const { spill, bytes, lines } = applyGate2(body);
955
- const details = { path: inputPath, inputType: type, engine, backend, ...pymupdfVersion !== void 0 ? { pymupdfVersion } : {}, degraded, bytes, lines, spilled: spill };
956
- const header = [
957
- `Source: ${inputPath}`,
958
- `Type: ${type} Engine: ${engine}${degraded ? " (degraded fallback)" : ""}`
959
- ];
960
- if (fallbackReason) header.push(`Fallback-Reason: ${fallbackReason}`);
961
- const compose = (parts) => parts.join("\n").replace(/\n+$/, "");
962
- if (!spill) {
963
- return { output: compose([...header, `Length: ${bytes} bytes, ${lines} lines`, "", body]), details };
1330
+ const isExcel = type === "xlsx" || type === "xls";
1331
+ const backend = await s.backend({ pymupdfVersion: o.pymupdfVersion, warmTimeoutMs: o.warmTimeoutMs });
1332
+ if (isExcel && (backend.kind === "none" || !backend.xlsx)) throw new Error(`Excel inspection needs a Python backend with openpyxl, xlrd and pillow. ${EXCEL_REMEDY}`);
1333
+ let office = null;
1334
+ try {
1335
+ let path = inputPath;
1336
+ if (type === "docx" || type === "pptx") {
1337
+ office = await convertOffice(o.sofficeTimeoutMs, inputPath, signal);
1338
+ path = office.pdfPath;
1339
+ }
1340
+ const r = await s.runTier("info", { path, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion }, { stagingDir: "" }, signal, isExcel ? o.excelTimeoutMs : o.fallbackTimeoutMs, backend);
1341
+ if (!r.ok) {
1342
+ if ("userError" in r) throw new Error(r.userError);
1343
+ if (isExcel && (r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes")) throw new Error(`Excel inspection failed: ${r.reason}. Remedy: raise excelTimeoutMs or lower maxCellsPerSheet${detailSuffix(r)}`);
1344
+ throw new Error(`Inspection failed: ${r.reason}${detailSuffix(r)}`);
1345
+ }
1346
+ const toc = (r.json.toc ?? []).map(([level, title, page]) => ({ level, title, page }));
1347
+ const details = { type, backend: backend.kind, pageCount: r.json.pageCount ?? null, metadata: r.json.metadata ?? {}, toc: toc.slice(0, o.outlineMaxEntries), tocTotal: toc.length, sheets: r.json.sheets ? r.json.sheets.slice(0, o.outlineMaxEntries) : null, sheetsTotal: r.json.sheets?.length ?? 0 };
1348
+ return { output: formatInfoHandle(details, o.outlineMaxEntries), details };
1349
+ } finally {
1350
+ office?.cleanup();
964
1351
  }
965
- const file = spillToFile2(inputPath, body);
966
- return {
967
- output: compose([
968
- ...header,
969
- `Body: ${formatSize2(bytes)} across ${lines} lines \u2014 written to file (too large to inline)`,
970
- `Saved-To: ${file}`,
971
- "",
972
- "Read slices of this file with the read tool (offset/limit) or grep it; do not read the whole file unless you must. Markdown is grep-able by heading (^#).",
973
- "",
974
- "----- preview (first 60 lines) -----",
975
- buildPreview2(body)
976
- ]),
977
- details: { ...details, spilled: true, file }
978
- };
979
1352
  }
980
1353
 
981
1354
  // bin/pi-quiver.ts
982
- var USAGE = 'Usage: pi-quiver fetch <url> [--method GET|HEAD|POST] [--header "K: V"]... [--body <str>] [--raw] [--timeout-ms <n>]\n pi-quiver doc-to-md <path>';
983
- function parseCliArgs(argv) {
984
- if (argv[0] === "doc-to-md") {
985
- const rest2 = argv.slice(1);
986
- if (rest2.length === 0) return { ok: false, error: "missing <path>" };
987
- if (rest2[0].startsWith("--")) return { ok: false, error: `unknown flag: ${rest2[0]}` };
988
- if (rest2.length > 1) return { ok: false, error: `unexpected argument: ${rest2[1]}` };
989
- return { ok: true, cmd: "doc-to-md", path: rest2[0] };
1355
+ var USAGE = 'Usage: pi-quiver fetch <url> [--method GET|HEAD|POST] [--header "K: V"]... [--body <str>] [--raw] [--timeout-ms <n>]\n pi-quiver doc-to-md [--info] [--pages <spec>] [--output-dir <dir>] [--overwrite] [tunable flags] <path> (--help for all flags)';
1356
+ function parseDocToMd(rest) {
1357
+ if (rest.includes("--help") || rest.includes("-h")) return { ok: true, cmd: "doc-to-md-help" };
1358
+ const perCall = {};
1359
+ let path;
1360
+ for (let i = 0; i < rest.length; i++) {
1361
+ const arg = rest[i];
1362
+ if (!arg.startsWith("--")) {
1363
+ if (path !== void 0) return { ok: false, error: `unexpected argument: ${arg}` };
1364
+ path = arg;
1365
+ continue;
1366
+ }
1367
+ const d = DOC_TO_MD_OPTIONS.find((o) => o.flag === arg);
1368
+ if (!d) return { ok: false, error: `unknown flag: ${arg}` };
1369
+ if (d.type === "bool") {
1370
+ perCall[d.key] = true;
1371
+ continue;
1372
+ }
1373
+ const value = rest[++i];
1374
+ if (value === void 0) return { ok: false, error: `${arg} requires a value` };
1375
+ if (d.type === "int") {
1376
+ const n = Number(value);
1377
+ if (!Number.isInteger(n) || n <= 0) return { ok: false, error: `invalid ${arg}: ${value}` };
1378
+ perCall[d.key] = n;
1379
+ } else perCall[d.key] = value;
1380
+ }
1381
+ if (!path) return { ok: false, error: "missing <path>" };
1382
+ return { ok: true, cmd: "doc-to-md", perCall: { ...perCall, path } };
1383
+ }
1384
+ function cliAgentDir(env) {
1385
+ const raw = env.PI_CODING_AGENT_DIR;
1386
+ const home = env.HOME ?? env.USERPROFILE ?? homedir2();
1387
+ if (raw && raw.length > 0) {
1388
+ if (raw === "~") return home;
1389
+ if (raw.startsWith("~/") || process.platform === "win32" && raw.startsWith("~\\")) return join4(home, raw.slice(2));
1390
+ return raw;
1391
+ }
1392
+ return join4(home, ".pi", "agent");
1393
+ }
1394
+ function readCliSettings(cwd, env, warn) {
1395
+ const out = {};
1396
+ for (const file of [join4(cliAgentDir(env), "settings.json"), join4(cwd, ".pi", "settings.json")]) {
1397
+ if (!existsSync3(file)) continue;
1398
+ let raw;
1399
+ try {
1400
+ raw = JSON.parse(readFileSync2(file, "utf8")).quiver?.docToMd;
1401
+ } catch {
1402
+ warn(`pi-quiver: ${file} is not valid JSON; ignored.`);
1403
+ continue;
1404
+ }
1405
+ if (raw === void 0) continue;
1406
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1407
+ const unknown = Object.keys(raw).filter((k) => !DOC_TO_MD_OPTIONS.some((o) => o.key === k && o.settable));
1408
+ if (unknown.length > 0) warn(`pi-quiver: quiver.docToMd in ${file} has keys that are not tunable settings; ignored: ${unknown.join(", ")}`);
1409
+ }
1410
+ const patch = coerceDocToMdSettings(raw, warn);
1411
+ if (patch) Object.assign(out, patch);
1412
+ else warn(`pi-quiver: "docToMd" in ${file} has an unrecognized value; ignored.`);
990
1413
  }
1414
+ return out;
1415
+ }
1416
+ function parseCliArgs(argv) {
1417
+ if (argv[0] === "doc-to-md") return parseDocToMd(argv.slice(1));
991
1418
  if (argv[0] !== "fetch") return { ok: false, error: `unknown command: ${argv[0] ?? "(none)"}` };
992
1419
  const rest = argv.slice(1);
993
1420
  let url;
@@ -1006,18 +1433,15 @@ function parseCliArgs(argv) {
1006
1433
  const value = rest[++i];
1007
1434
  if (value === void 0) return { ok: false, error: `${arg} requires a value` };
1008
1435
  if (arg === "--method") {
1009
- if (value !== "GET" && value !== "HEAD" && value !== "POST") {
1010
- return { ok: false, error: `invalid --method: ${value}` };
1011
- }
1436
+ if (value !== "GET" && value !== "HEAD" && value !== "POST") return { ok: false, error: `invalid --method: ${value}` };
1012
1437
  method = value;
1013
1438
  } else if (arg === "--header") {
1014
1439
  const sep = value.indexOf(": ");
1015
1440
  if (sep <= 0) return { ok: false, error: `malformed --header (expected "Key: Value"): ${value}` };
1016
1441
  headers ??= {};
1017
1442
  headers[value.slice(0, sep)] = value.slice(sep + 2);
1018
- } else if (arg === "--body") {
1019
- body = value;
1020
- } else {
1443
+ } else if (arg === "--body") body = value;
1444
+ else {
1021
1445
  const n = Number(value);
1022
1446
  if (!Number.isFinite(n) || n <= 0) return { ok: false, error: `invalid --timeout-ms: ${value}` };
1023
1447
  timeoutMs = n;
@@ -1045,10 +1469,30 @@ ${USAGE}
1045
1469
  `);
1046
1470
  return 2;
1047
1471
  }
1472
+ if (parsed.cmd === "doc-to-md-help") {
1473
+ process.stdout.write(`${renderHelp()}
1474
+ `);
1475
+ return 0;
1476
+ }
1048
1477
  if (parsed.cmd === "doc-to-md") {
1478
+ let o;
1049
1479
  try {
1050
- const result = await convertDocument(parsed.path, parseConfig(process.env));
1051
- process.stdout.write(`${result.output}
1480
+ const settings = readCliSettings(process.cwd(), process.env, (m) => process.stderr.write(`${m}
1481
+ `));
1482
+ const pc = parsed.perCall;
1483
+ o = resolveOptions({ ...pc, path: resolve3(process.cwd(), pc.path), ...pc.outputDir ? { outputDir: resolve3(process.cwd(), pc.outputDir) } : {} }, settings, process.env);
1484
+ } catch (err) {
1485
+ if (err instanceof UsageError) {
1486
+ process.stderr.write(`${err.message}
1487
+ ${USAGE}
1488
+ `);
1489
+ return 2;
1490
+ }
1491
+ throw err;
1492
+ }
1493
+ try {
1494
+ const r = o.info ? await inspectDocument(o) : await convertDocument(o);
1495
+ process.stdout.write(`${r.output}
1052
1496
  `);
1053
1497
  return 0;
1054
1498
  } catch (err) {
@@ -1071,16 +1515,16 @@ ${USAGE}
1071
1515
  function isMainEntry() {
1072
1516
  if (!process.argv[1]) return false;
1073
1517
  try {
1074
- return realpathSync(fileURLToPath2(import.meta.url)) === realpathSync(resolve2(process.argv[1]));
1518
+ return realpathSync(fileURLToPath2(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
1075
1519
  } catch {
1076
1520
  return false;
1077
1521
  }
1078
1522
  }
1079
- if (isMainEntry()) {
1080
- main().then((code) => {
1081
- process.exitCode = code;
1082
- });
1083
- }
1523
+ if (isMainEntry()) main().then((code) => {
1524
+ process.exitCode = code;
1525
+ });
1084
1526
  export {
1085
- parseCliArgs
1527
+ cliAgentDir,
1528
+ parseCliArgs,
1529
+ readCliSettings
1086
1530
  };