@gavin-hu/my-pi-agent 0.15.1 → 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 (87) hide show
  1. package/README.md +10 -7
  2. package/README.zh-CN.md +8 -5
  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 +38 -18
  26. package/extensions/file-browse/config.ts +8 -0
  27. package/extensions/file-browse/html.ts +2 -2
  28. package/extensions/file-browse/index.ts +17 -2
  29. package/extensions/file-browse/live-client.ts +13 -13
  30. package/extensions/file-browse/live.ts +44 -10
  31. package/extensions/file-browse/markdown.ts +3 -10
  32. package/extensions/file-browse/raw-html.ts +3 -10
  33. package/extensions/file-browse/server.ts +13 -2
  34. package/extensions/file-open/README.md +1 -2
  35. package/extensions/{document → file-read}/README.md +72 -56
  36. package/extensions/{document → file-read}/config.ts +12 -9
  37. package/extensions/{document → file-read}/extract/doc.ts +2 -2
  38. package/extensions/{document → file-read}/extract/docx.ts +3 -3
  39. package/extensions/{document → file-read}/extract/file.ts +9 -9
  40. package/extensions/{document → file-read}/extract/odt.ts +3 -3
  41. package/extensions/{document → file-read}/extract/pdf.ts +3 -3
  42. package/extensions/{document → file-read}/extract/rtf.ts +3 -3
  43. package/extensions/{document → file-read}/extract/xlsx.ts +4 -4
  44. package/extensions/{document → file-read}/formats.ts +11 -11
  45. package/extensions/file-read/index.ts +17 -0
  46. package/extensions/{document → file-read}/paging.ts +15 -13
  47. package/extensions/{document → file-read}/render.ts +17 -13
  48. package/extensions/{document → file-read}/schema.ts +7 -7
  49. package/extensions/{document → file-read}/tool.ts +36 -28
  50. package/extensions/goal/tools.ts +10 -14
  51. package/extensions/job/commands.ts +32 -46
  52. package/extensions/job/runtime.ts +6 -8
  53. package/extensions/job/tools.ts +7 -26
  54. package/extensions/memory/commands.ts +30 -44
  55. package/extensions/memory/tools.ts +6 -14
  56. package/extensions/plan/tools/enter.ts +6 -7
  57. package/extensions/plan/tools/exit.ts +6 -7
  58. package/extensions/plan/tools/steps.ts +6 -7
  59. package/extensions/plan/tools/write.ts +8 -9
  60. package/extensions/plan/web.ts +21 -12
  61. package/extensions/question/tools.ts +5 -4
  62. package/extensions/subagent/background.ts +1 -7
  63. package/extensions/todo/tools.ts +10 -14
  64. package/extensions/watch/README.md +5 -5
  65. package/extensions/watch/commands.ts +33 -47
  66. package/extensions/watch/runtime.ts +5 -7
  67. package/extensions/watch/tools.ts +7 -18
  68. package/extensions/web-access/README.md +3 -3
  69. package/extensions/web-access/search/search.ts +9 -18
  70. package/extensions/wechat/timer.ts +9 -30
  71. package/extensions/wechat/tools.ts +4 -7
  72. package/lib/README.md +9 -6
  73. package/lib/escape.ts +15 -2
  74. package/lib/pdf.ts +2 -2
  75. package/lib/requirements.ts +2 -2
  76. package/lib/screen-session.ts +85 -0
  77. package/lib/timer.ts +61 -0
  78. package/lib/tui/README.md +3 -3
  79. package/lib/tui/columns.ts +33 -8
  80. package/lib/tui/rows.ts +15 -6
  81. package/lib/tui/rules.ts +4 -8
  82. package/lib/tui/tool.ts +55 -2
  83. package/package.json +8 -4
  84. package/skills/changelog/SKILL.md +136 -0
  85. package/extensions/document/index.ts +0 -17
  86. /package/extensions/{document → file-read}/deadline.ts +0 -0
  87. /package/extensions/{document → file-read}/extract/vendor.d.ts +0 -0
