@better-fs-tools/read 0.1.0 → 0.2.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 +25 -18
- package/dist/classifiers/index.d.ts +4 -3
- package/dist/classifiers/index.js +4 -5
- package/dist/contract/limits.d.ts +1 -1
- package/dist/contract/messages.d.ts +0 -10
- package/dist/core/convert.js +2 -3
- package/dist/core/format.js +1 -15
- package/dist/core/limits.js +1 -1
- package/dist/core/messages.js +5 -8
- package/dist/core/notes.d.ts +0 -2
- package/dist/core/notes.js +0 -20
- package/dist/core/text-outcome.js +1 -2
- package/docs/architecture.md +5 -4
- package/docs/result-schema.md +18 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ read({ path, offset, limit }, ctx)
|
|
|
107
107
|
│ ├─ on a miss: suggest one bounded listing, names only
|
|
108
108
|
│ └─ on a directory .... a directory converter lists it, else NOT_A_FILE
|
|
109
109
|
│ authorize ............... host policy on the open file, before the core reads content
|
|
110
|
-
│ sample + classifiers .... text, image, PDF,
|
|
110
|
+
│ sample + classifiers .... text, image, PDF, Office, binary, ...
|
|
111
111
|
│ converters .............. other formats to text or media parts
|
|
112
112
|
│ scan .................... lines, clamping, view bytes, token budget, scan limit
|
|
113
113
|
│ verify .................. size check and change detection
|
|
@@ -343,7 +343,9 @@ While a prompt waits, the file stays open. If the file changes during the wait,
|
|
|
343
343
|
|
|
344
344
|
`classifiers` is an ordered list. Each classifier sees a bounded sample of the file (`limits.sampleBytes`). The first classifier with an opinion wins.
|
|
345
345
|
|
|
346
|
-
The default chain detects images, PDFs, Office documents,
|
|
346
|
+
The default chain detects images, PDFs, Office documents, binary content, and text encodings. It returns these `unsupported` codes: `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `BINARY`, and `UNKNOWN_ENCODING`. Each refusal has a note with code `unsupported-<code>`, for example `unsupported-pdf`.
|
|
347
|
+
|
|
348
|
+
A Jupyter notebook is JSON, so the default chain reads it as text. `notebookClassifier()` is exported but not in the chain. Put it first, as `classifiers: [notebookClassifier(), ...defaultClassifiers()]`, to refuse notebooks with code `NOTEBOOK`, or to convert them with `notebookConverter()`. The write tools use the same classifiers, so the same list makes them refuse a notebook too.
|
|
347
349
|
|
|
348
350
|
To detect by extension, or to change a refusal message:
|
|
349
351
|
|
|
@@ -370,13 +372,15 @@ A classifier only decides what a file is. A converter changes the content.
|
|
|
370
372
|
|
|
371
373
|
## File conversion, images, and directories
|
|
372
374
|
|
|
373
|
-
`converters` is a list. It runs after classification. The first file converter whose `accepts` returns `true` handles the file. The first directory converter handles directories. With no converters, text is read, other formats are `unsupported`, and a directory gives `NOT_A_FILE`.
|
|
375
|
+
`converters` is a list. It runs after classification. The first file converter whose `accepts` returns `true` handles the file. The first directory converter handles directories. With no converters, text is read, other formats are `unsupported`, and a directory gives `NOT_A_FILE`. The core has no converters by default. The Pi and AI SDK hosts add `imageConverter()` when you pass no `converters`.
|
|
374
376
|
|
|
375
377
|
```ts
|
|
376
378
|
import { createNodeReadTool } from "@better-fs-tools/node";
|
|
377
379
|
import {
|
|
380
|
+
defaultClassifiers,
|
|
378
381
|
directoryListing,
|
|
379
382
|
imageConverter,
|
|
383
|
+
notebookClassifier,
|
|
380
384
|
notebookConverter,
|
|
381
385
|
textConverter,
|
|
382
386
|
} from "@better-fs-tools/read";
|
|
@@ -385,6 +389,9 @@ import {
|
|
|
385
389
|
declare function pdfToText(source: AsyncIterable<Uint8Array>): AsyncIterable<string>;
|
|
386
390
|
|
|
387
391
|
export const read = createNodeReadTool({
|
|
392
|
+
// The default chain reads a notebook as JSON text. notebookClassifier()
|
|
393
|
+
// marks it NOTEBOOK, so notebookConverter() can take it.
|
|
394
|
+
classifiers: [notebookClassifier(), ...defaultClassifiers()],
|
|
388
395
|
converters: [
|
|
389
396
|
imageConverter(),
|
|
390
397
|
notebookConverter({ outputs: false }),
|
|
@@ -400,12 +407,12 @@ export const read = createNodeReadTool({
|
|
|
400
407
|
});
|
|
401
408
|
```
|
|
402
409
|
|
|
403
|
-
| Converter | Result
|
|
404
|
-
| ----------------------------------------------- |
|
|
405
|
-
| `imageConverter({ transform })` | Accepts code `IMAGE`. Gives a `media` result with the image as one content part. `transform` can resize it.
|
|
406
|
-
| `notebookConverter({ outputs })` | Accepts code `NOTEBOOK`. Gives the cells as text. `outputs` defaults to `true`. Broken notebook JSON is refused as `INVALID_NOTEBOOK`. |
|
|
407
|
-
| `textConverter({ id, accepts, mimeType, run })` | Builds a text converter from a stream function, for example `pdftotext`, `pandoc`, or `markitdown` in your host.
|
|
408
|
-
| `directoryListing({ trailingSlash, sort })` | A directory becomes text, one entry for each line. `sort` is `"name"` (the default) or `"type-then-name"`.
|
|
410
|
+
| Converter | Result |
|
|
411
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
412
|
+
| `imageConverter({ transform })` | Accepts code `IMAGE`. Gives a `media` result with the image as one content part. `transform` can resize it. |
|
|
413
|
+
| `notebookConverter({ outputs })` | Accepts code `NOTEBOOK`, so it needs `notebookClassifier()`. Gives the cells as text. `outputs` defaults to `true`. Broken notebook JSON is refused as `INVALID_NOTEBOOK`. |
|
|
414
|
+
| `textConverter({ id, accepts, mimeType, run })` | Builds a text converter from a stream function, for example `pdftotext`, `pandoc`, or `markitdown` in your host. |
|
|
415
|
+
| `directoryListing({ trailingSlash, sort })` | A directory becomes text, one entry for each line. `sort` is `"name"` (the default) or `"type-then-name"`. |
|
|
409
416
|
|
|
410
417
|
A converter returns one of these outcomes:
|
|
411
418
|
|
|
@@ -428,7 +435,7 @@ Conversion runs again on each continuation read. Cache inside your converter if
|
|
|
428
435
|
| Limit | Default | Meaning |
|
|
429
436
|
| --------------------- | ------- | ----------------------------------------------------- |
|
|
430
437
|
| `maxLines` | 2,000 | Lines in the view. Also the ceiling for `limit`. |
|
|
431
|
-
| `maxViewBytes` |
|
|
438
|
+
| `maxViewBytes` | 50 KiB | UTF-8 bytes of source text in the view |
|
|
432
439
|
| `maxCharsPerLine` | 2,000 | Characters in one line before it is clamped |
|
|
433
440
|
| `maxScanBytes` | 64 MiB | Bytes scanned before totals and content identity stop |
|
|
434
441
|
| `sampleBytes` | 8 KiB | Bytes given to classifiers. At most `maxScanBytes`. |
|
|
@@ -548,7 +555,7 @@ export const read = createNodeReadTool({
|
|
|
548
555
|
gutter: hashlineGutter({ width: 2 }), // "12:a3|text"
|
|
549
556
|
header: fileHashHeader(), // "file-hash: sha256:..."
|
|
550
557
|
footer: eofFooter((n) => `(End of file - total ${n} lines)`),
|
|
551
|
-
notes: (note) => (note.
|
|
558
|
+
notes: (note) => (note.severity === "info" ? null : note),
|
|
552
559
|
}),
|
|
553
560
|
});
|
|
554
561
|
```
|
|
@@ -568,7 +575,7 @@ import { opencodeFormat } from "@better-fs-tools/read/formats";
|
|
|
568
575
|
export const read = createNodeReadTool({ formatter: opencodeFormat() });
|
|
569
576
|
```
|
|
570
577
|
|
|
571
|
-
A custom formatter implements `format(outcome, ctx)`. It is synchronous and pure over the outcome. `ctx` has the `digest`, the `limits`, the `call`, and a `mode`. The mode is `"model"` for model output and `"view"` for the body only, with no notes, header, or footer. The Pi adapter uses `"view"` for its truncation details. If your formatter returns parts in `"view"` mode, Pi leaves those details out. A formatter sees only the finished outcome. It cannot read the file. If the formatter throws or returns neither a string nor an array, the call still succeeds: the core
|
|
578
|
+
A custom formatter implements `format(outcome, ctx)`. It is synchronous and pure over the outcome. `ctx` has the `digest`, the `limits`, the `call`, and a `mode`. The mode is `"model"` for model output and `"view"` for the body only, with no notes, header, or footer. The Pi adapter uses `"view"` for its truncation details. If your formatter returns parts in `"view"` mode, Pi leaves those details out. A formatter sees only the finished outcome. It cannot read the file. If the formatter throws or returns neither a string nor an array, the call still succeeds: the core formats the result with `lineNumberFormatter()` and adds no note.
|
|
572
579
|
|
|
573
580
|
## Messages and notes
|
|
574
581
|
|
|
@@ -683,7 +690,7 @@ Your dependencies can narrow access. They cannot widen it.
|
|
|
683
690
|
- The filesystem is the only place that grants access. It checks roots, deny roots, the realpath, and the type in one `open()` call. `resolve` runs before it and `authorize` runs after it.
|
|
684
691
|
- Each read opens at most one file. A resolver only changes the path that goes into that open.
|
|
685
692
|
- `nodeFileSystem` opens files with `O_NOFOLLOW | O_NONBLOCK` and checks the type first. It refuses directories, FIFOs, sockets, and devices before any content read. It refuses `/dev`, `/proc`, and `/sys` before it touches the filesystem.
|
|
686
|
-
- The core reads no content byte before `authorize` allows it, so no content reaches a classifier, a converter, or the model. `@better-fs-tools/cloudflare-shell` and `@better-fs-tools/just-bash`
|
|
693
|
+
- The core reads no content byte before `authorize` allows it, so no content reaches a classifier, a converter, or the model. `@better-fs-tools/cloudflare-shell` and `@better-fs-tools/just-bash` have already fetched the whole file in `open()`. `@better-fs-tools/cloudflare-computer` has started a `readFile` stream but read no chunk. If a denied read must not reach the backend, use a deny root, or leave the path out of the filesystem's allowed roots.
|
|
687
694
|
- The scan is bounded. Converters get a capped stream from the open file, never a path.
|
|
688
695
|
- Change detection runs after the scan and after conversion. A file that changed returns `CHANGED_DURING_READ`.
|
|
689
696
|
- A suggested name is never opened.
|
|
@@ -694,11 +701,11 @@ Your dependencies can narrow access. They cannot widen it.
|
|
|
694
701
|
|
|
695
702
|
Each adapter owns the signature, abort forwarding, and the mapping from `result.content` to the framework's content types. The adapters accept every core dependency and pass it through.
|
|
696
703
|
|
|
697
|
-
| Package | Factory | Notes
|
|
698
|
-
| ---------------------------------------------------------------------------------- | ------------------------------ |
|
|
699
|
-
| [`@better-fs-tools/ai-sdk`](https://www.npmjs.com/package/@better-fs-tools/ai-sdk) | `createAiSdkReadTool(options)` | `execute` returns the whole `ReadResult`. `toModelOutput` maps media parts to AI SDK `file` parts. You pass `fs`.
|
|
700
|
-
| [`@better-fs-tools/pi`](https://www.npmjs.com/package/@better-fs-tools/pi) | `createPiReadTool(options)` | Each call is confined to Pi's `ctx.cwd`. `fs`, `cwd`, and `allowedRoots` are refused. The `pi.extensions` entry registers `read`, `edit`,
|
|
701
|
-
| [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node) | `createNodeReadTool(options?)` | The core with Node defaults, for your own tool.
|
|
704
|
+
| Package | Factory | Notes |
|
|
705
|
+
| ---------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
706
|
+
| [`@better-fs-tools/ai-sdk`](https://www.npmjs.com/package/@better-fs-tools/ai-sdk) | `createAiSdkReadTool(options)` | `execute` returns the whole `ReadResult`. `toModelOutput` maps media parts to AI SDK `file` parts. You pass `fs`. |
|
|
707
|
+
| [`@better-fs-tools/pi`](https://www.npmjs.com/package/@better-fs-tools/pi) | `createPiReadTool(options)` | Each call is confined to Pi's `ctx.cwd`. `fs`, `cwd`, and `allowedRoots` are refused. The `pi.extensions` entry registers `read`, `edit`, and `write`. |
|
|
708
|
+
| [`@better-fs-tools/node`](https://www.npmjs.com/package/@better-fs-tools/node) | `createNodeReadTool(options?)` | The core with Node defaults, for your own tool. |
|
|
702
709
|
|
|
703
710
|
Cloudflare Agents hosts use `@better-fs-tools/ai-sdk` with `cloudflareShellFileSystem` from [`@better-fs-tools/cloudflare-shell`](https://www.npmjs.com/package/@better-fs-tools/cloudflare-shell). [`@better-fs-tools/cloudflare-computer`](https://www.npmjs.com/package/@better-fs-tools/cloudflare-computer) and [`@better-fs-tools/just-bash`](https://www.npmjs.com/package/@better-fs-tools/just-bash) are filesystems. Use them with `createReadTool()` or with an adapter.
|
|
704
711
|
|
|
@@ -8,8 +8,9 @@ export { officeClassifier } from "./office.ts";
|
|
|
8
8
|
export { pdfClassifier } from "./pdf.ts";
|
|
9
9
|
export { utf8Classifier } from "./utf8.ts";
|
|
10
10
|
/**
|
|
11
|
-
* The built-in chain in evaluation order: image, pdf, office,
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The built-in chain in evaluation order: image, pdf, office, binary, utf8.
|
|
12
|
+
* The first classifier with an opinion wins. utf8Classifier runs last and is
|
|
13
|
+
* the one that returns text. A notebook is JSON text here. Put
|
|
14
|
+
* notebookClassifier() before the chain to refuse or convert notebooks.
|
|
14
15
|
*/
|
|
15
16
|
export declare function defaultClassifiers(options?: DefaultClassifierOptions): Classifier[];
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { binaryClassifier } from "./binary.js";
|
|
2
2
|
import { imageClassifier } from "./image.js";
|
|
3
|
-
import { notebookClassifier } from "./notebook.js";
|
|
4
3
|
import { officeClassifier } from "./office.js";
|
|
5
4
|
import { pdfClassifier } from "./pdf.js";
|
|
6
5
|
import { utf8Classifier } from "./utf8.js";
|
|
@@ -12,9 +11,10 @@ export { officeClassifier } from "./office.js";
|
|
|
12
11
|
export { pdfClassifier } from "./pdf.js";
|
|
13
12
|
export { utf8Classifier } from "./utf8.js";
|
|
14
13
|
/**
|
|
15
|
-
* The built-in chain in evaluation order: image, pdf, office,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* The built-in chain in evaluation order: image, pdf, office, binary, utf8.
|
|
15
|
+
* The first classifier with an opinion wins. utf8Classifier runs last and is
|
|
16
|
+
* the one that returns text. A notebook is JSON text here. Put
|
|
17
|
+
* notebookClassifier() before the chain to refuse or convert notebooks.
|
|
18
18
|
*/
|
|
19
19
|
export function defaultClassifiers(options = {}) {
|
|
20
20
|
const notes = options.notes ?? {};
|
|
@@ -22,7 +22,6 @@ export function defaultClassifiers(options = {}) {
|
|
|
22
22
|
imageClassifier(withNote(notes.IMAGE)),
|
|
23
23
|
pdfClassifier(withNote(notes.PDF)),
|
|
24
24
|
officeClassifier(withNote(notes.OFFICE_DOCUMENT)),
|
|
25
|
-
notebookClassifier(withNote(notes.NOTEBOOK)),
|
|
26
25
|
binaryClassifier(withNote(notes.BINARY)),
|
|
27
26
|
utf8Classifier(withNote(notes.UNKNOWN_ENCODING)),
|
|
28
27
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface ReadLimits {
|
|
2
2
|
/** Lines in the view. Default 2_000. */
|
|
3
3
|
readonly maxLines: number;
|
|
4
|
-
/** UTF-8 bytes of source text in the view. Default
|
|
4
|
+
/** UTF-8 bytes of source text in the view. Default 50 KiB (51 200). */
|
|
5
5
|
readonly maxViewBytes: number;
|
|
6
6
|
/** Characters per line before clamping. Default 2_000. */
|
|
7
7
|
readonly maxCharsPerLine: number;
|
|
@@ -107,14 +107,4 @@ export interface ReadMessageCatalog extends ToolMessages {
|
|
|
107
107
|
viewModified(c: {
|
|
108
108
|
hook: string;
|
|
109
109
|
}): string;
|
|
110
|
-
weakIdentity(c: {
|
|
111
|
-
backend: string;
|
|
112
|
-
}): string;
|
|
113
|
-
bufferedBackend(c: {
|
|
114
|
-
backend: string;
|
|
115
|
-
}): string;
|
|
116
|
-
/** The formatter threw or returned neither a string nor an array. */
|
|
117
|
-
formatterFailed(c: {
|
|
118
|
-
formatter: string;
|
|
119
|
-
}): string;
|
|
120
110
|
}
|
package/dist/core/convert.js
CHANGED
|
@@ -3,7 +3,6 @@ import { ConvertSource } from "./convert-source.js";
|
|
|
3
3
|
import { checkConvertOutcome, firstMediaType, mediaBytes, refusedOutcome, scanConvertedText, tooLarge, } from "./converted.js";
|
|
4
4
|
import { AbortReadError, raceAbort } from "./cursor.js";
|
|
5
5
|
import { extensionId } from "./extension-error.js";
|
|
6
|
-
import { capabilityNotes } from "./notes.js";
|
|
7
6
|
import { buildObservation } from "./observation.js";
|
|
8
7
|
import { ReadStop } from "./outcomes.js";
|
|
9
8
|
import { textOutcome } from "./text-outcome.js";
|
|
@@ -111,7 +110,7 @@ async function drainAndVerify(source, handle, input, throwIfSourceFailed) {
|
|
|
111
110
|
await verifyHandle(handle, deps.messages, request, file, scope);
|
|
112
111
|
}
|
|
113
112
|
function mediaOutcome(input, classification, parts, notes, source) {
|
|
114
|
-
const { deps,
|
|
113
|
+
const { deps, request, file, converter } = input;
|
|
115
114
|
return {
|
|
116
115
|
tool: "read",
|
|
117
116
|
status: "media",
|
|
@@ -128,6 +127,6 @@ function mediaOutcome(input, classification, parts, notes, source) {
|
|
|
128
127
|
// Media is not source text, so it cannot back a text edit.
|
|
129
128
|
wholeFileVisible: false,
|
|
130
129
|
}),
|
|
131
|
-
notes
|
|
130
|
+
notes,
|
|
132
131
|
};
|
|
133
132
|
}
|
package/dist/core/format.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { lineNumberFormatter } from "../formatters/index.js";
|
|
2
|
-
import { extensionId } from "./extension-error.js";
|
|
3
2
|
export function formatResult(deps, call, outcome) {
|
|
4
3
|
const ctx = {
|
|
5
4
|
digest: deps.digest,
|
|
@@ -14,20 +13,7 @@ export function formatResult(deps, call, outcome) {
|
|
|
14
13
|
catch { }
|
|
15
14
|
if (content !== null)
|
|
16
15
|
return { ...outcome, content };
|
|
17
|
-
|
|
18
|
-
const failed = {
|
|
19
|
-
...outcome,
|
|
20
|
-
notes: [
|
|
21
|
-
...outcome.notes,
|
|
22
|
-
{
|
|
23
|
-
code: "extension-failed",
|
|
24
|
-
severity: "warning",
|
|
25
|
-
message: deps.messages.formatterFailed({ formatter: id ?? "formatter" }),
|
|
26
|
-
data: id === null ? { extension: "formatter" } : { extension: "formatter", id },
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
return { ...failed, content: toContent(lineNumberFormatter().format(failed, ctx)) ?? [] };
|
|
16
|
+
return { ...outcome, content: toContent(lineNumberFormatter().format(outcome, ctx)) ?? [] };
|
|
31
17
|
}
|
|
32
18
|
function toContent(output) {
|
|
33
19
|
if (typeof output === "string")
|
package/dist/core/limits.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The default read limits. They are not ceilings: a host may raise any of them. */
|
|
2
2
|
export const defaultReadLimits = Object.freeze({
|
|
3
3
|
maxLines: 2_000,
|
|
4
|
-
maxViewBytes:
|
|
4
|
+
maxViewBytes: 50 * 1_024,
|
|
5
5
|
maxCharsPerLine: 2_000,
|
|
6
6
|
maxScanBytes: 64 * 1_024 * 1_024,
|
|
7
7
|
sampleBytes: 8_192,
|
package/dist/core/messages.js
CHANGED
|
@@ -6,14 +6,14 @@ const catalog = {
|
|
|
6
6
|
offsetUnreached: ({ request, reachedLine, retry }) => `Scanning stopped before line ${request.offset} was reached. Retry from line ${reachedLine} with ${retry}.`,
|
|
7
7
|
lineClamped: ({ lines, total, maxChars }) => `Source line${total === 1 ? "" : "s"} ${lines.join(", ")}${total > lines.length ? ", …" : ""} exceeded ${maxChars} characters and ${total === 1 ? "was" : "were"} clamped.`,
|
|
8
8
|
limitClamped: ({ requested, max }) => `The requested line count of ${requested} is over the maximum, so at most ${max} lines are shown.`,
|
|
9
|
-
scanLimit: ({ maxScanBytes }) => `Scanning stopped at ${maxScanBytes} bytes
|
|
9
|
+
scanLimit: ({ maxScanBytes }) => `Scanning stopped at ${maxScanBytes} bytes, so the total line count is unknown.`,
|
|
10
10
|
empty: ({ path }) => `${path} is empty (0 bytes); no retry is needed.`,
|
|
11
11
|
notFound: ({ request, suggestions }) => suggestions.length === 0
|
|
12
12
|
? `${request.path} was not found.`
|
|
13
13
|
: `${request.path} was not found. Nearby names: ${quoted(suggestions)}.`,
|
|
14
14
|
pathRepaired: ({ from, to }) => `The requested filename ${JSON.stringify(from)} was repaired to the unique Unicode-equivalent path ${JSON.stringify(to)}.`,
|
|
15
|
-
notAFile: ({ request, kind }) => `${request.path} is a ${kindLabel(kind)}
|
|
16
|
-
dangerousPath: ({ request, detail }) => `${request.path}
|
|
15
|
+
notAFile: ({ request, kind }) => `${request.path} is a ${kindLabel(kind)}.`,
|
|
16
|
+
dangerousPath: ({ request, detail }) => `${request.path} is refused by policy${detail === null ? "" : ` (${detail})`}. Choose another path.`,
|
|
17
17
|
outsideAllowedRoots: ({ request }) => `${request.path} is outside every configured allowed root.`,
|
|
18
18
|
permissionDenied: ({ request }) => `Permission was denied for ${request.path}.`,
|
|
19
19
|
denied: ({ path, detail }) => `${path} was refused by policy${detail === null ? "" : ` (${detail})`}.`,
|
|
@@ -26,15 +26,12 @@ const catalog = {
|
|
|
26
26
|
: `The media converted from ${request.path} is larger than the ${limit}-byte media limit, so it was not returned.`;
|
|
27
27
|
},
|
|
28
28
|
changedDuringRead: ({ request, retry }) => `${request.path} changed while it was being read. Read it again with ${retry} before relying on this result.`,
|
|
29
|
-
aborted: ({ phase }) => `The read was aborted during the ${phase} phase
|
|
29
|
+
aborted: ({ phase }) => `The read was aborted during the ${phase} phase, so nothing was read.`,
|
|
30
30
|
invalidInput: ({ detail }) => `The read input was rejected: ${detail}`,
|
|
31
31
|
extensionFailed: ({ request, extension, phase }) => `The ${extension} extension failed during the ${phase} phase while reading ${request.path}; this is a host problem rather than a property of the file.`,
|
|
32
32
|
ioError: ({ request }) => `The backend failed while reading ${request === null ? "the requested path" : request.path}.`,
|
|
33
|
-
unsupportedBackend: ({ request, detail }) => `
|
|
33
|
+
unsupportedBackend: ({ request, detail }) => `This host cannot read ${request.path}${detail === null ? "" : ` (${detail})`}. Do not retry.`,
|
|
34
34
|
viewModified: ({ hook }) => `The ${hook} hook changed this view, so it is not the exact file text.`,
|
|
35
|
-
weakIdentity: ({ backend }) => `The ${backend} backend has no stable identity, so this observation cannot back a write precondition.`,
|
|
36
|
-
bufferedBackend: ({ backend }) => `The ${backend} backend buffers whole objects instead of streaming them.`,
|
|
37
|
-
formatterFailed: ({ formatter }) => `The ${formatter} formatter failed, so the default formatter formatted this result.`,
|
|
38
35
|
};
|
|
39
36
|
/** The default wording of every read message. */
|
|
40
37
|
export const defaultReadMessages = Object.freeze(catalog);
|
package/dist/core/notes.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { FileSystem } from "@better-fs-tools/fs";
|
|
2
1
|
import type { ReadRequest } from "../contract/input.ts";
|
|
3
2
|
import type { ReadLimits } from "../contract/limits.ts";
|
|
4
3
|
import type { ReadMessageCatalog } from "../contract/messages.ts";
|
|
@@ -13,4 +12,3 @@ export interface ViewNoteContext {
|
|
|
13
12
|
}
|
|
14
13
|
export declare function buildViewNotes(context: ViewNoteContext): ReadNote[];
|
|
15
14
|
export declare function emptyNote(messages: Readonly<ReadMessageCatalog>, path: string): ReadNote;
|
|
16
|
-
export declare function capabilityNotes(fs: FileSystem, messages: Readonly<ReadMessageCatalog>): ReadNote[];
|
package/dist/core/notes.js
CHANGED
|
@@ -89,26 +89,6 @@ export function buildViewNotes(context) {
|
|
|
89
89
|
export function emptyNote(messages, path) {
|
|
90
90
|
return { code: "empty", severity: "info", message: messages.empty({ path }) };
|
|
91
91
|
}
|
|
92
|
-
export function capabilityNotes(fs, messages) {
|
|
93
|
-
const notes = [];
|
|
94
|
-
if (!fs.capabilities.identity) {
|
|
95
|
-
notes.push({
|
|
96
|
-
code: "weak-identity",
|
|
97
|
-
severity: "info",
|
|
98
|
-
message: messages.weakIdentity({ backend: fs.id }),
|
|
99
|
-
data: { backend: fs.id },
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
if (!fs.capabilities.streaming) {
|
|
103
|
-
notes.push({
|
|
104
|
-
code: "buffered-backend",
|
|
105
|
-
severity: "info",
|
|
106
|
-
message: messages.bufferedBackend({ backend: fs.id }),
|
|
107
|
-
data: { backend: fs.id },
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return notes;
|
|
111
|
-
}
|
|
112
92
|
function retryFrom(request, offset) {
|
|
113
93
|
return { path: request.path, offset, limit: request.limit };
|
|
114
94
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildViewNotes,
|
|
1
|
+
import { buildViewNotes, emptyNote } from "./notes.js";
|
|
2
2
|
import { buildObservation } from "./observation.js";
|
|
3
3
|
export function textOutcome(input) {
|
|
4
4
|
const { deps, fs, request, file, classification, conversion, scan } = input;
|
|
@@ -67,7 +67,6 @@ export function textOutcome(input) {
|
|
|
67
67
|
...(conversion === null && reachedEof && scannedBytes === 0
|
|
68
68
|
? [emptyNote(deps.messages, file.displayPath)]
|
|
69
69
|
: []),
|
|
70
|
-
...(fs === null ? [] : capabilityNotes(fs, deps.messages)),
|
|
71
70
|
],
|
|
72
71
|
};
|
|
73
72
|
}
|
package/docs/architecture.md
CHANGED
|
@@ -113,10 +113,11 @@ handle.verify() did the opened object change?
|
|
|
113
113
|
handle.close() always, from a finally block
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
One capability remains. It adds no note to the result, because the model cannot act on it:
|
|
117
117
|
|
|
118
|
-
- `
|
|
119
|
-
|
|
118
|
+
- `identity: false` sets `file.identity` to `null`. The write tools then judge freshness by the content hash only.
|
|
119
|
+
|
|
120
|
+
The read core reads `bytes()` the same way whether it yields one chunk or many, so a backend does not report which.
|
|
120
121
|
|
|
121
122
|
An optional `list()` is the list capability. Without it, there are no suggestions and no directory listings.
|
|
122
123
|
|
|
@@ -214,7 +215,7 @@ The core makes the other failures itself:
|
|
|
214
215
|
- `IO_ERROR` when a filesystem method or the byte stream throws.
|
|
215
216
|
- `EXTENSION_FAILED` when host code throws or breaks a rule. `data.extension` names the dependency (`fs`, `state`, `resolve`, `suggest`, `authorize`, `converters`, `budget`, or `hooks`), `data.phase` names the stage, and `data.id` names the extension object when it has one.
|
|
216
217
|
|
|
217
|
-
A formatter that throws, or returns neither a string nor an array, does not reject the call. The core
|
|
218
|
+
A formatter that throws, or returns neither a string nor an array, does not reject the call. The core formats the result with `lineNumberFormatter()` and adds no note. The status stays as it was.
|
|
218
219
|
|
|
219
220
|
## Adapters
|
|
220
221
|
|
package/docs/result-schema.md
CHANGED
|
@@ -97,12 +97,13 @@ Field notes:
|
|
|
97
97
|
|
|
98
98
|
`code` comes from the classifier, a converter refusal, or the core:
|
|
99
99
|
|
|
100
|
-
| Code
|
|
101
|
-
|
|
|
102
|
-
| `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
|
|
|
100
|
+
| Code | Source |
|
|
101
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `IMAGE`, `PDF`, `OFFICE_DOCUMENT`, `BINARY`, `UNKNOWN_ENCODING` | The default classifiers |
|
|
103
|
+
| `NOTEBOOK` | `notebookClassifier()`, which is not in the default chain |
|
|
104
|
+
| `INVALID_NOTEBOOK` | `notebookConverter()` on broken notebook JSON |
|
|
105
|
+
| `TOO_LARGE` | The core: converter input over `maxConvertBytes`, or media over `maxMediaBytes`. A backend ceiling gives `status: "error"` with `TOO_LARGE` instead |
|
|
106
|
+
| any other string | Your own classifier or converter |
|
|
106
107
|
|
|
107
108
|
## `error`
|
|
108
109
|
|
|
@@ -162,8 +163,6 @@ Notes from the core:
|
|
|
162
163
|
| `scan-limit` | warning | none | `{ maxScanBytes }` |
|
|
163
164
|
| `offset-past-eof` | info | the last line | `{ totalLines }` |
|
|
164
165
|
| `empty` | info | none | none |
|
|
165
|
-
| `weak-identity` | info | none | `{ backend }` |
|
|
166
|
-
| `buffered-backend` | info | none | `{ backend }` |
|
|
167
166
|
| `not-found` | warning | none | `{ suggestions?, entriesTruncated?, detail?, cause? }` |
|
|
168
167
|
| `not-a-file` | warning | none | `{ kind, detail?, cause? }` |
|
|
169
168
|
| `dangerous-path`, `outside-allowed-roots`, `permission-denied`, `denied`, `unsupported-backend`, `io-error` | warning | none | `{ detail?, cause? }` from the filesystem. A thrown error gives `io-error` with `{ detail }`. |
|
|
@@ -172,20 +171,21 @@ Notes from the core:
|
|
|
172
171
|
| `changed-during-read` | warning | the same range | none |
|
|
173
172
|
| `aborted` | warning | none | `{ phase }` |
|
|
174
173
|
| `invalid-input` | warning | none | `{ path }` when the input had a string path |
|
|
175
|
-
| `extension-failed` | warning | none | `{ extension, phase, id? }
|
|
174
|
+
| `extension-failed` | warning | none | `{ extension, phase, id? }` |
|
|
176
175
|
| `view-modified` | info | none | `{ hook }` |
|
|
177
176
|
|
|
178
177
|
Notes from the built-in helpers:
|
|
179
178
|
|
|
180
|
-
| `code`
|
|
181
|
-
|
|
|
182
|
-
| `utf8-bom`
|
|
183
|
-
| `unsupported-image`, `unsupported-pdf`, `unsupported-office-document`, `unsupported-
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
179
|
+
| `code` | Severity | From | `data` |
|
|
180
|
+
| --------------------------------------------------------------------------------------------------------------------------- | -------- | ----------------------- | ------------------------------- |
|
|
181
|
+
| `utf8-bom` | info | `utf8Classifier` | none |
|
|
182
|
+
| `unsupported-image`, `unsupported-pdf`, `unsupported-office-document`, `unsupported-binary`, `unsupported-unknown-encoding` | warning | the default classifiers | as set by a `NoteOverride` |
|
|
183
|
+
| `unsupported-notebook` | warning | `notebookClassifier()` | as set by a `NoteOverride` |
|
|
184
|
+
| `path-repaired` | warning | `unicodeRepair()` | `{ from, to }` |
|
|
185
|
+
| `invalid-notebook` | warning | `notebookConverter()` | none |
|
|
186
|
+
| `directory-truncated` | warning | `directoryListing()` | `{ maxDirectoryEntries }` |
|
|
187
|
+
| `empty-directory` | info | `directoryListing()` | none |
|
|
188
|
+
| `repeat-read` | info | `repeatReadGuard()` | `{ observationId, observedAt }` |
|
|
189
189
|
|
|
190
190
|
`directoryListing()` also has a `list-failed` refusal for a listing that failed. The core reports the listing's own error first, so a read with the built-in converter gives that error, for example `DENIED` or `IO_ERROR`.
|
|
191
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-fs-tools/read",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A bounded file reader for TypeScript agent tools.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@better-fs-tools/fs": "^0.
|
|
49
|
+
"@better-fs-tools/fs": "^0.2.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"typebox": "^1.3.16"
|