@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.
Files changed (106) hide show
  1. package/README.md +12 -11
  2. package/README.zh-CN.md +9 -7
  3. package/extensions/bytefly/README.md +98 -116
  4. package/extensions/bytefly/notice/component.ts +6 -4
  5. package/extensions/bytefly/notice/config.ts +16 -2
  6. package/extensions/bytefly/panel/format.ts +3 -8
  7. package/extensions/bytefly/panel/join.ts +1 -6
  8. package/extensions/bytefly/panel/settings.ts +74 -377
  9. package/extensions/bytefly/panel/tui/entries.ts +16 -14
  10. package/extensions/bytefly/panel/tui/extensions-view.ts +3 -0
  11. package/extensions/bytefly/panel/tui/frame.ts +2 -2
  12. package/extensions/bytefly/panel/tui/settings-view.ts +8 -12
  13. package/extensions/bytefly/panel/tui.ts +4 -6
  14. package/extensions/bytefly/picker/resources.ts +7 -3
  15. package/extensions/bytefly/working/register.ts +2 -8
  16. package/extensions/check/tools.ts +2 -11
  17. package/extensions/context/README.md +162 -0
  18. package/extensions/context/collect.ts +53 -0
  19. package/extensions/context/commands.ts +100 -0
  20. package/extensions/context/format.ts +82 -0
  21. package/extensions/context/index.ts +16 -0
  22. package/extensions/context/report.ts +319 -0
  23. package/extensions/context/tui.ts +268 -0
  24. package/extensions/context/types.ts +122 -0
  25. package/extensions/file-browse/README.md +83 -8
  26. package/extensions/file-browse/client.ts +3 -1
  27. package/extensions/file-browse/config.ts +23 -0
  28. package/extensions/file-browse/files.ts +183 -48
  29. package/extensions/file-browse/fuzzy.ts +65 -11
  30. package/extensions/file-browse/html.ts +78 -133
  31. package/extensions/file-browse/index.ts +17 -2
  32. package/extensions/file-browse/live-client.ts +158 -0
  33. package/extensions/file-browse/live.ts +256 -0
  34. package/extensions/file-browse/markdown.ts +3 -10
  35. package/extensions/file-browse/raw-html.ts +3 -10
  36. package/extensions/file-browse/router.ts +33 -8
  37. package/extensions/file-browse/server.ts +78 -13
  38. package/extensions/file-browse/styles.ts +124 -0
  39. package/extensions/file-open/README.md +1 -2
  40. package/extensions/{document → file-read}/README.md +72 -56
  41. package/extensions/{document → file-read}/config.ts +12 -9
  42. package/extensions/{document → file-read}/extract/doc.ts +2 -2
  43. package/extensions/{document → file-read}/extract/docx.ts +3 -3
  44. package/extensions/{document → file-read}/extract/file.ts +9 -9
  45. package/extensions/{document → file-read}/extract/odt.ts +3 -3
  46. package/extensions/{document → file-read}/extract/pdf.ts +3 -3
  47. package/extensions/{document → file-read}/extract/rtf.ts +3 -3
  48. package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
  49. package/extensions/{document → file-read}/formats.ts +11 -11
  50. package/extensions/file-read/index.ts +17 -0
  51. package/extensions/{document → file-read}/paging.ts +15 -13
  52. package/extensions/{document → file-read}/render.ts +17 -13
  53. package/extensions/{document → file-read}/schema.ts +7 -7
  54. package/extensions/{document → file-read}/tool.ts +36 -28
  55. package/extensions/goal/tools.ts +10 -14
  56. package/extensions/job/commands.ts +32 -46
  57. package/extensions/job/runtime.ts +6 -8
  58. package/extensions/job/tools.ts +7 -26
  59. package/extensions/memory/commands.ts +30 -44
  60. package/extensions/memory/tools.ts +6 -14
  61. package/extensions/plan/tools/enter.ts +6 -7
  62. package/extensions/plan/tools/exit.ts +6 -7
  63. package/extensions/plan/tools/steps.ts +6 -7
  64. package/extensions/plan/tools/write.ts +8 -9
  65. package/extensions/plan/web-router.ts +2 -12
  66. package/extensions/plan/web.ts +30 -15
  67. package/extensions/question/tools.ts +5 -4
  68. package/extensions/rewind/checklist.ts +10 -3
  69. package/extensions/rewind/git.ts +27 -14
  70. package/extensions/rewind/hunk-picker.ts +1 -1
  71. package/extensions/rewind/path-picker.ts +1 -1
  72. package/extensions/rewind/review-tui.ts +3 -3
  73. package/extensions/rewind/snapshot.ts +13 -4
  74. package/extensions/subagent/background-tui.ts +1 -1
  75. package/extensions/subagent/background.ts +1 -7
  76. package/extensions/todo/tools.ts +10 -14
  77. package/extensions/watch/README.md +5 -5
  78. package/extensions/watch/commands.ts +33 -47
  79. package/extensions/watch/runtime.ts +5 -7
  80. package/extensions/watch/tools.ts +7 -18
  81. package/extensions/web-access/README.md +3 -3
  82. package/extensions/web-access/search/search.ts +9 -18
  83. package/extensions/wechat/timer.ts +9 -30
  84. package/extensions/wechat/tools.ts +4 -7
  85. package/extensions/worktree/status.ts +14 -9
  86. package/lib/README.md +12 -8
  87. package/lib/escape.ts +15 -2
  88. package/lib/git/read.ts +20 -0
  89. package/lib/http/headers.ts +20 -0
  90. package/lib/http/server.ts +22 -4
  91. package/lib/pdf.ts +2 -2
  92. package/lib/pool.ts +62 -0
  93. package/lib/requirements.ts +2 -2
  94. package/lib/screen-session.ts +85 -0
  95. package/lib/timer.ts +61 -0
  96. package/lib/tui/README.md +3 -3
  97. package/lib/tui/columns.ts +33 -8
  98. package/lib/tui/dock-screen.ts +12 -4
  99. package/lib/tui/rows.ts +15 -6
  100. package/lib/tui/rules.ts +4 -8
  101. package/lib/tui/tool.ts +55 -2
  102. package/package.json +8 -4
  103. package/skills/changelog/SKILL.md +136 -0
  104. package/extensions/document/index.ts +0 -17
  105. /package/extensions/{document → file-read}/deadline.ts +0 -0
  106. /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