@@ -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)) {
@@ -1,14 +1,14 @@
1
1
  /**
2
- * The supported document formats, in one table.
2
+ * The supported formats, in one table.
3
3
  *
4
4
  * The tool handler, the config defaults, the `formats` merge, and the
5
- * unsupported-file error all derive from `FORMATS`, so adding a format is a
6
- * local change: one extractor module plus one entry here. See the README's
7
- * "Adding a format" section.
5
+ * unsupported-file error all derive from `FORMATS`, so a format is a local
6
+ * change: one extractor module plus one entry here. See the README's "Adding a
7
+ * format" section.
8
8
  */
9
9
 
10
10
  import { extname } from "node:path";
11
- import type { DocExtractor } from "./extract/file.ts";
11
+ import type { FormatExtractor } from "./extract/file.ts";
12
12
  import { extractDocText } from "./extract/doc.ts";
13
13
  import { extractDocxText } from "./extract/docx.ts";
14
14
  import { extractOdtText } from "./extract/odt.ts";
@@ -16,17 +16,17 @@ import { extractPdfDocument } from "./extract/pdf.ts";
16
16
  import { extractRtfText } from "./extract/rtf.ts";
17
17
  import { extractXlsxText } from "./extract/xlsx.ts";
18
18
 
19
- export interface DocumentFormat {
20
- /** Stable id, also the `document.formats` key in `bytefly.json` and the output `format`. */
19
+ export interface ReadFormat {
20
+ /** Stable id, also the `file-read.formats` key in `bytefly.json` and the output `format`. */
21
21
  id: "pdf" | "docx" | "doc" | "odt" | "rtf" | "xlsx";
22
22
  /** Human label for messages. */
23
23
  label: string;
24
24
  /** Lower-case extensions, with the dot, that select this format. */
25
25
  extensions: readonly string[];
26
- /** Enabled unless `document.formats[id]` in `bytefly.json` says otherwise. */
26
+ /** Enabled unless `file-read.formats[id]` in `bytefly.json` says otherwise. */
27
27
  defaultEnabled: boolean;
28
28
  /** Extractor; runs the format's parser and throws a readable error when it fails. */
29
- extract: DocExtractor;
29
+ extract: FormatExtractor;
30
30
  }
31
31
 
32
32
  export const FORMATS = [
@@ -36,7 +36,7 @@ export const FORMATS = [
36
36
  { id: "odt", label: "ODT", extensions: [".odt"], defaultEnabled: true, extract: extractOdtText },
37
37
  { id: "rtf", label: "RTF", extensions: [".rtf"], defaultEnabled: true, extract: extractRtfText },
38
38
  { id: "xlsx", label: "XLSX", extensions: [".xlsx"], defaultEnabled: true, extract: extractXlsxText },
39
- ] as const satisfies readonly DocumentFormat[];
39
+ ] as const satisfies readonly ReadFormat[];
40
40
 
41
41
  export type FormatId = (typeof FORMATS)[number]["id"];
42
42
 
@@ -46,7 +46,7 @@ export function supportedExtensions(): string[] {
46
46
  }
47
47
 
48
48
  /** The format selected by a path's extension, or undefined when unsupported. */
49
- export function detectFormat(path: string): DocumentFormat | undefined {
49
+ export function detectFormat(path: string): ReadFormat | undefined {
50
50
  const ext = extname(path).toLowerCase();
51
51
  return FORMATS.find((format) => (format.extensions as readonly string[]).includes(ext));
52
52
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The `file-read` extension entrypoint.
3
+ *
4
+ * Registers `read_doc`, which extracts text from a local document or workbook
5
+ * under the effective working root using in-process parsers. The factory only
6
+ * registers; a parser library loads lazily on the first read of its format, so
7
+ * the extension costs nothing at startup.
8
+ */
9
+
10
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
+ import { isExtensionEnabled } from "../../lib/env.ts";
12
+ import { registerReadTool } from "./tool.ts";
13
+
14
+ export default function fileRead(pi: ExtensionAPI): void {
15
+ if (!isExtensionEnabled("file-read")) return;
16
+ registerReadTool(pi);
17
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Model-facing text formatting and slicing for `read_doc`.
3
3
  *
4
- * `formatDoc` slices extracted text by code point from `startIndex`, so CJK and
4
+ * `formatPage` slices extracted text by code point from `startIndex`, so CJK and
5
5
  * emoji stay well-formed, and names the offset of the next chunk. The body is
6
6
  * the clean slice; the text adds a header and any truncation note.
7
7
  */
@@ -9,7 +9,9 @@
9
9
  import { formatCount } from "../../lib/format.ts";
10
10
  import type { FormatId } from "./formats.ts";
11
11
 
12
- export interface DocPageInput {
12
+ export interface PageInput {
13
+ /** The tool that produced this page; named in the truncation note. */
14
+ tool: string;
13
15
  path: string;
14
16
  format: FormatId;
15
17
  bytes: number;
@@ -18,8 +20,8 @@ export interface DocPageInput {
18
20
  maxChars: number;
19
21
  }
20
22
 
21
- export interface FormattedDoc {
22
- /** The requested slice of the document text. */
23
+ export interface FormattedPage {
24
+ /** The requested slice of the extracted text. */
23
25
  body: string;
24
26
  /** Header, body, and any truncation note. */
25
27
  text: string;
@@ -30,13 +32,13 @@ export interface FormattedDoc {
30
32
  chars: number;
31
33
  }
32
34
 
33
- export function formatDoc(input: DocPageInput): FormattedDoc {
35
+ export function formatPage(input: PageInput): FormattedPage {
34
36
  const points = Array.from(input.text);
35
37
  const total = points.length;
36
38
  const header = `Path: ${input.path}\nFormat: ${input.format.toUpperCase()}\nSize: ${input.bytes} bytes`;
37
39
 
38
40
  if (total === 0) {
39
- const text = `${header}\n\n(no extractable text; the document may be scanned or image-only)`;
41
+ const text = `${header}\n\n(no extractable text; the file may be empty or contain no readable text)`;
40
42
  return { body: "", text, truncated: false, nextIndex: input.startIndex, chars: 0 };
41
43
  }
42
44
 
@@ -52,14 +54,14 @@ export function formatDoc(input: DocPageInput): FormattedDoc {
52
54
 
53
55
  let text = `${header}\n\n${body}`;
54
56
  if (truncated) {
55
- text += `\n\n… (truncated at ${nextIndex} of ${total} characters; call read_doc again with startIndex=${nextIndex})`;
57
+ text += `\n\n… (truncated at ${nextIndex} of ${total} characters; call ${input.tool} again with startIndex=${nextIndex})`;
56
58
  }
57
59
 
58
60
  return { body, text, truncated, nextIndex, chars: total };
59
61
  }
60
62
 
61
- /** Inputs for `summarizeDoc`, the fields the transcript summary reads. */
62
- export interface DocSummaryInput {
63
+ /** Inputs for `summarizeRead`, the fields the transcript summary reads. */
64
+ export interface SummaryInput {
63
65
  format: FormatId;
64
66
  /** Total characters in the extracted text. */
65
67
  chars: number;
@@ -69,7 +71,7 @@ export interface DocSummaryInput {
69
71
  }
70
72
 
71
73
  /** Theme-free transcript summary: the format, the range detail, and any note. */
72
- export interface DocSummary {
74
+ export interface PageSummary {
73
75
  /** Uppercase format id, e.g. `PDF`. */
74
76
  format: string;
75
77
  /** Range detail, e.g. `chars 1–40,000 of 42,478`. */
@@ -81,13 +83,13 @@ export interface DocSummary {
81
83
  }
82
84
 
83
85
  /**
84
- * Describe a `read_doc` result for the transcript, without theme colours.
86
+ * Describe a file-read result for the transcript, without theme colours.
85
87
  *
86
88
  * A complete first read shows the total; a paged read shows the returned range
87
89
  * and where to continue, so a truncated result is never mistaken for the whole
88
- * document.
90
+ * file.
89
91
  */
90
- export function summarizeDoc(input: DocSummaryInput): DocSummary {
92
+ export function summarizeRead(input: SummaryInput): PageSummary {
91
93
  const format = input.format.toUpperCase();
92
94
  if (input.chars === 0) return { format, detail: "no extractable text", truncated: false };
93
95
  if (input.startIndex >= input.chars) {