pi-quiver 5.5.0 → 6.0.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.
@@ -12,7 +12,7 @@ import { type ChildProcess, spawn } from "node:child_process";
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { basename, dirname, extname, join, resolve } from "node:path";
14
14
  import { fileURLToPath, pathToFileURL } from "node:url";
15
- import { type Bundle, abortBundle, commitBundle, openBundle, publishSheetImages, publishStaged, rewriteImageLinks, tempBundleRoot, validateImageLinks } from "./doc-to-md-bundle.ts";
15
+ import { type Bundle, abortBundle, commitBundle, openBundle, publishSheetCsvs, publishSheetImages, publishStaged, rewriteLinks, tempBundleRoot, validateImageLinks } from "./doc-to-md-bundle.ts";
16
16
  import { type Engine, type HandleData, type InfoData, type Tier, formatHandle, formatInfoHandle, scanOutline, type SheetInfo, type TocEntry } from "./doc-to-md-handle.ts";
17
17
  import { type DocToMdOptions, type InputType, TUNABLE_DEFAULTS, classifyInput, sanitizeStem } from "./doc-to-md-options.ts";
18
18
 
@@ -28,6 +28,7 @@ export const VENV_DIR_NAME = "doc-to-md-venv-v2";
28
28
  export const LEGACY_VENV_DIR_NAME = "pymupdf-venv";
29
29
  const STDERR_CAP = 1_000_000;
30
30
  export const OUTPUT_MAX_BYTES = 20_000_000;
31
+ export const EXCEL_PDF_FILTER = 'pdf:calc_pdf_Export:{"SinglePageSheets":{"type":"boolean","value":"true"}}';
31
32
 
32
33
  export interface BackendConfig { pymupdfVersion: string; warmTimeoutMs: number; }
33
34
 
@@ -59,12 +60,12 @@ export function uvChildArgs(cfg: BackendConfig, script: string, mode: string): s
59
60
 
60
61
  export function pythonChildArgs(script: string, mode: string): string[] { return [script, mode]; }
61
62
 
