@gavin-hu/my-pi-agent 0.15.0 → 0.16.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.
- package/README.md +12 -11
- package/README.zh-CN.md +9 -7
- package/extensions/bytefly/README.md +98 -116
- package/extensions/bytefly/notice/component.ts +6 -4
- package/extensions/bytefly/notice/config.ts +16 -2
- package/extensions/bytefly/panel/format.ts +3 -8
- package/extensions/bytefly/panel/join.ts +1 -6
- package/extensions/bytefly/panel/settings.ts +74 -377
- package/extensions/bytefly/panel/tui/entries.ts +16 -14
- package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
- package/extensions/bytefly/panel/tui/frame.ts +2 -2
- package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
- package/extensions/bytefly/panel/tui.ts +4 -6
- package/extensions/bytefly/picker/resources.ts +7 -3
- package/extensions/bytefly/working/register.ts +2 -8
- package/extensions/check/tools.ts +2 -11
- package/extensions/context/README.md +162 -0
- package/extensions/context/collect.ts +53 -0
- package/extensions/context/commands.ts +100 -0
- package/extensions/context/format.ts +82 -0
- package/extensions/context/index.ts +16 -0
- package/extensions/context/report.ts +319 -0
- package/extensions/context/tui.ts +268 -0
- package/extensions/context/types.ts +122 -0
- package/extensions/file-browse/README.md +83 -8
- package/extensions/file-browse/client.ts +3 -1
- package/extensions/file-browse/config.ts +23 -0
- package/extensions/file-browse/files.ts +183 -48
- package/extensions/file-browse/fuzzy.ts +65 -11
- package/extensions/file-browse/html.ts +78 -133
- package/extensions/file-browse/index.ts +17 -2
- package/extensions/file-browse/live-client.ts +158 -0
- package/extensions/file-browse/live.ts +256 -0
- package/extensions/file-browse/markdown.ts +3 -10
- package/extensions/file-browse/raw-html.ts +3 -10
- package/extensions/file-browse/router.ts +33 -8
- package/extensions/file-browse/server.ts +78 -13
- package/extensions/file-browse/styles.ts +124 -0
- package/extensions/file-open/README.md +1 -2
- package/extensions/{document → file-read}/README.md +72 -56
- package/extensions/{document → file-read}/config.ts +12 -9
- package/extensions/{document → file-read}/extract/doc.ts +2 -2
- package/extensions/{document → file-read}/extract/docx.ts +3 -3
- package/extensions/{document → file-read}/extract/file.ts +9 -9
- package/extensions/{document → file-read}/extract/odt.ts +3 -3
- package/extensions/{document → file-read}/extract/pdf.ts +3 -3
- package/extensions/{document → file-read}/extract/rtf.ts +3 -3
- package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
- package/extensions/{document → file-read}/formats.ts +11 -11
- package/extensions/file-read/index.ts +17 -0
- package/extensions/{document → file-read}/paging.ts +15 -13
- package/extensions/{document → file-read}/render.ts +17 -13
- package/extensions/{document → file-read}/schema.ts +7 -7
- package/extensions/{document → file-read}/tool.ts +36 -28
- package/extensions/goal/tools.ts +10 -14
- package/extensions/job/commands.ts +32 -46
- package/extensions/job/runtime.ts +6 -8
- package/extensions/job/tools.ts +7 -26
- package/extensions/memory/commands.ts +30 -44
- package/extensions/memory/tools.ts +6 -14
- package/extensions/plan/tools/enter.ts +6 -7
- package/extensions/plan/tools/exit.ts +6 -7
- package/extensions/plan/tools/steps.ts +6 -7
- package/extensions/plan/tools/write.ts +8 -9
- package/extensions/plan/web-router.ts +2 -12
- package/extensions/plan/web.ts +30 -15
- package/extensions/question/tools.ts +5 -4
- package/extensions/rewind/checklist.ts +10 -3
- package/extensions/rewind/git.ts +27 -14
- package/extensions/rewind/hunk-picker.ts +1 -1
- package/extensions/rewind/path-picker.ts +1 -1
- package/extensions/rewind/review-tui.ts +3 -3
- package/extensions/rewind/snapshot.ts +13 -4
- package/extensions/subagent/background-tui.ts +1 -1
- package/extensions/subagent/background.ts +1 -7
- package/extensions/todo/tools.ts +10 -14
- package/extensions/watch/README.md +5 -5
- package/extensions/watch/commands.ts +33 -47
- package/extensions/watch/runtime.ts +5 -7
- package/extensions/watch/tools.ts +7 -18
- package/extensions/web-access/README.md +3 -3
- package/extensions/web-access/search/search.ts +9 -18
- package/extensions/wechat/timer.ts +9 -30
- package/extensions/wechat/tools.ts +4 -7
- package/extensions/worktree/status.ts +14 -9
- package/lib/README.md +12 -8
- package/lib/escape.ts +15 -2
- package/lib/git/read.ts +20 -0
- package/lib/http/headers.ts +20 -0
- package/lib/http/server.ts +22 -4
- package/lib/pdf.ts +2 -2
- package/lib/pool.ts +62 -0
- package/lib/requirements.ts +2 -2
- package/lib/screen-session.ts +85 -0
- package/lib/timer.ts +61 -0
- package/lib/tui/README.md +3 -3
- package/lib/tui/columns.ts +33 -8
- package/lib/tui/dock-screen.ts +12 -4
- package/lib/tui/rows.ts +15 -6
- package/lib/tui/rules.ts +4 -8
- package/lib/tui/tool.ts +55 -2
- package/package.json +8 -4
- package/skills/changelog/SKILL.md +136 -0
- package/extensions/document/index.ts +0 -17
- /package/extensions/{document → file-read}/deadline.ts +0 -0
- /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browse pages' stylesheet: the shared Bytefly shell and components, then
|
|
3
|
+
* the page-only `browse-*` rules layered on top.
|
|
4
|
+
*
|
|
5
|
+
* Kept apart from `html.ts` so the renderers stay a renderer and the CSS stays
|
|
6
|
+
* findable as one block; the `bf-*` kit itself lives in `lib/web`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
SIDEBAR_COLLAPSE_MAX,
|
|
11
|
+
WEB_BASE_CSS,
|
|
12
|
+
WEB_COMPONENTS_CSS,
|
|
13
|
+
WEB_LAYOUT_CSS,
|
|
14
|
+
WEB_THEME_CSS,
|
|
15
|
+
} from "../../lib/web/index.ts";
|
|
16
|
+
|
|
17
|
+
/** The shared shell, components, and the page-only `browse-*` rules. */
|
|
18
|
+
export const BROWSE_CSS = `${WEB_BASE_CSS}
|
|
19
|
+
${WEB_THEME_CSS}
|
|
20
|
+
${WEB_LAYOUT_CSS}
|
|
21
|
+
${WEB_COMPONENTS_CSS}
|
|
22
|
+
${WEB_THEME_CSS}
|
|
23
|
+
${WEB_LAYOUT_CSS}
|
|
24
|
+
${WEB_COMPONENTS_CSS}
|
|
25
|
+
.browse-side{padding:var(--space-3)}
|
|
26
|
+
.browse-root{flex:1 1 auto;direction:rtl;text-align:left}
|
|
27
|
+
.browse-git{font-family:var(--font-mono)}
|
|
28
|
+
.browse-quick-open{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);width:100%;margin-bottom:var(--space-2);padding:6px var(--space-2);border:1px solid var(--border);border-radius:var(--radius-md);background:var(--canvas);color:var(--muted);font:var(--fs-sm) var(--font-ui);cursor:pointer}
|
|
29
|
+
.browse-quick-open:hover{border-color:var(--accent);color:var(--accent)}
|
|
30
|
+
.browse-quick-open kbd{font-family:var(--font-mono);font-size:var(--fs-xs);color:var(--dim)}
|
|
31
|
+
.browse-side__filters{display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)}
|
|
32
|
+
.browse-side__filters .bf-field{flex:1 1 auto;min-width:0}
|
|
33
|
+
.browse-side__filters .bf-btn{flex:0 0 auto;white-space:nowrap}
|
|
34
|
+
.browse-cat{width:16px;height:16px;flex:0 0 16px;color:var(--accent)}
|
|
35
|
+
.browse-cat.doc{color:var(--muted)}.browse-cat.img{color:var(--success)}.browse-cat.cfg{color:var(--warn)}.browse-cat.arc{color:var(--warn)}
|
|
36
|
+
.browse-heading{margin:0 0 var(--space-3);font-size:var(--fs-lg)}
|
|
37
|
+
.browse-listing th.bf-table__size,.browse-listing th.bf-table__when{text-align:right}
|
|
38
|
+
.browse-listing th.bf-table__size{width:96px}
|
|
39
|
+
.browse-listing th.bf-table__when{width:140px}
|
|
40
|
+
.browse-dotfile .bf-table__name a{opacity:.72}
|
|
41
|
+
.browse-broken{color:var(--muted);text-decoration:line-through}
|
|
42
|
+
.browse-link-mark{color:var(--muted);font-size:var(--fs-xs)}
|
|
43
|
+
.browse-when{display:none}
|
|
44
|
+
body.palette-open{overflow:hidden}
|
|
45
|
+
.browse-diff{font-family:var(--font-mono);gap:5px}
|
|
46
|
+
.browse-diff:hover{color:var(--accent);text-decoration:none}
|
|
47
|
+
.browse-diff__dot{color:var(--warn);font-size:9px;line-height:1}
|
|
48
|
+
.browse-change{color:var(--warn);font-size:9px;line-height:1;flex:0 0 auto}
|
|
49
|
+
/* Live status: the connection chip keeps its own border and padding so a tone
|
|
50
|
+
swap only changes the colour, and the footer line reads as one muted clause. */
|
|
51
|
+
.browse-live{font-family:var(--font-mono);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
52
|
+
.browse-live__glyph{font-size:9px;line-height:1}
|
|
53
|
+
.browse-live-line{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
54
|
+
.browse-live-line::after{content:" · "}
|
|
55
|
+
.browse-diff-totals{font-family:var(--font-mono)}
|
|
56
|
+
.browse-diff-add{color:var(--success)}
|
|
57
|
+
.browse-diff-del{color:var(--danger)}
|
|
58
|
+
.browse-diff-scope{color:var(--muted);font-size:var(--fs-sm);font-weight:var(--fw-medium)}
|
|
59
|
+
.browse-diff-toolbar{display:flex;align-items:center;gap:var(--space-3);margin:0 0 var(--space-3);flex-wrap:wrap}
|
|
60
|
+
.browse-diff-stat{margin:0 0 var(--space-3);font:var(--fs-xs)/1.5 var(--font-mono);color:var(--muted);white-space:pre-wrap;overflow-x:auto}
|
|
61
|
+
.browse-diff th.browse-diff-status{width:72px}
|
|
62
|
+
.browse-diff th.browse-diff-word{width:130px}
|
|
63
|
+
.browse-diff-badge{display:inline-block;min-width:16px;text-align:center;font:var(--fw-semibold) var(--fs-xs) var(--font-mono);border-radius:var(--radius-sm);padding:1px 5px;border:1px solid var(--border)}
|
|
64
|
+
.browse-diff-badge--M{color:var(--warn);border-color:currentColor}
|
|
65
|
+
.browse-diff-badge--A,.browse-diff-badge--Q{color:var(--success);border-color:currentColor}
|
|
66
|
+
.browse-diff-badge--D{color:var(--danger);border-color:currentColor}
|
|
67
|
+
.browse-diff-badge--R,.browse-diff-badge--C{color:var(--accent);border-color:currentColor}
|
|
68
|
+
.browse-diff-orig{color:var(--muted);font-size:var(--fs-xs)}
|
|
69
|
+
.browse-diff-word{color:var(--muted);font-size:var(--fs-sm)}
|
|
70
|
+
.browse-diff-no{display:inline-block;width:46px;padding-right:var(--space-2);text-align:right;color:var(--dim);user-select:none}
|
|
71
|
+
.browse-diff-sign{display:inline-block;width:16px;text-align:center;color:var(--muted);user-select:none}
|
|
72
|
+
.bf-diff-line--add{background:var(--success-soft)}
|
|
73
|
+
.bf-diff-line--add .browse-diff-sign{color:var(--success)}
|
|
74
|
+
.bf-diff-line--del{background:var(--danger-soft)}
|
|
75
|
+
.bf-diff-line--del .browse-diff-sign{color:var(--danger)}
|
|
76
|
+
.bf-diff-line--hunk{color:var(--accent);background:var(--canvas)}
|
|
77
|
+
.bf-diff-line--meta{color:var(--muted)}
|
|
78
|
+
.bf-diff-line--note{color:var(--muted);font-style:italic}
|
|
79
|
+
/* A hunk header pins to the top of the diff viewport while its body scrolls.
|
|
80
|
+
Each hunk is a section so the pinned header is bounded by its own body and
|
|
81
|
+
cannot stack on the next one. */
|
|
82
|
+
.bf-diff-hunk{display:block}
|
|
83
|
+
.bf-diff .bf-diff-line--hunk{position:sticky;top:0;z-index:1}
|
|
84
|
+
/* Reading progress, pinned to the sticky header's bottom edge. */
|
|
85
|
+
.browse-progress{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden}
|
|
86
|
+
.browse-progress__fill{display:block;height:100%;width:0;background:var(--accent);transition:width var(--motion-fast) linear}
|
|
87
|
+
/* File and diff pages: a measured reading column inside the card. */
|
|
88
|
+
/* The Markdown body fills the card, so the scroll box is the card's width: its
|
|
89
|
+
scrollbar sits at the card edge. The measure lives on the blocks themselves:
|
|
90
|
+
on .bf-md it would be a flex item with auto inline margins, which is
|
|
91
|
+
shrink-to-fit, so a short document collapsed to its widest line. */
|
|
92
|
+
.browse-measure{--browse-measure:1012px}
|
|
93
|
+
.browse-measure .bf-md{--browse-gutter:max(0px,calc((100% - var(--browse-measure)) / 2));max-width:none;margin-inline:0;padding-inline:var(--space-5);padding-block:var(--space-4) var(--space-6)}
|
|
94
|
+
.browse-measure .bf-md>*{max-width:var(--browse-measure);margin-inline:auto}
|
|
95
|
+
/* Tables start at the prose column and take only the gutter to its right, so a
|
|
96
|
+
wide window widens them without a dead band on their left. Code stays inside
|
|
97
|
+
the measure with the rest of the prose. */
|
|
98
|
+
.browse-measure .bf-md>.md-table-frame{max-width:none;padding-left:var(--browse-gutter)}
|
|
99
|
+
/* A table wider than the pane scrolls, but an overlay scrollbar is invisible
|
|
100
|
+
until it moves; a gradient on the frame marks the columns still out of view.
|
|
101
|
+
It has to be the frame: the scroller clips its own content, so a pseudo-element
|
|
102
|
+
inside it would scroll away with the table. */
|
|
103
|
+
.browse-measure .bf-md>.md-table-frame.browse-table-more::after{content:"";position:absolute;top:0;right:0;bottom:0;width:40px;pointer-events:none;background:linear-gradient(to left,var(--surface) 12%,transparent)}
|
|
104
|
+
.bf-card__meta{font-variant-numeric:tabular-nums}
|
|
105
|
+
.browse-diff-summary{font-family:var(--font-mono);white-space:nowrap}
|
|
106
|
+
.browse-diff-summary .browse-diff-add{color:var(--success)}
|
|
107
|
+
.browse-diff-summary .browse-diff-del{color:var(--danger)}
|
|
108
|
+
.browse-hunks{color:var(--muted)}
|
|
109
|
+
/* Wide screens: pin quick open and the filter, and scroll the tree on its own
|
|
110
|
+
box. The sidebar is a details element, whose content sits in an anonymous
|
|
111
|
+
wrapper with no definite height, so the panel fills the sidebar by absolute
|
|
112
|
+
positioning; below the sidebar breakpoint the disclosure keeps normal page
|
|
113
|
+
scroll. */
|
|
114
|
+
@media not (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
115
|
+
.browse-sidebar{position:relative;overflow:hidden}
|
|
116
|
+
.browse-side{position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden}
|
|
117
|
+
.browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
|
|
118
|
+
}
|
|
119
|
+
@media (max-width:820px){
|
|
120
|
+
.browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
|
|
121
|
+
}
|
|
122
|
+
@media (max-width:520px){
|
|
123
|
+
.bf-toolbar .bf-theme-toggle{margin-left:auto}
|
|
124
|
+
}`;
|
|
@@ -152,5 +152,4 @@ of the model's active tool set unless a script explicitly reaches for it.
|
|
|
152
152
|
| `index.ts` | The extension factory: registers `open_path` and the `/open` command, resolves the target, and routes a launch failure to `ctx.ui.notify`. |
|
|
153
153
|
| `open.ts` | `normalizeTarget` and `buildCommand` (pure), `resolveTarget` (existence check), and `openTarget` with the injectable launcher, which reports a start failure and a meaningful non-zero exit. |
|
|
154
154
|
| `schema.ts` | `TOOL_NAME`, the TypeBox `parameters` / `outputSchema`, and the `OpenPathArgs` / `OpenPathResult` types. |
|
|
155
|
-
| `
|
|
156
|
-
| `extension.test.ts` | Extension-surface tests: registration, disabling, the tool call, and the command. |
|
|
155
|
+
| `extension.test.ts` | The capability suite: normalization, resolution, per-platform commands, the launcher seam, registration, disabling, the tool call, and the command. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# file-read — read local PDF, DOCX, DOC, ODT, RTF, and XLSX files as text
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`file-read` adds a `read_doc` tool that extracts plain text from a local `.pdf`,
|
|
4
4
|
`.docx`, `.doc`, `.odt`, `.rtf`, or `.xlsx` file, paged with `startIndex` /
|
|
5
5
|
`maxChars`. Extraction runs in process through JavaScript libraries (`unpdf`,
|
|
6
6
|
`mammoth`, `word-extractor`, `fflate` + `fast-xml-parser`, `rtf-parser`,
|
|
@@ -8,34 +8,30 @@
|
|
|
8
8
|
the effective working directory with the same symlink-aware guard the built-in
|
|
9
9
|
path tools use.
|
|
10
10
|
|
|
11
|
-
## Tool rendering
|
|
12
|
-
|
|
13
|
-
How the `read_doc` call and its result look in the transcript — the call line as a headline and the body indented under it:
|
|
14
|
-
|
|
15
|
-
<img src="../../assets/tools/read_doc.png" alt="The read_doc tool's result: the call line and an indented body with the file path, format, size, and the extracted text preview" width="100%">
|
|
16
|
-
|
|
17
11
|
## Quickstart
|
|
18
12
|
|
|
19
13
|
```bash
|
|
20
|
-
pi --extension ./extensions/
|
|
14
|
+
pi --extension ./extensions/file-read # this extension only
|
|
21
15
|
pi -e . # the package from this checkout
|
|
22
16
|
pi install ./ # install the package for a user
|
|
23
17
|
```
|
|
24
18
|
|
|
25
19
|
The extraction libraries are ordinary dependencies, so a normal install brings
|
|
26
|
-
them along. Then ask the model to read a document, or call the
|
|
20
|
+
them along. Then ask the model to read a document or a workbook, or call the
|
|
21
|
+
tool directly:
|
|
27
22
|
|
|
28
23
|
```jsonc
|
|
29
24
|
{ "path": "docs/spec.pdf" }
|
|
30
25
|
{ "path": "docs/spec.pdf", "startIndex": 40000, "maxChars": 40000 }
|
|
26
|
+
{ "path": "report.xlsx" }
|
|
31
27
|
```
|
|
32
28
|
|
|
33
29
|
## What it does
|
|
34
30
|
|
|
35
31
|
- Extracts plain text from `.pdf`, `.docx`, `.doc`, `.odt`, `.rtf`, and `.xlsx`
|
|
36
32
|
files for the model to read.
|
|
37
|
-
- Pages long
|
|
38
|
-
|
|
33
|
+
- Pages long files: when output is truncated, call again with the `startIndex`
|
|
34
|
+
named in the note.
|
|
39
35
|
- Slices by code point, so CJK and emoji stay well-formed.
|
|
40
36
|
- Reads only inside the effective working directory, resolving symlinks so a
|
|
41
37
|
link cannot escape it.
|
|
@@ -47,6 +43,11 @@ them along. Then ask the model to read a document, or call the tool directly:
|
|
|
47
43
|
`read_doc` — direct exposure, active by default, read-only, closed-world
|
|
48
44
|
(no network).
|
|
49
45
|
|
|
46
|
+
How the call and its result look in the transcript — the call line as a headline
|
|
47
|
+
and the body indented under it:
|
|
48
|
+
|
|
49
|
+
<img src="../../assets/tools/read_doc.png" alt="The read_doc tool's result: the call line and an indented body with the file path, format, size, and the extracted text preview" width="100%">
|
|
50
|
+
|
|
50
51
|
| Parameter | Type | Meaning |
|
|
51
52
|
|---|---|---|
|
|
52
53
|
| `path` | string, required, 1–4096 chars | Path to a `.pdf`, `.docx`, `.doc`, `.odt`, `.rtf`, or `.xlsx`, relative to the working directory (or absolute inside it). |
|
|
@@ -67,7 +68,9 @@ Result (`details` / `structuredContent`):
|
|
|
67
68
|
| `text` | The clean slice, without the header or truncation note. |
|
|
68
69
|
|
|
69
70
|
The model-facing `content` is a header (`Path:`, `Format:`, `Size:`), a blank
|
|
70
|
-
line, the slice, and a truncation note when applicable.
|
|
71
|
+
line, the slice, and a truncation note when applicable. A workbook reads as one
|
|
72
|
+
`[Sheet]` heading per worksheet followed by tab-separated rows, so the shape of
|
|
73
|
+
the text says what the file was.
|
|
71
74
|
|
|
72
75
|
Errors are thrown for: a missing or non-file path, a path outside the root, an
|
|
73
76
|
unsupported extension (with a targeted hint for legacy `.xls`), a disabled
|
|
@@ -116,7 +119,7 @@ sanitized error path.
|
|
|
116
119
|
|
|
117
120
|
## Configuration
|
|
118
121
|
|
|
119
|
-
The `
|
|
122
|
+
The `file-read` section of the shared `bytefly.json`, loaded from
|
|
120
123
|
`~/.pi/agent/bytefly.json` then `<cwd>/.pi/bytefly.json` (project wins), each
|
|
121
124
|
merged over the defaults and clamped:
|
|
122
125
|
|
|
@@ -128,12 +131,13 @@ merged over the defaults and clamped:
|
|
|
128
131
|
| `formats` | `{ "pdf": true, "docx": true, "doc": true, "odt": true, "rtf": true, "xlsx": true }` | — | Per-format on/off, merged over each registry entry's default; unknown keys are ignored. |
|
|
129
132
|
|
|
130
133
|
```jsonc
|
|
131
|
-
// bytefly.json → "
|
|
134
|
+
// bytefly.json → "file-read"
|
|
132
135
|
{ "maxChars": 80000, "extractTimeoutMs": 60000, "formats": { "docx": false } }
|
|
133
136
|
```
|
|
134
137
|
|
|
135
|
-
`
|
|
136
|
-
|
|
138
|
+
The section was named `document` before this extension was renamed; the loader
|
|
139
|
+
reads only `file-read`, and `bun run config:migrate` renames an existing
|
|
140
|
+
`document` section and moves a legacy `document.json` into it.
|
|
137
141
|
|
|
138
142
|
## Security
|
|
139
143
|
|
|
@@ -145,14 +149,14 @@ run `bun run config:migrate` to move a `document.json` into the shared file.
|
|
|
145
149
|
parser receives.
|
|
146
150
|
- **Size-capped and time-bounded.** Files larger than `maxFileBytes` are rejected
|
|
147
151
|
before any parser runs, and a parser that outlasts `extractTimeoutMs` is
|
|
148
|
-
abandoned so the turn is not held open by one
|
|
152
|
+
abandoned so the turn is not held open by one file.
|
|
149
153
|
- **No subprocesses.** Extraction never spawns a process, so it grants no
|
|
150
154
|
privileges beyond the current process.
|
|
151
|
-
- **Untrusted text.** Extracted
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
- **Untrusted text.** Extracted text is treated as untrusted: it is returned as
|
|
156
|
+
model-facing `content` and data. The collapsed transcript draws only a
|
|
157
|
+
sanitized summary; the expanded transcript draws a sanitized, line-capped
|
|
158
|
+
preview (`PREVIEW_LINES`). Control characters are stripped before a theme
|
|
159
|
+
colour is applied, so raw file bytes never reach the terminal.
|
|
156
160
|
- **Worktree guard.** Under the `worktree` extension, `read_doc` is in the
|
|
157
161
|
worktree guard's path-tool map, so `blockReadEscapes` / `blockSymlinkEscapes`
|
|
158
162
|
apply to it exactly as to the built-in `read`.
|
|
@@ -171,11 +175,11 @@ Each extractor builds a plain-text document from the format's bytes:
|
|
|
171
175
|
| XLSX | `exceljs` | One `[Sheet]` heading per non-empty worksheet, then tab-separated rows. Dates and numbers use the workbook's displayed text; formulas show their computed value. Cell formatting is dropped. |
|
|
172
176
|
|
|
173
177
|
The tool is pageable across the whole extracted text, so a word-processing
|
|
174
|
-
document or workbook is read in the same `startIndex` / `maxChars` way as a PDF.
|
|
178
|
+
document or a workbook is read in the same `startIndex` / `maxChars` way as a PDF.
|
|
175
179
|
|
|
176
180
|
Each call is bounded by `extractTimeoutMs` (30 s by default). A parser that
|
|
177
181
|
outlasts the deadline is abandoned — the read fails with the limit named, and a
|
|
178
|
-
late result is discarded — so a pathological
|
|
182
|
+
late result is discarded — so a pathological file cannot park the turn.
|
|
179
183
|
|
|
180
184
|
## Supported formats
|
|
181
185
|
|
|
@@ -194,13 +198,13 @@ The format table in `formats.ts` is the single source of truth; the handler,
|
|
|
194
198
|
config defaults, and error listing all derive from it.
|
|
195
199
|
|
|
196
200
|
1. Add the extraction library to `dependencies` in `package.json`.
|
|
197
|
-
2. Add `extensions/
|
|
198
|
-
`await import(...)`s its library, reads the bytes with `
|
|
201
|
+
2. Add `extensions/file-read/extract/<fmt>.ts` exporting a `FormatExtractor` that
|
|
202
|
+
`await import(...)`s its library, reads the bytes with `readFormatBytes`, and
|
|
199
203
|
normalizes the output. Keep the parsing in a pure, exported helper so it is
|
|
200
204
|
testable without a real file.
|
|
201
|
-
3. Add a `
|
|
202
|
-
4. Add `extract
|
|
203
|
-
`
|
|
205
|
+
3. Add a `ReadFormat` entry to `FORMATS` and widen the `id` union.
|
|
206
|
+
4. Add the new extractor to `extract/extract.test.ts` with a real minimal
|
|
207
|
+
fixture, and extend `tool.test.ts` for format selection and config.
|
|
204
208
|
5. Add a row to the tables above.
|
|
205
209
|
|
|
206
210
|
`tool.ts`, `paging.ts`, and the config normalizer need no changes.
|
|
@@ -235,51 +239,63 @@ config defaults, and error listing all derive from it.
|
|
|
235
239
|
|
|
236
240
|
| Aspect | Contract |
|
|
237
241
|
|---|---|
|
|
238
|
-
| Registration | `
|
|
242
|
+
| Registration | `fileRead(pi)` in `index.ts` calls `registerReadTool(pi)`, which registers `read_doc`. The factory only registers; no parser loads until a read needs it. |
|
|
239
243
|
| Tool | `read_doc`, direct exposure and active on registration (the defaults), default (`parallel`) execution, `annotations: { readOnlyHint: true, openWorldHint: false }`, TypeBox `parameters` and `outputSchema`, and `renderCall`/`renderResult` (reuse `context.lastComponent`; read `context.cwd` and `context.isError`). |
|
|
240
244
|
| State | None persisted. Per-result state lives in the tool-result `details` / `structuredContent`; the renderer reads only that. |
|
|
241
245
|
| Lifecycle | No `session_start` / `session_shutdown` hooks: each read imports a parser, runs it, and holds no long-lived resources. |
|
|
242
246
|
| Host seams | `resolveEffectiveCwd` (`lib/env.ts`), `isInsideReal` / `realPathOfNearest` (`lib/path.ts`), `loadConfigSection` (`lib/config.ts`), `stripControlChars` / `sanitize` / `formatTokens` (`lib/format.ts`), `SEPARATORS` (`lib/tui/glyphs.ts`). |
|
|
243
|
-
| Config | The `
|
|
247
|
+
| Config | The `file-read` section of the shared `bytefly.json` via `loadConfigSection` (`~/.pi/agent`, `<cwd>/.pi`). |
|
|
244
248
|
|
|
245
249
|
## Design notes
|
|
246
250
|
|
|
247
|
-
A separate tool rather than an overload of the built-in `read
|
|
251
|
+
**A separate tool rather than an overload of the built-in `read`.** The worktree
|
|
248
252
|
extension owns the built-in path tools and re-registers them, so overloading
|
|
249
|
-
`read` would fight that machinery.
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
`read` would fight that machinery. The tool also stays one tool for all six
|
|
254
|
+
formats, the way Pi's `read` covers text and images: the split would be a choice
|
|
255
|
+
the model can get wrong (and an extra entry in the worktree guard's path map) in
|
|
256
|
+
exchange for prompt copy, while the workbook-specific guidance fits in one
|
|
257
|
+
`promptGuidelines` bullet. A second tool becomes worth it only when it carries
|
|
258
|
+
behaviour a document read must not have, such as reading named worksheets.
|
|
259
|
+
|
|
260
|
+
**The name.** `document` stopped describing the job once XLSX joined the table:
|
|
261
|
+
the extension reads any local file whose bytes the built-in `read` cannot decode,
|
|
262
|
+
and `file-read` sits beside `file-browse` and `file-open`.
|
|
263
|
+
|
|
264
|
+
**A table-driven registry** keeps future formats additive, and extracting in
|
|
265
|
+
process means the tool works on any host without a per-format install step and
|
|
266
|
+
without spawning a child process per read.
|
|
252
267
|
|
|
253
268
|
**Parsers load lazily.** The extension registers on every session, so a static
|
|
254
269
|
top-level import of `exceljs`, `mammoth`, and `unpdf` would tax every startup,
|
|
255
|
-
whether or not a
|
|
270
|
+
whether or not a file is ever read. Each extractor therefore imports its
|
|
256
271
|
library inside the call (`await import(...)`), matching `web-access`'s lazy
|
|
257
272
|
`unpdf` import.
|
|
258
273
|
|
|
259
|
-
**The XLSX
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
274
|
+
**The XLSX text is rebuilt, not passed through.** `exceljs` exposes worksheets as
|
|
275
|
+
objects; the extractor walks every worksheet and reassembles `[Sheet]` + tab rows
|
|
276
|
+
so the model-facing format is stable across extractor changes and every worksheet
|
|
277
|
+
is included, not just the first.
|
|
263
278
|
|
|
264
|
-
The renderer
|
|
265
|
-
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
The renderer reuses `context.lastComponent` (`Text.setText`), binding the expand
|
|
280
|
+
hint to `app.tools.expand`. Path shortening lives in `render.ts` (not `lib/`):
|
|
281
|
+
`lib/` helpers are for two or more extensions, and the built-in-style
|
|
282
|
+
`~`/boundary rule here is this extension's only. The result line deliberately
|
|
283
|
+
omits the path: Pi stacks the call and result lines in one row, so repeating it
|
|
284
|
+
would be pure duplication.
|
|
270
285
|
|
|
271
286
|
## Files
|
|
272
287
|
|
|
273
288
|
| File | Responsibility |
|
|
274
289
|
|---|---|
|
|
275
290
|
| `index.ts` | Factory; registers the tool. |
|
|
276
|
-
| `formats.ts` | `
|
|
277
|
-
| `tool.ts` | The tool definition, handler, path guard, extractor override seam, and renderer wiring.
|
|
291
|
+
| `formats.ts` | `ReadFormat`, the `FORMATS` registry, `FormatId`, `detectFormat`, `supportedExtensions`, `unsupportedHint`. |
|
|
292
|
+
| `tool.ts` | The tool definition, handler, path guard, extractor override seam, and renderer wiring. |
|
|
278
293
|
| `render.ts` | Theme-aware transcript formatting: call/result lines, `~` + OSC-8 path, expand hint, sanitized preview, and the shared rejection/error path. The call reuses `ToolCallText` (one line, middle clip); the result reuses `Text`. |
|
|
279
|
-
| `schema.ts` | `TOOL_NAME`, `
|
|
280
|
-
| `config.ts` | `
|
|
281
|
-
| `paging.ts` | `
|
|
282
|
-
| `
|
|
294
|
+
| `schema.ts` | `TOOL_NAME`, `ReadParams`, `ReadOutput`, limits, result type. |
|
|
295
|
+
| `config.ts` | `ReadConfig`, registry-derived defaults, `normalizeConfig`, `loadConfig`, `isFormatEnabled`. |
|
|
296
|
+
| `paging.ts` | `formatPage`: code-point slicing and header/truncation formatting; `summarizeRead`: the theme-free transcript summary. |
|
|
297
|
+
| `deadline.ts` | `withDeadline`: abandons a parser that outlasts `extractTimeoutMs` and discards its late result. |
|
|
298
|
+
| `extract/file.ts` | `ExtractInput` / `FormatExtractor`, `readFormatBytes` (abort-aware read), `normalizeBodyText`. |
|
|
283
299
|
| `extract/vendor.d.ts` | Ambient types for `word-extractor` and `rtf-parser` (no bundled declarations). |
|
|
284
300
|
| `extract/pdf.ts` | Adapter over the shared `lib/pdf.ts` seam, plus the PDF body normalization. |
|
|
285
301
|
| `extract/docx.ts` | `mammoth` extractor. |
|
|
@@ -291,12 +307,12 @@ repeating it would be pure duplication.
|
|
|
291
307
|
## Testing
|
|
292
308
|
|
|
293
309
|
```bash
|
|
294
|
-
bun test extensions/
|
|
310
|
+
bun test extensions/file-read
|
|
295
311
|
```
|
|
296
312
|
|
|
297
313
|
Extractor tests run against real minimal files (`test/helpers/fixtures/documents.ts`
|
|
298
314
|
builds a genuine PDF, DOCX, and ODT; XLSX is written with `exceljs`; legacy `.doc`
|
|
299
315
|
reads the committed `files/legacy.doc`), so the libraries are exercised, not
|
|
300
316
|
stubbed. `tool.test.ts` injects fake extractors for behavioural cases and reads a
|
|
301
|
-
real PDF and a real `.doc` end to end; `test/helpers/fixtures/
|
|
302
|
-
provides the scratch root and an escape symlink.
|
|
317
|
+
real PDF and a real `.doc` end to end; `test/helpers/fixtures/file-read.ts`
|
|
318
|
+
provides the scratch root and an escape symlink.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Configuration for the `
|
|
2
|
+
* Configuration for the `file-read` extension.
|
|
3
3
|
*
|
|
4
4
|
* `formats` defaults are derived from the format registry, so a new format needs
|
|
5
|
-
* no config edit. The `
|
|
5
|
+
* no config edit. The `file-read` section of the shared `bytefly.json` is read
|
|
6
6
|
* from the global document and the project document (project wins), and every
|
|
7
7
|
* field is clamped.
|
|
8
8
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { clampInteger, loadConfigSection } from "../../lib/config.ts";
|
|
11
11
|
import { FORMATS, type FormatId } from "./formats.ts";
|
|
12
12
|
|
|
13
|
-
export interface
|
|
13
|
+
export interface ReadConfig {
|
|
14
14
|
/** Reject files larger than this many bytes. */
|
|
15
15
|
maxFileBytes: number;
|
|
16
16
|
/** Default character budget for the returned slice. */
|
|
@@ -21,7 +21,7 @@ export interface DocConfig {
|
|
|
21
21
|
formats: Record<FormatId, boolean>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const DEFAULT_CONFIG:
|
|
24
|
+
export const DEFAULT_CONFIG: ReadConfig = {
|
|
25
25
|
maxFileBytes: 20 * 1024 * 1024,
|
|
26
26
|
maxChars: 40_000,
|
|
27
27
|
extractTimeoutMs: 30_000,
|
|
@@ -41,7 +41,10 @@ function normalizeFormats(raw: unknown, base: Record<FormatId, boolean>): Record
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Validate and clamp a raw config object over the running base. */
|
|
44
|
-
export function normalizeConfig(
|
|
44
|
+
export function normalizeConfig(
|
|
45
|
+
raw: Record<string, unknown> | undefined,
|
|
46
|
+
base: ReadConfig = DEFAULT_CONFIG,
|
|
47
|
+
): ReadConfig {
|
|
45
48
|
if (!raw) return base;
|
|
46
49
|
return {
|
|
47
50
|
maxFileBytes: clampInteger(raw.maxFileBytes, base.maxFileBytes, 1_024, 512 * 1024 * 1024),
|
|
@@ -51,12 +54,12 @@ export function normalizeConfig(raw: Record<string, unknown> | undefined, base:
|
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
/** Effective config from the shared `
|
|
55
|
-
export function loadConfig(cwd: string):
|
|
56
|
-
return loadConfigSection(cwd, "
|
|
57
|
+
/** Effective config from the shared `file-read` section (defaults, global, project, env). */
|
|
58
|
+
export function loadConfig(cwd: string): ReadConfig {
|
|
59
|
+
return loadConfigSection(cwd, "file-read", DEFAULT_CONFIG, normalizeConfig);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
/** Whether a format is enabled in the effective config. */
|
|
60
|
-
export function isFormatEnabled(config:
|
|
63
|
+
export function isFormatEnabled(config: ReadConfig, id: FormatId): boolean {
|
|
61
64
|
return config.formats[id] !== false;
|
|
62
65
|
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* is only wired to the `.doc` extension.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { normalizeBodyText, type
|
|
9
|
+
import { normalizeBodyText, type FormatExtractor } from "./file.ts";
|
|
10
10
|
|
|
11
11
|
/** Extract the body text from a legacy OLE `.doc` file. */
|
|
12
|
-
export const extractDocText:
|
|
12
|
+
export const extractDocText: FormatExtractor = async (file) => {
|
|
13
13
|
file.signal?.throwIfAborted();
|
|
14
14
|
const module = await import("word-extractor");
|
|
15
15
|
const extractor = new module.default();
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* part of the body and so are not included.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { normalizeBodyText,
|
|
9
|
+
import { normalizeBodyText, readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
10
10
|
|
|
11
11
|
/** Extract the body text from a DOCX file. */
|
|
12
|
-
export const extractDocxText:
|
|
13
|
-
const bytes = await
|
|
12
|
+
export const extractDocxText: FormatExtractor = async (file) => {
|
|
13
|
+
const bytes = await readFormatBytes(file);
|
|
14
14
|
const module = await import("mammoth");
|
|
15
15
|
const mammoth = module.default;
|
|
16
16
|
const { value } = await mammoth.extractRawText({ buffer: Buffer.from(bytes) });
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The in-process extractor contract for `read_doc`.
|
|
3
3
|
*
|
|
4
|
-
* Each
|
|
5
|
-
*
|
|
6
|
-
* {@link
|
|
4
|
+
* Each format is extracted by a JavaScript library that reads the file itself.
|
|
5
|
+
* This module owns the file read, the abort check, and the
|
|
6
|
+
* {@link FormatExtractor} type, so the format modules stay tiny and testable
|
|
7
7
|
* through real fixtures.
|
|
8
8
|
*
|
|
9
9
|
* Imports of the extraction libraries are lazy (`await import(...)` inside each
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { readFile } from "node:fs/promises";
|
|
15
15
|
|
|
16
16
|
/** What an extractor receives: the real, root-confined absolute path. */
|
|
17
|
-
export interface
|
|
17
|
+
export interface ExtractInput {
|
|
18
18
|
/** Absolute, symlink-resolved path inside the effective working root. */
|
|
19
19
|
path: string;
|
|
20
20
|
/** The active turn's abort signal, checked before any work. */
|
|
@@ -22,13 +22,13 @@ export interface DocFile {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Extract plain text for one file. */
|
|
25
|
-
export type
|
|
25
|
+
export type FormatExtractor = (input: ExtractInput) => Promise<string>;
|
|
26
26
|
|
|
27
27
|
/** Read the whole file, refusing to start once the turn is aborted. */
|
|
28
|
-
export async function
|
|
29
|
-
|
|
30
|
-
const bytes = await readFile(
|
|
31
|
-
|
|
28
|
+
export async function readFormatBytes(input: ExtractInput): Promise<Uint8Array> {
|
|
29
|
+
input.signal?.throwIfAborted();
|
|
30
|
+
const bytes = await readFile(input.path);
|
|
31
|
+
input.signal?.throwIfAborted();
|
|
32
32
|
return new Uint8Array(bytes);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { strFromU8, unzipSync } from "fflate";
|
|
12
12
|
import { XMLParser } from "fast-xml-parser";
|
|
13
|
-
import { normalizeBodyText,
|
|
13
|
+
import { normalizeBodyText, readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
14
14
|
|
|
15
15
|
/** Elements that end a line. */
|
|
16
16
|
const BLOCK_ELEMENTS = new Set(["text:p", "text:h", "text:list-item"]);
|
|
@@ -93,8 +93,8 @@ export function odtXmlToText(xml: string): string {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/** Extract the body text from an ODT file. */
|
|
96
|
-
export const extractOdtText:
|
|
97
|
-
const archive = unzipSync(await
|
|
96
|
+
export const extractOdtText: FormatExtractor = async (file) => {
|
|
97
|
+
const archive = unzipSync(await readFormatBytes(file));
|
|
98
98
|
const content = archive["content.xml"];
|
|
99
99
|
if (!content) throw new Error("ODT archive has no content.xml.");
|
|
100
100
|
return odtXmlToText(strFromU8(content));
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { extractPdfText } from "../../../lib/pdf.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
11
11
|
|
|
12
12
|
/** Extract text from every page of a PDF. */
|
|
13
|
-
export const extractPdfDocument:
|
|
14
|
-
const bytes = await
|
|
13
|
+
export const extractPdfDocument: FormatExtractor = async (file) => {
|
|
14
|
+
const bytes = await readFormatBytes(file);
|
|
15
15
|
const text = await extractPdfText(bytes);
|
|
16
16
|
return text.replace(/\r\n?/g, "\n").replace(/\n+$/, "");
|
|
17
17
|
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* within a paragraph, paragraphs separated by newlines. Formatting is dropped.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { normalizeBodyText,
|
|
9
|
+
import { normalizeBodyText, readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
10
10
|
|
|
11
11
|
interface RtfNode {
|
|
12
12
|
value?: string;
|
|
@@ -39,7 +39,7 @@ async function parseRtf(input: string): Promise<RtfDocument> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Extract the body text from an RTF file. */
|
|
42
|
-
export const extractRtfText:
|
|
43
|
-
const bytes = await
|
|
42
|
+
export const extractRtfText: FormatExtractor = async (file) => {
|
|
43
|
+
const bytes = await readFormatBytes(file);
|
|
44
44
|
return rtfDocumentToText(await parseRtf(new TextDecoder().decode(bytes)));
|
|
45
45
|
};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { stripControlChars } from "../../../lib/format.ts";
|
|
12
|
-
import {
|
|
12
|
+
import { readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
13
13
|
|
|
14
14
|
/** One sheet as the text builder consumes it. */
|
|
15
15
|
interface Sheet {
|
|
@@ -60,11 +60,11 @@ export function sheetsToText(value: unknown): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** Extract text from a workbook, one `[Sheet]` section per non-empty worksheet. */
|
|
63
|
-
export const extractXlsxText:
|
|
64
|
-
const bytes = await
|
|
63
|
+
export const extractXlsxText: FormatExtractor = async (file) => {
|
|
64
|
+
const bytes = await readFormatBytes(file);
|
|
65
65
|
const { Workbook } = await import("exceljs");
|
|
66
66
|
const workbook = new Workbook();
|
|
67
|
-
// `
|
|
67
|
+
// `readFormatBytes` copies into a fresh `Uint8Array`, so its buffer is exactly
|
|
68
68
|
// the file bytes; narrow it for exceljs, which types `load` as ArrayBuffer.
|
|
69
69
|
const { buffer } = bytes;
|
|
70
70
|
if (!(buffer instanceof ArrayBuffer)) {
|