- | `open.test.ts` | Pure-module tests for normalization, resolution, per-platform commands, and the launcher seam. |
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
- # document — read local PDF, DOCX, DOC, ODT, RTF, and XLSX files as text
1
+ # file-read — read local PDF, DOCX, DOC, ODT, RTF, and XLSX files as text
2
2
 
3
- `document` adds a `read_doc` tool that extracts plain text from a local `.pdf`,
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/document # this extension only
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 tool directly:
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 documents: when output is truncated, call again with the
38
- `startIndex` named in the note.
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 `document` section of the shared `bytefly.json`, loaded from
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 → "document"
134
+ // bytefly.json → "file-read"
132
135
  { "maxChars": 80000, "extractTimeoutMs": 60000, "formats": { "docx": false } }
133
136
  ```
134
137
 
135
- `doc.json` is not read, and neither is the old per-extension `document.json`;
136
- run `bun run config:migrate` to move a `document.json` into the shared file.
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 document.
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 document text is treated as untrusted: it is
152
- returned as model-facing `content` and data. The collapsed transcript draws
153
- only a sanitized summary; the expanded transcript draws a sanitized,
154
- line-capped preview (`PREVIEW_LINES`). Control characters are stripped before
155
- a theme colour is applied, so raw document bytes never reach the terminal.
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 document cannot park the turn.
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/document/extract/<fmt>.ts` exporting a `DocExtractor` that
198
- `await import(...)`s its library, reads the bytes with `readDocBytes`, and
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 `DocumentFormat` entry to `FORMATS` and widen the `id` union.
202
- 4. Add `extract/<fmt>.test.ts` with a real minimal fixture, and extend
203
- `formats.test.ts` / `config.test.ts`.
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 | `document(pi)` in `index.ts` calls `registerDocTool(pi)`, which registers `read_doc`. The factory only registers; no parser loads until a read needs it. |
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 `document` section of the shared `bytefly.json` via `loadConfigSection` (`~/.pi/agent`, `<cwd>/.pi`). |
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`: the worktree
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. A table-driven registry keeps future formats
250
- additive, and extracting in process means the tool works on any host without a
251
- per-format install step and without spawning a child process per read.
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 document is ever read. Each extractor therefore imports its
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 format is rebuilt, not passed through.** `exceljs` exposes worksheets
260
- as objects; the extractor walks every worksheet and reassembles `[Sheet]` + tab
261
- rows so `read_doc`'s model-facing format is stable across extractor changes and
262
- every worksheet is included, not just the first.
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 is unchanged and reuses `context.lastComponent` (`Text.setText`),
265
- binding the expand hint to `app.tools.expand`. Path shortening lives in
266
- `render.ts` (not `lib/`): `lib/` helpers are for two or more extensions, and the
267
- built-in-style `~`/boundary rule here is document-only. The result line
268
- deliberately omits the path: Pi stacks the call and result lines in one row, so
269
- repeating it would be pure duplication.
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` | `DocumentFormat`, the `FORMATS` registry, `FormatId`, `detectFormat`, `supportedExtensions`, `unsupportedHint`. |
277
- | `tool.ts` | The tool definition, handler, path guard, extractor override seam, and renderer wiring. Re-exports `TOOL_NAME`. |
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`, `DocParams`, `DocOutput`, limits, result type. |
280
- | `config.ts` | `DocConfig`, registry-derived defaults, `normalizeConfig`, `loadConfig`, `isFormatEnabled`. |
281
- | `paging.ts` | `formatDoc`: code-point slicing and header/truncation formatting; `summarizeDoc`: the theme-free transcript summary. |
282
- | `extract/file.ts` | `DocFile` / `DocExtractor`, `readDocBytes` (abort-aware read), `normalizeBodyText`. |
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/document
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/document.ts`
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 `document` extension.
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 `document` section of the shared `bytefly.json` is read
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 DocConfig {
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: DocConfig = {
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(raw: Record<string, unknown> | undefined, base: DocConfig = DEFAULT_CONFIG): DocConfig {
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 `document` section (defaults, global, project, env). */
55
- export function loadConfig(cwd: string): DocConfig {
56
- return loadConfigSection(cwd, "document", DEFAULT_CONFIG, normalizeConfig);
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: DocConfig, id: FormatId): boolean {
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 DocExtractor } from "./file.ts";
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: DocExtractor = async (file) => {
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, readDocBytes, type DocExtractor } from "./file.ts";
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: DocExtractor = async (file) => {
13
- const bytes = await readDocBytes(file);
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 document format is extracted by a JavaScript library that reads the file
5
- * itself. This module owns the file read, the abort check, and the
6
- * {@link DocExtractor} type, so the format modules stay tiny and testable
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 DocFile {
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 DocExtractor = (file: DocFile) => Promise<string>;
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 readDocBytes(file: DocFile): Promise<Uint8Array> {
29
- file.signal?.throwIfAborted();
30
- const bytes = await readFile(file.path);
31
- file.signal?.throwIfAborted();
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, readDocBytes, type DocExtractor } from "./file.ts";
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: DocExtractor = async (file) => {
97
- const archive = unzipSync(await readDocBytes(file));
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 { readDocBytes, type DocExtractor } from "./file.ts";
10
+ import { readFormatBytes, type FormatExtractor } from "./file.ts";
11
11
 
12
12
  /** Extract text from every page of a PDF. */
13
- export const extractPdfDocument: DocExtractor = async (file) => {
14
- const bytes = await readDocBytes(file);
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, readDocBytes, type DocExtractor } from "./file.ts";
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: DocExtractor = async (file) => {
43
- const bytes = await readDocBytes(file);
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 { readDocBytes, type DocExtractor } from "./file.ts";
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: DocExtractor = async (file) => {
64
- const bytes = await readDocBytes(file);
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
- // `readDocBytes` copies into a fresh `Uint8Array`, so its buffer is exactly
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)) {