62
- export function soffArgs(src: string, profileDir: string, outDir: string): string[] {
63
+ export function soffArgs(src: string, profileDir: string, outDir: string, filter = "pdf"): string[] {
63
64
  return [
64
65
  "--headless", "--invisible", "--nocrashreport", "--nodefault", "--nofirststartwizard",
65
66
  "--nolockcheck", "--nologo", "--norestore", "--quickstart=no",
66
67
  `-env:UserInstallation=${pathToFileURL(profileDir).href}`,
67
- "--convert-to", "pdf", "--outdir", outDir, src,
68
+ "--convert-to", filter, "--outdir", outDir, src,
68
69
  ];
69
70
  }
70
71
 
@@ -367,37 +368,37 @@ export function resetBackendCacheForTests(): void {
367
368
  backendPromise = null;
368
369
  }
369
370
 
370
- export async function convertOffice(
371
- sofficeTimeoutMs: number,
372
- src: string,
373
- signal?: AbortSignal,
374
- run: RunFn = runCapped,
375
- ): Promise<{ pdfPath: string; cleanup: () => void }> {
371
+ export type OfficeResult =
372
+ | { ok: true; pdfPath: string; cleanup: () => void }
373
+ | { ok: false; kind: "missing" | "timeout" | "exit" | "no-pdf"; code: number | null; timedOut: boolean; stderr: string };
374
+
375
+ export async function tryConvertOffice(sofficeTimeoutMs: number, src: string, signal?: AbortSignal, run: RunFn = runCapped, filter = "pdf"): Promise<OfficeResult> {
376
376
  const profileDir = mkdtempSync(join(tmpdir(), "pi-doc-soffice-prof-"));
377
377
  const outDir = mkdtempSync(join(tmpdir(), "pi-doc-soffice-out-"));
378
- const cleanup = () => {
379
- for (const d of [profileDir, outDir]) {
380
- try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ }
381
- }
382
- };
378
+ const cleanup = () => { for (const d of [profileDir, outDir]) try { rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } };
383
379
  try {
384
380
  const env = { ...process.env, SAL_USE_VCLPLUGIN: "svp", OOO_DISABLE_RECOVERY: "1", SAL_NO_MOUSEGRABS: "1" };
385
- const r = await run("soffice", soffArgs(src, profileDir, outDir), { timeoutMs: sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
381
+ const r = await run("soffice", soffArgs(src, profileDir, outDir, filter), { timeoutMs: sofficeTimeoutMs, capBytes: OUTPUT_MAX_BYTES, env, signal });
386
382
  if (signal?.aborted) throw new Error("aborted");
387
- if (r.code === null && !r.timedOut) {
388
- 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.");
389
- }
390
- if (r.timedOut || r.code !== 0) throw new Error(`soffice failed (code=${r.code} timedOut=${r.timedOut}): ${r.stderr.slice(0, 500)}`);
391
- const base = basename(src).replace(/\.[^.]+$/, "");
392
- const pdfPath = join(outDir, `${base}.pdf`);
383
+ const fail = (kind: "missing" | "timeout" | "exit" | "no-pdf"): OfficeResult => { cleanup(); return { ok: false, kind, code: r.code, timedOut: r.timedOut, stderr: r.stderr.slice(0, 500) }; };
384
+ if (r.code === null && !r.timedOut) return fail("missing");
385
+ if (r.timedOut) return fail("timeout");
386
+ if (r.code !== 0) return fail("exit");
387
+ const pdfPath = join(outDir, `${basename(src).replace(/\.[^.]+$/, "")}.pdf`);
393
388
  const st = statSync(pdfPath, { throwIfNoEntry: false });
394
- if (!st || !st.isFile() || st.size === 0) {
395
- 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.");
396
- }
397
- return { pdfPath, cleanup };
389
+ if (!st || !st.isFile() || st.size === 0) return fail("no-pdf");
390
+ return { ok: true, pdfPath, cleanup };
398
391
  } catch (e) { cleanup(); throw e; }
399
392
  }
400
393
 
394
+ export async function convertOffice(sofficeTimeoutMs: number, src: string, signal?: AbortSignal, run: RunFn = runCapped): Promise<{ pdfPath: string; cleanup: () => void }> {
395
+ const r = await tryConvertOffice(sofficeTimeoutMs, src, signal, run);
396
+ if (r.ok) return r;
397
+ if (r.kind === "missing") 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.");
398
+ if (r.kind === "no-pdf") 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.");
399
+ throw new Error(`soffice failed (code=${r.code} timedOut=${r.timedOut}): ${r.stderr}`);
400
+ }
401
+
401
402
 
402
403
  // --- Conversion tiers and bundle orchestration ---
403
404
 
@@ -405,13 +406,14 @@ export const DEGRADED_TEXT = "PyMuPDF text extraction - layout/tables not preser
405
406
  export const DEGRADED_UNPDF = "unpdf text extraction - structure not preserved";
406
407
  export const EXCEL_REMEDY = "Remedy: install uv, or pip install openpyxl xlrd pillow";
407
408
 
408
- export type Mode = "info" | "pdf-primary" | "pdf-fallback" | "xlsx" | "pdf-text";
409
- export interface TierJson { markdown?: string; pages?: number[]; pageCount?: number; emptyPages?: number[]; failedPages?: { page: number; error: string }[]; notes?: string[]; images?: { sheetIndex: number; file: string }[]; metadata?: Record<string, string>; toc?: [number, string, number][]; sheets?: SheetInfo[]; }
409
+ export type Mode = "info" | "pdf-primary" | "pdf-fallback" | "xlsx" | "pdf-text" | "render-pages";
410
+ export interface TierJson { markdown?: string; pages?: number[]; pageCount?: number; emptyPages?: number[]; failedPages?: { page: number; error: string }[]; notes?: string[]; images?: { sheetIndex: number; file: string }[]; metadata?: Record<string, string>; toc?: [number, string, number][]; sheets?: SheetInfo[]; renderPages?: number[]; sheetCount?: number; ok?: boolean; reason?: string; rendered?: { idx: number; file: string; dpi: number }[]; failed?: { idx: number; reason: string }[]; }
410
411
  export type TierResult = { ok: true; json: TierJson } | { ok: false; reason: string; detail?: string } | { ok: false; userError: string; pageCount?: number };
411
412
 
412
413
  export interface PipelineSeams {
413
414
  backend: (cfg: BackendConfig) => Promise<Backend>;
414
415
  runTier: (mode: Mode, childOptions: Record<string, unknown>, bundle: Pick<Bundle, "stagingDir">, signal: AbortSignal | undefined, timeoutMs: number, backend: Backend) => Promise<TierResult>;
416
+ office: typeof tryConvertOffice;
415
417
  }
416
418
 
417
419
  export function resolveUnpdfWorker(candidates: string[], exists: (path: string) => boolean): string {
@@ -463,8 +465,19 @@ function detailSuffix(r: Extract<TierResult, { ok: false; reason: string }>): st
463
465
  return r.detail ? ` (${r.detail})` : "";
464
466
  }
465
467
 
468
+ export function reconcileRenderMarkers(md: string, renderPages: number[], fmt: string, sourceMap: Map<string, string>, reason: (idx: number) => string): string {
469
+ for (const idx of renderPages) {
470
+ const file = `s${idx}.${fmt}`;
471
+ const ok = sourceMap.has(file);
472
+ md = md.replace(`<!--rv:${idx}-->`, ok ? `Rendered view: ![Rendered view of sheet ${idx}](${file})` : `Rendered view: unavailable (${reason(idx)})`);
473
+ md = md.replace(`<!--rvs:${idx}-->`, ok ? "yes" : "no");
474
+ }
475
+ if (/<!--rvs?:\d+-->/.test(md)) throw new Error("internal: unresolved render marker");
476
+ return md;
477
+ }
478
+
466
479
  export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, seams?: Partial<PipelineSeams>): Promise<ConvertOutcome> {
467
- const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, ...seams };
480
+ const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, office: tryConvertOffice, ...seams };
468
481
  const inputPath = resolve(o.path);
469
482
  const st = statSync(inputPath, { throwIfNoEntry: false });
470
483
  if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
@@ -479,17 +492,40 @@ export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, s
479
492
  try {
480
493
  let pdfPath = inputPath;
481
494
  if (type === "docx" || type === "pptx") { office = await convertOffice(o.sofficeTimeoutMs, inputPath, signal); pdfPath = office.pdfPath; }
482
- 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 };
495
+ const base = { path: pdfPath, pages: o.pages, stagingDir: b.stagingDir, sheetsStagingDir: b.sheetsStagingDir, imageDpi: o.imageDpi, imageFormat: o.imageFormat, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion };
483
496
  let tier: Tier, engine: Engine, json: TierJson, degraded: string | null = null, fallbackReason: string | null = null;
497
+ let notes: string[] = [];
484
498
  if (isExcel) {
485
499
  const r = await s.runTier("xlsx", base, b, signal, o.excelTimeoutMs, backend);
486
500
  if (!r.ok) {
487
501
  if ("userError" in r) throw new Error(r.userError);
488
- const remedy = r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes" ? ". Remedy: raise excelTimeoutMs or lower maxCellsPerSheet" : "";
502
+ const remedy = r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes" ? ". Remedy: raise excelTimeoutMs" : "";
489
503
  throw new Error(`Excel conversion failed: ${r.reason}${detailSuffix(r)}${remedy}`);
490
504
  }
491
- publishSheetImages(b);
492
- tier = "excel"; engine = type === "xls" ? "xlrd" : "openpyxl"; json = r.json;
505
+ publishSheetImages(b); publishSheetCsvs(b);
506
+ tier = "excel"; engine = type === "xls" ? "xlrd" : "openpyxl"; json = r.json; notes = [...(json.notes ?? [])];
507
+ const renderPages = json.renderPages ?? [];
508
+ let skip: string | null = null;
509
+ const perSheet = new Map<number, string>();
510
+ if (renderPages.length) {
511
+ const off = await s.office(o.sofficeTimeoutMs, inputPath, signal, runCapped, EXCEL_PDF_FILTER);
512
+ if (!off.ok) skip = off.kind === "missing" ? "LibreOffice not found" : off.kind === "timeout" ? `soffice failed: timeout after ${o.sofficeTimeoutMs}ms` : off.kind === "exit" ? `soffice failed: exit ${off.code}` : "soffice produced no PDF";
513
+ else {
514
+ try {
515
+ const rp = await s.runTier("render-pages", { path: off.pdfPath, sheetIndices: renderPages, expectedPages: json.sheetCount, imageDpi: o.imageDpi, imageFormat: o.imageFormat, stagingDir: b.stagingDir, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion }, b, signal, o.fallbackTimeoutMs, backend);
516
+ if (!rp.ok) skip = `render failed: ${"userError" in rp ? rp.userError : rp.reason}`;
517
+ else if (rp.json.ok === false) skip = rp.json.reason ?? "render failed";
518
+ else {
519
+ publishSheetImages(b);
520
+ for (const f of rp.json.failed ?? []) perSheet.set(f.idx, f.reason);
521
+ for (const d of rp.json.rendered ?? []) if (d.dpi < o.imageDpi) notes.push(`Rendered view s${d.idx}: rendered at ${d.dpi} dpi`);
522
+ }
523
+ } finally { off.cleanup(); }
524
+ }
525
+ if (skip) notes.push(`Rendered views skipped: ${skip}`);
526
+ else if (perSheet.size) notes.push(`Rendered views: ${perSheet.size} of ${renderPages.length} unavailable`);
527
+ }
528
+ json = { ...json, markdown: reconcileRenderMarkers(json.markdown ?? "", renderPages, o.imageFormat, b.sourceMap, (idx) => perSheet.get(idx) ?? skip ?? "render failed") };
493
529
  } else if (backend.kind === "none") {
494
530
  const r = await s.runTier("pdf-text", base, b, signal, o.primaryTimeoutMs, backend);
495
531
  if (!r.ok) throw new Error("userError" in r ? r.userError : `Conversion failed: unpdf ${r.reason}${detailSuffix(r)}`);
@@ -508,25 +544,26 @@ export async function convertDocument(o: DocToMdOptions, signal?: AbortSignal, s
508
544
  tier = "fallback"; engine = "pymupdf-text"; json = f.json; degraded = DEGRADED_TEXT; fallbackReason = `primary ${p.reason}`;
509
545
  }
510
546
  }
511
- let body = rewriteImageLinks(json.markdown ?? "", b.sourceMap);
512
- validateImageLinks(body, b.manifest);
547
+ if (!isExcel) notes = json.notes ?? [];
548
+ const body = rewriteLinks(json.markdown ?? "", b.sourceMap);
549
+ validateImageLinks(body, b.manifest, b.csvManifest);
513
550
  const head: string[] = [];
514
551
  if (degraded) head.push(`Degraded: ${degraded}`);
515
552
  if (fallbackReason) head.push(`Fallback-Reason: ${fallbackReason}`);
516
553
  if (json.failedPages?.length) head.push(`Failed pages: ${json.failedPages.map((f) => `${f.page} (${f.error})`).join("; ")}`);
517
554
  if (json.emptyPages?.length) head.push(`Empty pages: ${json.emptyPages.join(", ")}`);
518
- for (const n of json.notes ?? []) head.push(`Notes: ${n}`);
555
+ for (const n of notes) head.push(`Notes: ${n}`);
519
556
  const markdown = (head.length ? `${head.join("\n")}\n\n` : "") + body;
520
557
  commitBundle(b, markdown);
521
558
  const outline = scanOutline(markdown, o.outlineMaxEntries);
522
- const details: DocToMdDetails = { 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 };
559
+ const details: DocToMdDetails = { path: inputPath, backend: backend.kind, pymupdfVersion: o.pymupdfVersion, inputType: type, file: b.mdPath, outputDir: b.root, savedTo: b.mdPath, imagesDir: b.imagesDir, sheetsDir: b.csvManifest.size ? b.sheetsDir : null, 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, outline: outline.entries, outlineTotal: outline.total };
523
560
  return { output: formatHandle(details), details };
524
561
  } catch (e) { abortBundle(b); throw e; }
