@oh-my-pi/pi-coding-agent 17.3.2 → 17.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/{CHANGELOG-fr2awajz.md → CHANGELOG-trcc215s.md} +21 -0
- package/dist/cli.js +3558 -3530
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/markit/converters/pdf/index.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/tools/read-pdf.d.ts +15 -0
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +3 -4
- package/package.json +13 -16
- package/scripts/build-binary.ts +0 -2
- package/scripts/bundle-dist.ts +0 -1
- package/src/cli/read-cli.ts +2 -0
- package/src/markit/NOTICE +8 -8
- package/src/markit/converters/pdf/index.ts +14 -126
- package/src/mcp/client.ts +8 -9
- package/src/modes/controllers/event-controller.ts +57 -11
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/empty-stop-retry.md +1 -1
- package/src/session/agent-session.ts +8 -1
- package/src/session/turn-recovery.ts +59 -55
- package/src/tools/read-pdf.ts +135 -0
- package/src/tools/read.ts +63 -37
- package/src/utils/external-editor.ts +24 -5
- package/src/utils/markit.ts +6 -44
- package/dist/types/markit/converters/pdf/columns.d.ts +0 -35
- package/dist/types/markit/converters/pdf/extract.d.ts +0 -10
- package/dist/types/markit/converters/pdf/grid.d.ts +0 -25
- package/dist/types/markit/converters/pdf/headers.d.ts +0 -24
- package/dist/types/markit/converters/pdf/render.d.ts +0 -24
- package/dist/types/markit/converters/pdf/types.d.ts +0 -75
- package/dist/types/tools/read-pdf-images.d.ts +0 -12
- package/dist/types/utils/mupdf-wasm-embed.d.ts +0 -1
- package/scripts/embed-mupdf-wasm.ts +0 -67
- package/src/markit/converters/pdf/columns.ts +0 -103
- package/src/markit/converters/pdf/extract.ts +0 -598
- package/src/markit/converters/pdf/grid.ts +0 -780
- package/src/markit/converters/pdf/headers.ts +0 -106
- package/src/markit/converters/pdf/render.ts +0 -501
- package/src/markit/converters/pdf/types.ts +0 -84
- package/src/tools/read-pdf-images.ts +0 -250
- package/src/utils/mupdf-wasm-embed.ts +0 -12
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Table grid detection from vector segments and text boxes.
|
|
3
|
-
*
|
|
4
|
-
* Ported from @oharato/pdf2md-ts with TypeScript types and without
|
|
5
|
-
* CJK-specific borderless table heuristics. The core algorithm:
|
|
6
|
-
*
|
|
7
|
-
* 1. Classify segments as horizontal or vertical lines
|
|
8
|
-
* 2. Group horizontal Y-lines into table groups (split by vertical gaps)
|
|
9
|
-
* 3. For each group:
|
|
10
|
-
* a. Full grid (H+V lines): build cells from grid intersections,
|
|
11
|
-
* place text via raycasting
|
|
12
|
-
* b. H-line only (no V lines): infer columns from text X positions
|
|
13
|
-
* 4. Prune empty rows/cols
|
|
14
|
-
*
|
|
15
|
-
* Coordinate system: PDF native (bottom-left origin, Y increases upward).
|
|
16
|
-
*/
|
|
17
|
-
import type { Segment, TableGrid, TextBox } from "./types.js";
|
|
18
|
-
export interface GridResult {
|
|
19
|
-
grids: TableGrid[];
|
|
20
|
-
consumedIds: string[];
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Detect all table grids on a single page from its text boxes and segments.
|
|
24
|
-
*/
|
|
25
|
-
export declare function resolveTableGrids(pageNumber: number, textBoxes: TextBox[], segments: Segment[]): GridResult;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Running header/footer detection and removal.
|
|
3
|
-
*
|
|
4
|
-
* Many PDFs have repeated text at the top or bottom of every page:
|
|
5
|
-
* document titles, chapter names, page numbers, copyright notices.
|
|
6
|
-
* These pollute the markdown output as false headings or noise.
|
|
7
|
-
*
|
|
8
|
-
* Algorithm:
|
|
9
|
-
* 1. For each page, bucket text boxes by Y position (top/bottom zones)
|
|
10
|
-
* 2. Collect the text content at each zone across all pages
|
|
11
|
-
* 3. Text appearing on >20% of pages OR 8+ consecutive pages is a
|
|
12
|
-
* running header/footer
|
|
13
|
-
* 4. Remove matching text boxes before further processing
|
|
14
|
-
*/
|
|
15
|
-
import type { PageContent } from "./types.js";
|
|
16
|
-
/**
|
|
17
|
-
* Detect and remove running headers and footers from all pages.
|
|
18
|
-
* Mutates the pages array in place, removing header/footer text boxes.
|
|
19
|
-
*
|
|
20
|
-
* Uses two strategies:
|
|
21
|
-
* 1. Global frequency: text appearing on > 20% of all pages
|
|
22
|
-
* 2. Consecutive runs: text appearing on 8+ consecutive pages
|
|
23
|
-
*/
|
|
24
|
-
export declare function stripHeadersFooters(pages: PageContent[]): void;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Markdown rendering for PDF pages.
|
|
3
|
-
*
|
|
4
|
-
* Converts table grids and free text boxes into markdown, handling:
|
|
5
|
-
* - Table grid → markdown table (`| col | col |`)
|
|
6
|
-
* - Free text → paragraphs with heading detection (by font size)
|
|
7
|
-
* - Content ordering (top-to-bottom via Y coordinate)
|
|
8
|
-
* - Paragraph wrap merging (lines broken across PDF line boundaries)
|
|
9
|
-
* - Page number removal
|
|
10
|
-
*
|
|
11
|
-
* Ported from @oharato/pdf2md-ts, stripped of CJK/TDnet-specific logic.
|
|
12
|
-
*/
|
|
13
|
-
import type { TableGrid, TextBox } from "./types.js";
|
|
14
|
-
/**
|
|
15
|
-
* Render a TableGrid as a markdown table.
|
|
16
|
-
*/
|
|
17
|
-
export declare function renderTableToMarkdown(table: TableGrid): string;
|
|
18
|
-
/**
|
|
19
|
-
* Render one page's content: free text and tables interleaved top-to-bottom.
|
|
20
|
-
*/
|
|
21
|
-
export declare function renderPageContent(freeTextBoxes: TextBox[], tables: TableGrid[], imageBlocks?: Array<{
|
|
22
|
-
topY: number;
|
|
23
|
-
markdown: string;
|
|
24
|
-
}>, allTextBoxes?: TextBox[]): string;
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/** Bounding box in PDF coordinate space (origin = bottom-left). */
|
|
2
|
-
export type Bounds = {
|
|
3
|
-
left: number;
|
|
4
|
-
right: number;
|
|
5
|
-
/** Higher value = higher on the page. */
|
|
6
|
-
top: number;
|
|
7
|
-
bottom: number;
|
|
8
|
-
};
|
|
9
|
-
/** A text fragment with position and font metadata. */
|
|
10
|
-
export type TextBox = {
|
|
11
|
-
id: string;
|
|
12
|
-
text: string;
|
|
13
|
-
bounds: Bounds;
|
|
14
|
-
pageNumber: number;
|
|
15
|
-
/** Dominant font size in points. */
|
|
16
|
-
fontSize: number;
|
|
17
|
-
/** True if rendered bold (font name or rendering mode). */
|
|
18
|
-
isBold: boolean;
|
|
19
|
-
};
|
|
20
|
-
/** A horizontal or vertical line segment extracted from vector graphics. */
|
|
21
|
-
export type Segment = {
|
|
22
|
-
id: string;
|
|
23
|
-
x1: number;
|
|
24
|
-
y1: number;
|
|
25
|
-
x2: number;
|
|
26
|
-
y2: number;
|
|
27
|
-
};
|
|
28
|
-
/** A single cell in a resolved table grid. */
|
|
29
|
-
export type TableCell = {
|
|
30
|
-
row: number;
|
|
31
|
-
col: number;
|
|
32
|
-
text: string;
|
|
33
|
-
rowSpan: number;
|
|
34
|
-
colSpan: number;
|
|
35
|
-
};
|
|
36
|
-
/** A resolved table grid ready for markdown rendering. */
|
|
37
|
-
export type TableGrid = {
|
|
38
|
-
pageNumber: number;
|
|
39
|
-
rows: number;
|
|
40
|
-
cols: number;
|
|
41
|
-
cells: TableCell[];
|
|
42
|
-
warnings: string[];
|
|
43
|
-
/** Top Y coordinate (PDF space: larger = higher on page). */
|
|
44
|
-
topY: number;
|
|
45
|
-
/** True for tables detected without vector borders. */
|
|
46
|
-
isBorderless: boolean;
|
|
47
|
-
};
|
|
48
|
-
/** An image/diagram region detected on a page. */
|
|
49
|
-
export type ImageRegion = {
|
|
50
|
-
id: string;
|
|
51
|
-
pageNumber: number;
|
|
52
|
-
/** Bounding box in mupdf coordinates (top-left origin). */
|
|
53
|
-
bbox: {
|
|
54
|
-
x: number;
|
|
55
|
-
y: number;
|
|
56
|
-
w: number;
|
|
57
|
-
h: number;
|
|
58
|
-
};
|
|
59
|
-
/** Y position in PDF coordinates (bottom-left) for ordering. */
|
|
60
|
-
topY: number;
|
|
61
|
-
};
|
|
62
|
-
/** Result of extracting content from a single PDF page. */
|
|
63
|
-
export type PageContent = {
|
|
64
|
-
pageNumber: number;
|
|
65
|
-
textBoxes: TextBox[];
|
|
66
|
-
segments: Segment[];
|
|
67
|
-
images: ImageRegion[];
|
|
68
|
-
};
|
|
69
|
-
/** A block of rendered content (text paragraph or table). */
|
|
70
|
-
export type ContentBlock = {
|
|
71
|
-
topY: number;
|
|
72
|
-
content: string;
|
|
73
|
-
/** True if this line has wide gaps between text boxes (column headers). */
|
|
74
|
-
isTabular?: boolean;
|
|
75
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { ToolSession } from "../sdk.js";
|
|
3
|
-
import type { ReadToolDetails } from "./read.js";
|
|
4
|
-
export declare function rewritePdfImagePlaceholders(markdown: string, pdfPath: string): string;
|
|
5
|
-
export declare function splitPdfImageMemberReadPath(readPath: string): {
|
|
6
|
-
pdfPath: string;
|
|
7
|
-
member: string;
|
|
8
|
-
} | null;
|
|
9
|
-
export declare function readPdfImageMember(session: ToolSession, autoResizeImages: boolean, absolutePdfPath: string, pdfDisplayPath: string, member: string, suffixResolution: {
|
|
10
|
-
from: string;
|
|
11
|
-
to: string;
|
|
12
|
-
} | undefined, signal?: AbortSignal): Promise<AgentToolResult<ReadToolDetails>>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function loadEmbeddedMupdfWasm(): Uint8Array | undefined;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
// Embeds mupdf's `mupdf-wasm.wasm` into the compiled single-file binary.
|
|
4
|
-
//
|
|
5
|
-
// mupdf loads its wasm by reading the `mupdf-wasm.wasm` sibling of its own
|
|
6
|
-
// module via `new URL(..., import.meta.url)` + `readFileSync`. A `bun --compile`
|
|
7
|
-
// binary has no node_modules, so that read fails (`ENOENT .../mupdf-wasm.wasm`),
|
|
8
|
-
// and marking mupdf `--external` instead makes `bun --compile` eagerly fail to
|
|
9
|
-
// resolve the package at startup (the static `import * as mupdf` lives in a lazy
|
|
10
|
-
// chunk but is hoisted). So the binary build bundles mupdf and embeds the wasm
|
|
11
|
-
// bytes here, handing them to the WASM module as `$libmupdf_wasm_Module.wasmBinary`
|
|
12
|
-
// (see src/utils/markit.ts).
|
|
13
|
-
//
|
|
14
|
-
// `--generate` copies the wasm next to src/utils/mupdf-wasm-embed.ts and rewrites
|
|
15
|
-
// that module to import it via `with { type: "file" }`; `--reset` restores the
|
|
16
|
-
// checked-in placeholder and removes the copy. The npm `dist/cli.js` bundle never
|
|
17
|
-
// runs this — it keeps mupdf external and loads the wasm from node_modules.
|
|
18
|
-
|
|
19
|
-
import * as fs from "node:fs/promises";
|
|
20
|
-
import { createRequire } from "node:module";
|
|
21
|
-
import * as path from "node:path";
|
|
22
|
-
|
|
23
|
-
const utilsDir = path.join(import.meta.dir, "..", "src", "utils");
|
|
24
|
-
const helperPath = path.join(utilsDir, "mupdf-wasm-embed.ts");
|
|
25
|
-
const wasmCopyPath = path.join(utilsDir, "mupdf-wasm.wasm");
|
|
26
|
-
|
|
27
|
-
const placeholder = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit by hand.
|
|
28
|
-
//
|
|
29
|
-
// Compiled single-file binaries cannot let mupdf resolve its \`mupdf-wasm.wasm\`
|
|
30
|
-
// sibling from the read-only bunfs, so the binary build (scripts/build-binary.ts
|
|
31
|
-
// and scripts/ci-release-build-binaries.ts) regenerates this module to embed the
|
|
32
|
-
// wasm bytes via \`with { type: "file" }\` and copies the wasm next to it. Source
|
|
33
|
-
// checkouts, \`bun test\`, and the npm \`dist/cli.js\` bundle keep mupdf external and
|
|
34
|
-
// load the wasm from node_modules, so this placeholder returns undefined and the
|
|
35
|
-
// build resets back to it afterward.
|
|
36
|
-
export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
|
|
37
|
-
\treturn undefined;
|
|
38
|
-
}
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
const generated = `// AUTOGENERATED -- managed by scripts/embed-mupdf-wasm.ts. Do not edit or commit.
|
|
42
|
-
import { readFileSync } from "node:fs";
|
|
43
|
-
import wasmPath from "./mupdf-wasm.wasm" with { type: "file" };
|
|
44
|
-
|
|
45
|
-
export function loadEmbeddedMupdfWasm(): Uint8Array | undefined {
|
|
46
|
-
\treturn readFileSync(wasmPath);
|
|
47
|
-
}
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
|
-
if (process.argv.includes("--reset")) {
|
|
51
|
-
await Bun.write(helperPath, placeholder);
|
|
52
|
-
try {
|
|
53
|
-
await fs.unlink(wasmCopyPath);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
|
|
56
|
-
}
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const wasmSource = path.join(path.dirname(createRequire(import.meta.url).resolve("mupdf")), "mupdf-wasm.wasm");
|
|
61
|
-
const wasmFile = Bun.file(wasmSource);
|
|
62
|
-
if (!(await wasmFile.exists())) {
|
|
63
|
-
throw new Error(`mupdf wasm not found at ${wasmSource}; run \`bun install\` first.`);
|
|
64
|
-
}
|
|
65
|
-
await Bun.write(wasmCopyPath, wasmFile);
|
|
66
|
-
await Bun.write(helperPath, generated);
|
|
67
|
-
console.log(`Embedded mupdf wasm (${wasmFile.size} bytes) into ${path.relative(process.cwd(), wasmCopyPath)}`);
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// Adapted from markit-ai (MIT). See ../../NOTICE.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Multi-column layout detection and text box reordering.
|
|
5
|
-
*
|
|
6
|
-
* Many PDFs (legal documents, datasheets, academic papers) use two-column
|
|
7
|
-
* layouts. Without column detection, text boxes are ordered by Y position
|
|
8
|
-
* only, interleaving left and right column content.
|
|
9
|
-
*
|
|
10
|
-
* Algorithm:
|
|
11
|
-
* 1. Collect left edges of all text boxes on the page
|
|
12
|
-
* 2. Find the largest horizontal gap between consecutive left edges
|
|
13
|
-
* 3. If gap > MIN_GAP_RATIO of the text width and both sides have
|
|
14
|
-
* enough boxes → multi-column detected
|
|
15
|
-
* 4. Assign each text box to a column based on its center X
|
|
16
|
-
* 5. Return columns in reading order (left-to-right, top-to-bottom)
|
|
17
|
-
*
|
|
18
|
-
* This only detects the column structure. The caller is responsible for
|
|
19
|
-
* processing each column's text boxes independently (table detection,
|
|
20
|
-
* rendering, etc.).
|
|
21
|
-
*/
|
|
22
|
-
import type { TextBox } from "./types";
|
|
23
|
-
|
|
24
|
-
export interface ColumnLayout {
|
|
25
|
-
/** Number of columns detected (1 = single column, 2+ = multi-column). */
|
|
26
|
-
columnCount: number;
|
|
27
|
-
/** Text boxes grouped by column, in reading order (left to right). */
|
|
28
|
-
columns: TextBox[][];
|
|
29
|
-
/** X positions of column boundaries (between columns). */
|
|
30
|
-
boundaries: number[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Minimum gap as a fraction of the total text width to consider a column
|
|
35
|
-
* boundary. A two-column layout typically has ~50% gap; we use a lower
|
|
36
|
-
* threshold to catch asymmetric columns.
|
|
37
|
-
*/
|
|
38
|
-
const MIN_GAP_RATIO = 0.15;
|
|
39
|
-
/** Minimum number of text boxes on each side of the gap. */
|
|
40
|
-
const MIN_BOXES_PER_COLUMN = 4;
|
|
41
|
-
/** Minimum gap in absolute points to avoid splitting on small whitespace. */
|
|
42
|
-
const MIN_GAP_PTS = 40;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Detect column layout and return text boxes grouped by column.
|
|
46
|
-
*
|
|
47
|
-
* For single-column pages, returns all boxes in one group.
|
|
48
|
-
* For multi-column pages, returns boxes split by column in reading order.
|
|
49
|
-
*/
|
|
50
|
-
export function detectColumns(textBoxes: TextBox[]): ColumnLayout {
|
|
51
|
-
if (textBoxes.length < MIN_BOXES_PER_COLUMN * 2) {
|
|
52
|
-
return { columnCount: 1, columns: [textBoxes], boundaries: [] };
|
|
53
|
-
}
|
|
54
|
-
// Collect unique left edges (rounded to avoid float noise)
|
|
55
|
-
const lefts = [...new Set(textBoxes.map(tb => Math.round(tb.bounds.left)))].sort((a, b) => a - b);
|
|
56
|
-
if (lefts.length < 2) {
|
|
57
|
-
return { columnCount: 1, columns: [textBoxes], boundaries: [] };
|
|
58
|
-
}
|
|
59
|
-
const textXMin = lefts[0];
|
|
60
|
-
const textXMax = Math.max(...textBoxes.map(tb => Math.round(tb.bounds.right)));
|
|
61
|
-
const textWidth = textXMax - textXMin;
|
|
62
|
-
if (textWidth <= 0) {
|
|
63
|
-
return { columnCount: 1, columns: [textBoxes], boundaries: [] };
|
|
64
|
-
}
|
|
65
|
-
// Find the largest gap between consecutive left-edge positions
|
|
66
|
-
let maxGap = 0;
|
|
67
|
-
let gapLeft = 0;
|
|
68
|
-
let gapRight = 0;
|
|
69
|
-
for (let i = 1; i < lefts.length; i++) {
|
|
70
|
-
const gap = lefts[i] - lefts[i - 1];
|
|
71
|
-
if (gap > maxGap) {
|
|
72
|
-
maxGap = gap;
|
|
73
|
-
gapLeft = lefts[i - 1];
|
|
74
|
-
gapRight = lefts[i];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const gapRatio = maxGap / textWidth;
|
|
78
|
-
if (gapRatio < MIN_GAP_RATIO || maxGap < MIN_GAP_PTS) {
|
|
79
|
-
return { columnCount: 1, columns: [textBoxes], boundaries: [] };
|
|
80
|
-
}
|
|
81
|
-
// Split point is the midpoint of the gap
|
|
82
|
-
const splitX = (gapLeft + gapRight) / 2;
|
|
83
|
-
// Assign boxes to columns based on center X
|
|
84
|
-
const leftCol: TextBox[] = [];
|
|
85
|
-
const rightCol: TextBox[] = [];
|
|
86
|
-
for (const tb of textBoxes) {
|
|
87
|
-
const cx = (tb.bounds.left + tb.bounds.right) / 2;
|
|
88
|
-
if (cx < splitX) {
|
|
89
|
-
leftCol.push(tb);
|
|
90
|
-
} else {
|
|
91
|
-
rightCol.push(tb);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Validate both columns have enough content
|
|
95
|
-
if (leftCol.length < MIN_BOXES_PER_COLUMN || rightCol.length < MIN_BOXES_PER_COLUMN) {
|
|
96
|
-
return { columnCount: 1, columns: [textBoxes], boundaries: [] };
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
columnCount: 2,
|
|
100
|
-
columns: [leftCol, rightCol],
|
|
101
|
-
boundaries: [splitX],
|
|
102
|
-
};
|
|
103
|
-
}
|