525
562
  finally { office?.cleanup(); }
526
563
  }
527
564
 
528
565
  export async function inspectDocument(o: DocToMdOptions, signal?: AbortSignal, seams?: Partial<PipelineSeams>): Promise<{ output: string; details: InfoData }> {
529
- const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, ...seams };
566
+ const s: PipelineSeams = { backend: (c) => getBackend(c, undefined, signal), runTier: runTierReal, office: tryConvertOffice, ...seams };
530
567
  const inputPath = resolve(o.path);
531
568
  const st = statSync(inputPath, { throwIfNoEntry: false });
532
569
  if (!st || !st.isFile()) throw new Error(`Not a readable file: ${o.path}`);
@@ -541,7 +578,7 @@ export async function inspectDocument(o: DocToMdOptions, signal?: AbortSignal, s
541
578
  const r = await s.runTier("info", { path, maxOutputBytes: o.maxOutputBytes, pymupdfVersion: o.pymupdfVersion }, { stagingDir: "" }, signal, isExcel ? o.excelTimeoutMs : o.fallbackTimeoutMs, backend);
542
579
  if (!r.ok) {
543
580
  if ("userError" in r) throw new Error(r.userError);
544
- 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)}`);
581
+ if (isExcel && (r.reason.startsWith("timeout after") || r.reason === "output exceeded maxOutputBytes")) throw new Error(`Excel inspection failed: ${r.reason}. Remedy: raise excelTimeoutMs${detailSuffix(r)}`);
545
582
  throw new Error(`Inspection failed: ${r.reason}${detailSuffix(r)}`);
546
583
  }
547
584
  const toc: TocEntry[] = (r.json.toc ?? []).map(([level, title, page]) => ({ level, title, page }));
@@ -6,10 +6,20 @@ export type BackendKind = "uv" | "python" | "venv" | "none";
6
6
 
7
7
  export interface OutlineEntry { line: number; level: number; title: string; }
8
8
  export interface TocEntry { level: number; title: string; page: number; }
9
- export interface SheetInfo { index: number; name: string; hidden: boolean; rows: number | null; cols: number | null; hiddenRows: number; hiddenCols: number; }
9
+ export interface SheetInfo {
10
+ index: number;
11
+ name: string;
12
+ kind: "worksheet" | "chartsheet";
13
+ hidden: boolean;
14
+ rows: number | null; cols: number | null;
15
+ hiddenRows: number; hiddenCols: number;
16
+ charts: number; images: number;
17
+ rendered: boolean;
18
+ csv: string | null;
19
+ }
10
20
 
11
21
  export interface HandleData {
12
- savedTo: string; imagesDir: string | null; type: InputType; engine: Engine; tier: Tier;
22
+ savedTo: string; imagesDir: string | null; sheetsDir: string | null; type: InputType; engine: Engine; tier: Tier;
13
23
  pageCount: number | null; pages: number[] | null; imageCount: number; bytes: number; lines: number;
14
24
  degraded: string | null; fallbackReason: string | null; failedPages: number[]; emptyPages: number[];
15
25
  notes: string[]; outline: OutlineEntry[]; outlineTotal: number;
@@ -73,6 +83,7 @@ function outlineLines(entries: OutlineEntry[], total: number): string[] {
73
83
  export function formatHandle(h: HandleData): string {
74
84
  const lines = [`Saved-To: ${h.savedTo}`];
75
85
  if (h.imagesDir && h.imageCount > 0) lines.push(`Images-Dir: ${h.imagesDir}`);
86
+ if (h.sheetsDir) lines.push(`Sheets-Dir: ${h.sheetsDir}`);
76
87
  lines.push(`Type: ${h.type} Engine: ${h.engine} Tier: ${h.tier}`);
77
88
  lines.push(`Page-Count: ${h.pageCount ?? "?"} Pages: ${h.pages ? compactRanges(h.pages) : "all"} Images: ${h.imageCount} Size: ${formatSize(h.bytes)} / ${h.lines} lines`);
78
89
  if (h.degraded) lines.push(`Degraded: ${h.degraded}`);
@@ -90,7 +101,7 @@ export function formatInfoHandle(i: InfoData, max: number): string {
90
101
  if (i.sheets) {
91
102
  const lines = [`Type: ${i.type} Sheets: ${i.sheetsTotal}`];
92
103
  for (const s of i.sheets.slice(0, max)) {
93
- const dims = `rows=${s.rows ?? "?"} cols=${s.cols ?? "?"}`;
104
+ const dims = `${s.kind} rows=${s.rows ?? "-"} cols=${s.cols ?? "-"} charts=${s.charts} images=${s.images}`;
94
105
  const hidden = s.hiddenRows || s.hiddenCols ? ` hiddenRows=${s.hiddenRows} hiddenCols=${s.hiddenCols}` : "";
95
106
  lines.push(` ${trunc(s.name, TITLE_MAX)} ${s.hidden ? "hidden " : ""}${dims}${hidden}`);
96
107
  }
@@ -16,7 +16,6 @@ export interface Tunables {
16
16
  pymupdfVersion: string;
17
17
  imageDpi: number;
18
18
  imageFormat: ImageFormat;
19
- maxCellsPerSheet: number;
20
19
  maxOutputBytes: number;
21
20
  outlineMaxEntries: number;
22
21
  }
@@ -63,14 +62,13 @@ export const DOC_TO_MD_OPTIONS: readonly OptionDescriptor[] = [
63
62
  { key: "outputDir", flag: "--output-dir", type: "string", default: null, settable: false, help: "Bundle root for <stem>.md + images/; default a per-call temp dir" },
64
63
  { key: "overwrite", flag: "--overwrite", type: "bool", default: false, settable: false, help: "Replace an existing completed <stem>.md bundle" },
65
64
  { key: "primaryTimeoutMs", flag: "--primary-timeout", type: "int", default: 60000, settable: true, env: "PI_DOC_TO_MD_CONVERT_TIMEOUT_MS", help: "pymupdf4llm tier; also the unpdf tier" },
66
- { key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 30000, settable: true, help: "PyMuPDF get_text tier; also info on PDF" },
67
- { key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice" },
65
+ { key: "fallbackTimeoutMs", flag: "--fallback-timeout", type: "int", default: 30000, settable: true, help: "PyMuPDF get_text tier; also info on PDF and Excel rendered views" },
66
+ { key: "sofficeTimeoutMs", flag: "--soffice-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_SOFFICE_TIMEOUT_MS", help: "DOCX/PPTX -> PDF via LibreOffice; also Excel rendered views" },
68
67
  { key: "excelTimeoutMs", flag: "--excel-timeout", type: "int", default: 60000, settable: true, help: "Excel child (both openpyxl loads); also info on Excel" },
69
68
  { key: "warmTimeoutMs", flag: "--warm-timeout", type: "int", default: 120000, settable: true, env: "PI_DOC_TO_MD_WARM_TIMEOUT_MS", help: "Absolute backend discovery/bootstrap deadline (first call per process)" },
70
69
  { 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)" },
71
- { key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images" },
70
+ { key: "imageDpi", flag: "--image-dpi", type: "int", default: 150, settable: true, help: "Render DPI for page images and Excel rendered views" },
72
71
  { key: "imageFormat", flag: "--image-format", type: "enum", default: "png", settable: true, enumValues: ["png", "jpg"], help: "Rendered image format (embedded images keep their native extension)" },
73
- { key: "maxCellsPerSheet", flag: "--max-cells-per-sheet", type: "int", default: 50000, settable: true, help: "rows x cols budget per worksheet" },
74
72
  { key: "maxOutputBytes", flag: "--max-output-bytes", type: "int", default: 20000000, settable: true, help: "Child stdout cap in bytes" },
75
73
  { key: "outlineMaxEntries", flag: "--outline-max-entries", type: "int", default: 40, settable: true, help: "Heading outline / TOC / sheet inventory cap in the handle" },
76
74
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quiver",
3
- "version": "5.5.0",
3
+ "version": "6.0.0",
4
4
  "description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",