@better-fs-tools/read 0.0.0-tegami-trusted-publish-setup → 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/LICENSE +21 -0
- package/README.md +807 -6
- package/dist/authorize/ask-user.d.ts +7 -0
- package/dist/authorize/ask-user.js +25 -0
- package/dist/authorize/authorizers.d.ts +7 -0
- package/dist/authorize/authorizers.js +34 -0
- package/dist/authorize/deny-paths.d.ts +7 -0
- package/dist/authorize/deny-paths.js +25 -0
- package/dist/authorize/glob.d.ts +7 -0
- package/dist/authorize/glob.js +46 -0
- package/dist/authorize/index.d.ts +5 -0
- package/dist/authorize/index.js +5 -0
- package/dist/authorize/shared.d.ts +9 -0
- package/dist/authorize/shared.js +12 -0
- package/dist/authorize/size-ceiling.d.ts +10 -0
- package/dist/authorize/size-ceiling.js +34 -0
- package/dist/budget/chars-per-token.d.ts +9 -0
- package/dist/budget/chars-per-token.js +15 -0
- package/dist/budget/index.d.ts +2 -0
- package/dist/budget/index.js +1 -0
- package/dist/classifiers/binary.d.ts +5 -0
- package/dist/classifiers/binary.js +13 -0
- package/dist/classifiers/extension.d.ts +17 -0
- package/dist/classifiers/extension.js +45 -0
- package/dist/classifiers/image.d.ts +5 -0
- package/dist/classifiers/image.js +22 -0
- package/dist/classifiers/index.d.ts +16 -0
- package/dist/classifiers/index.js +31 -0
- package/dist/classifiers/notebook.d.ts +5 -0
- package/dist/classifiers/notebook.js +14 -0
- package/dist/classifiers/office.d.ts +5 -0
- package/dist/classifiers/office.js +30 -0
- package/dist/classifiers/pdf.d.ts +4 -0
- package/dist/classifiers/pdf.js +8 -0
- package/dist/classifiers/shared.d.ts +17 -0
- package/dist/classifiers/shared.js +57 -0
- package/dist/classifiers/utf8.d.ts +8 -0
- package/dist/classifiers/utf8.js +61 -0
- package/dist/contract/base.d.ts +126 -0
- package/dist/contract/base.js +0 -0
- package/dist/contract/classify.d.ts +50 -0
- package/dist/contract/classify.js +0 -0
- package/dist/contract/context.d.ts +13 -0
- package/dist/contract/context.js +0 -0
- package/dist/contract/deps.d.ts +60 -0
- package/dist/contract/deps.js +0 -0
- package/dist/contract/digest.d.ts +13 -0
- package/dist/contract/digest.js +0 -0
- package/dist/contract/extensions.d.ts +115 -0
- package/dist/contract/extensions.js +0 -0
- package/dist/contract/format.d.ts +16 -0
- package/dist/contract/format.js +0 -0
- package/dist/contract/input.d.ts +16 -0
- package/dist/contract/input.js +0 -0
- package/dist/contract/json.d.ts +6 -0
- package/dist/contract/json.js +0 -0
- package/dist/contract/limits.d.ts +20 -0
- package/dist/contract/limits.js +0 -0
- package/dist/contract/messages.d.ts +110 -0
- package/dist/contract/messages.js +0 -0
- package/dist/contract/result.d.ts +153 -0
- package/dist/contract/result.js +0 -0
- package/dist/contract/state.d.ts +36 -0
- package/dist/contract/state.js +0 -0
- package/dist/converters/directory-listing.d.ts +13 -0
- package/dist/converters/directory-listing.js +63 -0
- package/dist/converters/image.d.ts +7 -0
- package/dist/converters/image.js +25 -0
- package/dist/converters/index.d.ts +8 -0
- package/dist/converters/index.js +4 -0
- package/dist/converters/notebook.d.ts +12 -0
- package/dist/converters/notebook.js +106 -0
- package/dist/converters/shared.d.ts +5 -0
- package/dist/converters/shared.js +18 -0
- package/dist/converters/text.d.ts +12 -0
- package/dist/converters/text.js +24 -0
- package/dist/core/authorize.d.ts +8 -0
- package/dist/core/authorize.js +76 -0
- package/dist/core/budget.d.ts +7 -0
- package/dist/core/budget.js +23 -0
- package/dist/core/call-scope.d.ts +36 -0
- package/dist/core/call-scope.js +112 -0
- package/dist/core/classify.d.ts +9 -0
- package/dist/core/classify.js +27 -0
- package/dist/core/convert-source.d.ts +29 -0
- package/dist/core/convert-source.js +85 -0
- package/dist/core/convert.d.ts +23 -0
- package/dist/core/convert.js +132 -0
- package/dist/core/converted.d.ts +18 -0
- package/dist/core/converted.js +111 -0
- package/dist/core/create-read-tool.d.ts +7 -0
- package/dist/core/create-read-tool.js +18 -0
- package/dist/core/cursor.d.ts +14 -0
- package/dist/core/cursor.js +93 -0
- package/dist/core/deps.d.ts +6 -0
- package/dist/core/deps.js +121 -0
- package/dist/core/directory.d.ts +16 -0
- package/dist/core/directory.js +113 -0
- package/dist/core/extension-error.d.ts +9 -0
- package/dist/core/extension-error.js +18 -0
- package/dist/core/format.d.ts +8 -0
- package/dist/core/format.js +29 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/hooks.js +140 -0
- package/dist/core/input.d.ts +10 -0
- package/dist/core/input.js +45 -0
- package/dist/core/limits.d.ts +9 -0
- package/dist/core/limits.js +43 -0
- package/dist/core/messages.d.ts +8 -0
- package/dist/core/messages.js +78 -0
- package/dist/core/notes.d.ts +14 -0
- package/dist/core/notes.js +94 -0
- package/dist/core/observation.d.ts +11 -0
- package/dist/core/observation.js +51 -0
- package/dist/core/open.d.ts +16 -0
- package/dist/core/open.js +33 -0
- package/dist/core/outcomes.d.ts +27 -0
- package/dist/core/outcomes.js +172 -0
- package/dist/core/pipeline.d.ts +4 -0
- package/dist/core/pipeline.js +175 -0
- package/dist/core/record.d.ts +3 -0
- package/dist/core/record.js +37 -0
- package/dist/core/resolve.d.ts +15 -0
- package/dist/core/resolve.js +43 -0
- package/dist/core/same.d.ts +2 -0
- package/dist/core/same.js +22 -0
- package/dist/core/sample.d.ts +6 -0
- package/dist/core/sample.js +24 -0
- package/dist/core/scan.d.ts +26 -0
- package/dist/core/scan.js +95 -0
- package/dist/core/scanner.d.ts +29 -0
- package/dist/core/scanner.js +124 -0
- package/dist/core/suggest.d.ts +6 -0
- package/dist/core/suggest.js +41 -0
- package/dist/core/text-outcome.d.ts +19 -0
- package/dist/core/text-outcome.js +72 -0
- package/dist/core/verify.d.ts +7 -0
- package/dist/core/verify.js +15 -0
- package/dist/digest/sha256.d.ts +7 -0
- package/dist/digest/sha256.js +131 -0
- package/dist/formats/deep-agents.d.ts +7 -0
- package/dist/formats/deep-agents.js +23 -0
- package/dist/formats/hashline.d.ts +6 -0
- package/dist/formats/hashline.js +16 -0
- package/dist/formats/hermes.d.ts +9 -0
- package/dist/formats/hermes.js +62 -0
- package/dist/formats/index.d.ts +4 -0
- package/dist/formats/index.js +4 -0
- package/dist/formats/opencode.d.ts +7 -0
- package/dist/formats/opencode.js +35 -0
- package/dist/formats/shared.d.ts +4 -0
- package/dist/formats/shared.js +9 -0
- package/dist/formatters/eof.d.ts +7 -0
- package/dist/formatters/eof.js +20 -0
- package/dist/formatters/file-hash.d.ts +7 -0
- package/dist/formatters/file-hash.js +13 -0
- package/dist/formatters/hashline.d.ts +11 -0
- package/dist/formatters/hashline.js +18 -0
- package/dist/formatters/index.d.ts +10 -0
- package/dist/formatters/index.js +7 -0
- package/dist/formatters/json.d.ts +17 -0
- package/dist/formatters/json.js +36 -0
- package/dist/formatters/layout.d.ts +15 -0
- package/dist/formatters/layout.js +33 -0
- package/dist/formatters/line-number.d.ts +11 -0
- package/dist/formatters/line-number.js +23 -0
- package/dist/formatters/plain.d.ts +7 -0
- package/dist/formatters/plain.js +14 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/redact.d.ts +14 -0
- package/dist/hooks/redact.js +53 -0
- package/dist/hooks/repeat-read-guard.d.ts +13 -0
- package/dist/hooks/repeat-read-guard.js +49 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +15 -0
- package/dist/resolve/expand-home.d.ts +5 -0
- package/dist/resolve/expand-home.js +17 -0
- package/dist/resolve/index.d.ts +5 -0
- package/dist/resolve/index.js +5 -0
- package/dist/resolve/path-resolvers.d.ts +7 -0
- package/dist/resolve/path-resolvers.js +38 -0
- package/dist/resolve/reanchor-leading-slash.d.ts +9 -0
- package/dist/resolve/reanchor-leading-slash.js +23 -0
- package/dist/resolve/strip-prefixes.d.ts +6 -0
- package/dist/resolve/strip-prefixes.js +30 -0
- package/dist/resolve/unicode-repair.d.ts +9 -0
- package/dist/resolve/unicode-repair.js +38 -0
- package/dist/signature/contract.d.ts +18 -0
- package/dist/signature/contract.js +0 -0
- package/dist/signature/index.d.ts +4 -0
- package/dist/signature/index.js +3 -0
- package/dist/signature/line-range.d.ts +7 -0
- package/dist/signature/line-range.js +59 -0
- package/dist/signature/messages.d.ts +4 -0
- package/dist/signature/messages.js +9 -0
- package/dist/signature/offset-limit.d.ts +13 -0
- package/dist/signature/offset-limit.js +59 -0
- package/dist/signature/schema.d.ts +10 -0
- package/dist/signature/schema.js +80 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.js +77 -0
- package/dist/suggest/canonicalize.d.ts +6 -0
- package/dist/suggest/canonicalize.js +12 -0
- package/dist/suggest/default-suggest.d.ts +3 -0
- package/dist/suggest/default-suggest.js +12 -0
- package/dist/suggest/index.d.ts +3 -0
- package/dist/suggest/index.js +3 -0
- package/dist/suggest/suggest-file-names.d.ts +2 -0
- package/dist/suggest/suggest-file-names.js +46 -0
- package/docs/architecture.md +250 -0
- package/docs/result-schema.md +222 -0
- package/package.json +51 -2
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Note, ToolError } from "./base.ts";
|
|
2
|
+
import type { ReadInput, ReadRequest } from "./input.ts";
|
|
3
|
+
import type { ReadPhase } from "./messages.ts";
|
|
4
|
+
export interface TextPart {
|
|
5
|
+
readonly type: "text";
|
|
6
|
+
readonly text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MediaPart {
|
|
9
|
+
readonly type: "media";
|
|
10
|
+
readonly mediaType: string;
|
|
11
|
+
readonly data: Uint8Array;
|
|
12
|
+
readonly name?: string;
|
|
13
|
+
}
|
|
14
|
+
export type ContentPart = TextPart | MediaPart;
|
|
15
|
+
/**
|
|
16
|
+
* One variant for each status. `status` narrows the union: only the "error"
|
|
17
|
+
* variant has `error`, and it is never null there.
|
|
18
|
+
*/
|
|
19
|
+
export type ReadReport = ReadOk | ReadMedia | ReadUnsupported | ReadFailure;
|
|
20
|
+
/** The outcome plus the formatter's model-facing content. */
|
|
21
|
+
export type ReadResult = ReadReport & {
|
|
22
|
+
readonly content: readonly ContentPart[];
|
|
23
|
+
};
|
|
24
|
+
export interface ReadOk {
|
|
25
|
+
readonly tool: "read";
|
|
26
|
+
readonly status: "ok";
|
|
27
|
+
readonly request: ReadRequest;
|
|
28
|
+
readonly file: FileInfo;
|
|
29
|
+
readonly classification: ClassificationInfo;
|
|
30
|
+
/** Set when a converter produced the text. */
|
|
31
|
+
readonly conversion: ConversionInfo | null;
|
|
32
|
+
readonly view: ReadView;
|
|
33
|
+
readonly truncation: ReadTruncation;
|
|
34
|
+
readonly continuation: ReadContinuation;
|
|
35
|
+
readonly totals: ReadTotals;
|
|
36
|
+
readonly observation: ReadObservation | null;
|
|
37
|
+
readonly notes: readonly ReadNote[];
|
|
38
|
+
}
|
|
39
|
+
export interface ReadMedia {
|
|
40
|
+
readonly tool: "read";
|
|
41
|
+
readonly status: "media";
|
|
42
|
+
readonly request: ReadRequest;
|
|
43
|
+
readonly file: FileInfo;
|
|
44
|
+
readonly classification: ClassificationInfo;
|
|
45
|
+
readonly conversion: ConversionInfo;
|
|
46
|
+
readonly parts: readonly ContentPart[];
|
|
47
|
+
readonly observation: ReadObservation | null;
|
|
48
|
+
readonly notes: readonly ReadNote[];
|
|
49
|
+
}
|
|
50
|
+
export interface ReadUnsupported {
|
|
51
|
+
readonly tool: "read";
|
|
52
|
+
readonly status: "unsupported";
|
|
53
|
+
/** Open vocabulary: classifier codes, converter refusal codes, "TOO_LARGE". */
|
|
54
|
+
readonly code: string;
|
|
55
|
+
readonly request: ReadRequest;
|
|
56
|
+
readonly file: FileInfo;
|
|
57
|
+
readonly classification: ClassificationInfo;
|
|
58
|
+
readonly notes: readonly ReadNote[];
|
|
59
|
+
}
|
|
60
|
+
export interface ReadFailure {
|
|
61
|
+
readonly tool: "read";
|
|
62
|
+
readonly status: "error";
|
|
63
|
+
readonly error: ReadError;
|
|
64
|
+
readonly request: ReadRequest | null;
|
|
65
|
+
readonly file: FileInfo | null;
|
|
66
|
+
readonly notes: readonly ReadNote[];
|
|
67
|
+
}
|
|
68
|
+
/** The error of a failed read. Same shape as the write and shell errors. */
|
|
69
|
+
export type ReadError = ToolError<ReadErrorCode, ReadPhase>;
|
|
70
|
+
/** UPPER_SNAKE, like every tool's error codes. The error note's code is the kebab-case form. */
|
|
71
|
+
export type ReadErrorCode = "INVALID_INPUT" | "NOT_FOUND" | "NOT_A_FILE" | "DANGEROUS_PATH" | "OUTSIDE_ALLOWED_ROOTS" | "PERMISSION_DENIED" | "DENIED" | "TOO_LARGE" | "CHANGED_DURING_READ" | "ABORTED" | "UNSUPPORTED_BACKEND" | "EXTENSION_FAILED" | "IO_ERROR";
|
|
72
|
+
export interface FileInfo {
|
|
73
|
+
/** The path in the model's input, before any resolver. */
|
|
74
|
+
readonly requestedPath: string;
|
|
75
|
+
readonly resolvedPath: string;
|
|
76
|
+
readonly displayPath: string;
|
|
77
|
+
/** `fs.id` of the backend that served the read. */
|
|
78
|
+
readonly backend: string;
|
|
79
|
+
readonly size: number | null;
|
|
80
|
+
readonly mtimeMs: number | null;
|
|
81
|
+
/** `null` unless `fs.capabilities.identity`. */
|
|
82
|
+
readonly identity: string | null;
|
|
83
|
+
readonly mimeType: string | null;
|
|
84
|
+
/** The requested path when a resolver changed it, else null. */
|
|
85
|
+
readonly resolvedFrom: string | null;
|
|
86
|
+
/** `info.version` from the backend, or null. Kept when `capabilities.identity` is false. */
|
|
87
|
+
readonly version: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface ClassificationInfo {
|
|
90
|
+
readonly kind: "text" | "unsupported" | "directory";
|
|
91
|
+
/** Classifier id, or "fs" for a directory. */
|
|
92
|
+
readonly classifier: string;
|
|
93
|
+
/** Unsupported code from the classifier. null for text and directories. */
|
|
94
|
+
readonly code: string | null;
|
|
95
|
+
readonly mimeType: string | null;
|
|
96
|
+
readonly confidence: Confidence;
|
|
97
|
+
readonly reasons: readonly string[];
|
|
98
|
+
}
|
|
99
|
+
export interface ConversionInfo {
|
|
100
|
+
readonly converter: string;
|
|
101
|
+
/** Media type of the converted output. */
|
|
102
|
+
readonly mimeType: string | null;
|
|
103
|
+
}
|
|
104
|
+
export type Confidence = "high" | "medium" | "low";
|
|
105
|
+
export interface ReadView {
|
|
106
|
+
readonly lines: readonly ReadLine[];
|
|
107
|
+
readonly startLine: number;
|
|
108
|
+
/** startLine - 1 when the view is empty. */
|
|
109
|
+
readonly endLine: number;
|
|
110
|
+
/** UTF-8 bytes of source text in the view, before any gutter. */
|
|
111
|
+
readonly bytes: number;
|
|
112
|
+
readonly partial: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface ReadLine {
|
|
115
|
+
readonly number: number;
|
|
116
|
+
/** Source text, clamped to limits.maxCharsPerLine. No gutter, no marker. */
|
|
117
|
+
readonly text: string;
|
|
118
|
+
readonly clamped: boolean;
|
|
119
|
+
/** Length of the source line in characters when clamped, else null. */
|
|
120
|
+
readonly sourceChars: number | null;
|
|
121
|
+
}
|
|
122
|
+
export type TruncationReason = "lines" | "bytes" | "budget" | "line-length" | "scan-limit";
|
|
123
|
+
export interface ReadTruncation {
|
|
124
|
+
readonly truncated: boolean;
|
|
125
|
+
readonly reasons: readonly TruncationReason[];
|
|
126
|
+
readonly primary: TruncationReason | null;
|
|
127
|
+
}
|
|
128
|
+
export interface ReadContinuation {
|
|
129
|
+
readonly available: boolean;
|
|
130
|
+
/** The exact next canonical call. */
|
|
131
|
+
readonly next: ReadInput | null;
|
|
132
|
+
}
|
|
133
|
+
export interface ReadTotals {
|
|
134
|
+
/** null when the scan limit stopped counting. */
|
|
135
|
+
readonly lines: number | null;
|
|
136
|
+
readonly exact: boolean;
|
|
137
|
+
readonly bytes: number;
|
|
138
|
+
}
|
|
139
|
+
export interface ReadNote extends Note {
|
|
140
|
+
/** A concrete canonical retry. */
|
|
141
|
+
readonly retry?: ReadInput;
|
|
142
|
+
}
|
|
143
|
+
export interface ReadObservation {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
readonly statId: string;
|
|
146
|
+
/** Hash of the source bytes. null when the scan stopped before EOF. */
|
|
147
|
+
readonly contentId: string | null;
|
|
148
|
+
/** Hash of what the model saw. Recomputed after a hook edits the view. */
|
|
149
|
+
readonly viewId: string;
|
|
150
|
+
readonly observedAt: string;
|
|
151
|
+
/** True when offset is 1, nothing was truncated or clamped, and no hook edited the view. */
|
|
152
|
+
readonly wholeFileVisible: boolean;
|
|
153
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state for read observations. A store is a cache: a failed store call
|
|
3
|
+
* never fails a read.
|
|
4
|
+
*/
|
|
5
|
+
export interface ReadStateStore {
|
|
6
|
+
get(key: string): Promise<ReadRecord | null>;
|
|
7
|
+
put(key: string, record: ReadRecord): Promise<void>;
|
|
8
|
+
delete(key: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What the read tool stores after a read with an observation, and what a write
|
|
12
|
+
* tool stores after a commit. Never holds `host`.
|
|
13
|
+
*/
|
|
14
|
+
export interface ReadRecord {
|
|
15
|
+
readonly schema: 2;
|
|
16
|
+
/** "write" when a write tool stored it after a commit. */
|
|
17
|
+
readonly origin: "read" | "write";
|
|
18
|
+
readonly observationId: string;
|
|
19
|
+
readonly resolvedPath: string;
|
|
20
|
+
readonly identity: string | null;
|
|
21
|
+
/** The backend change token. null when the backend gave none. */
|
|
22
|
+
readonly version: string | null;
|
|
23
|
+
/** Digest.id that made contentId and viewId. A record with another digest counts as absent. */
|
|
24
|
+
readonly digest: string;
|
|
25
|
+
readonly contentId: string | null;
|
|
26
|
+
/** For a write record: equal to contentId. */
|
|
27
|
+
readonly viewId: string;
|
|
28
|
+
readonly observedAt: string;
|
|
29
|
+
readonly wholeFileVisible: boolean;
|
|
30
|
+
readonly totalsExact: boolean;
|
|
31
|
+
/** The read range. null for a write record. */
|
|
32
|
+
readonly request: {
|
|
33
|
+
readonly offset: number;
|
|
34
|
+
readonly limit: number;
|
|
35
|
+
} | null;
|
|
36
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DirectoryConverter } from "../contract/extensions.ts";
|
|
2
|
+
export interface DirectoryListingOptions {
|
|
3
|
+
/** Add "/" after directory names. Default false. */
|
|
4
|
+
trailingSlash?: boolean;
|
|
5
|
+
/** Default "name". "type-then-name" puts directories first, then files, then others. */
|
|
6
|
+
sort?: "name" | "type-then-name";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Lists a directory, one entry per line, so offset, limit, and continuation
|
|
10
|
+
* page over entries. The listing holds at most limits.maxDirectoryEntries
|
|
11
|
+
* entries; a cut listing has a note.
|
|
12
|
+
*/
|
|
13
|
+
export declare function directoryListing(options?: DirectoryListingOptions): DirectoryConverter<unknown>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const TYPE_ORDER = {
|
|
2
|
+
directory: 0,
|
|
3
|
+
file: 1,
|
|
4
|
+
other: 2,
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Lists a directory, one entry per line, so offset, limit, and continuation
|
|
8
|
+
* page over entries. The listing holds at most limits.maxDirectoryEntries
|
|
9
|
+
* entries; a cut listing has a note.
|
|
10
|
+
*/
|
|
11
|
+
export function directoryListing(options = {}) {
|
|
12
|
+
const trailingSlash = options.trailingSlash ?? false;
|
|
13
|
+
const sort = options.sort ?? "name";
|
|
14
|
+
if (typeof trailingSlash !== "boolean") {
|
|
15
|
+
throw new TypeError("directoryListing trailingSlash must be a boolean");
|
|
16
|
+
}
|
|
17
|
+
if (sort !== "name" && sort !== "type-then-name") {
|
|
18
|
+
throw new TypeError('directoryListing sort must be "name" or "type-then-name"');
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
id: "directory-listing",
|
|
22
|
+
target: "directory",
|
|
23
|
+
async convert(input, ctx) {
|
|
24
|
+
const path = input.target?.displayPath ?? input.path;
|
|
25
|
+
const listed = await input.list();
|
|
26
|
+
// The read tool reports a failed listing itself; this refusal is never shown.
|
|
27
|
+
if (!listed.ok) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "refuse",
|
|
30
|
+
code: "LIST_FAILED",
|
|
31
|
+
note: { code: "list-failed", severity: "warning", message: `${path} was not listed.` },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const entries = [...listed.entries].sort(sort === "name" ? byName : byTypeThenName);
|
|
35
|
+
const notes = [];
|
|
36
|
+
if (listed.truncated) {
|
|
37
|
+
const limit = ctx.limits.maxDirectoryEntries;
|
|
38
|
+
notes.push({
|
|
39
|
+
code: "directory-truncated",
|
|
40
|
+
severity: "warning",
|
|
41
|
+
message: `The listing of ${path} stopped at ${limit} entries; the directory has more.`,
|
|
42
|
+
data: { maxDirectoryEntries: limit },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else if (entries.length === 0) {
|
|
46
|
+
notes.push({ code: "empty-directory", severity: "info", message: `${path} is empty.` });
|
|
47
|
+
}
|
|
48
|
+
const lines = entries.map((entry) => {
|
|
49
|
+
const name = /[\r\n]/u.test(entry.name) ? JSON.stringify(entry.name) : entry.name;
|
|
50
|
+
return trailingSlash && entry.type === "directory" ? `${name}/` : name;
|
|
51
|
+
});
|
|
52
|
+
return { kind: "text", text: lines.join("\n"), mimeType: "text/plain", notes };
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function byName(a, b) {
|
|
57
|
+
if (a.name === b.name)
|
|
58
|
+
return 0;
|
|
59
|
+
return a.name < b.name ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
function byTypeThenName(a, b) {
|
|
62
|
+
return TYPE_ORDER[a.type] - TYPE_ORDER[b.type] || byName(a, b);
|
|
63
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileConverter, ReadHookContext } from "../contract/extensions.ts";
|
|
2
|
+
export interface ImageConverterOptions<THost = unknown> {
|
|
3
|
+
/** Resize or re-encode before the image goes to the model. The media type stays the same. */
|
|
4
|
+
transform?: (bytes: Uint8Array, mediaType: string, ctx: ReadHookContext<THost>) => Promise<Uint8Array>;
|
|
5
|
+
}
|
|
6
|
+
/** Converts an image (classification code "IMAGE") into one media part, capped by limits.maxMediaBytes. */
|
|
7
|
+
export declare function imageConverter<THost = unknown>(options?: ImageConverterOptions<THost>): FileConverter<THost>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { posixPaths } from "@better-fs-tools/fs";
|
|
2
|
+
import { collect, isUnsupportedWithCode } from "./shared.js";
|
|
3
|
+
/** Converts an image (classification code "IMAGE") into one media part, capped by limits.maxMediaBytes. */
|
|
4
|
+
export function imageConverter(options = {}) {
|
|
5
|
+
const { transform } = options;
|
|
6
|
+
if (transform !== undefined && typeof transform !== "function") {
|
|
7
|
+
throw new TypeError("imageConverter transform must be a function");
|
|
8
|
+
}
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
id: "image",
|
|
11
|
+
target: "file",
|
|
12
|
+
accepts: (match) => isUnsupportedWithCode(match.classification, "IMAGE"),
|
|
13
|
+
async convert(input, ctx) {
|
|
14
|
+
const mediaType = input.classification.mimeType ?? input.info.mimeType ?? "application/octet-stream";
|
|
15
|
+
const bytes = await collect(input.bytes());
|
|
16
|
+
const data = transform === undefined ? bytes : await transform(bytes, mediaType, ctx);
|
|
17
|
+
return {
|
|
18
|
+
kind: "media",
|
|
19
|
+
parts: [
|
|
20
|
+
{ type: "media", mediaType, data, name: posixPaths.basename(input.info.displayPath) },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { directoryListing } from "./directory-listing.ts";
|
|
2
|
+
export type { DirectoryListingOptions } from "./directory-listing.ts";
|
|
3
|
+
export { imageConverter } from "./image.ts";
|
|
4
|
+
export type { ImageConverterOptions } from "./image.ts";
|
|
5
|
+
export { notebookConverter } from "./notebook.ts";
|
|
6
|
+
export type { NotebookConverterOptions } from "./notebook.ts";
|
|
7
|
+
export { textConverter } from "./text.ts";
|
|
8
|
+
export type { TextConverterOptions } from "./text.ts";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FileConverter } from "../contract/extensions.ts";
|
|
2
|
+
export interface NotebookConverterOptions {
|
|
3
|
+
/** Render cell outputs after each code cell. Default true. */
|
|
4
|
+
outputs?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Accepts classification code "NOTEBOOK". Renders cells as text: a
|
|
8
|
+
* `[cell N: type]` line, the source, and for code cells the text outputs.
|
|
9
|
+
* Rich outputs are named, not shown. A file that is not notebook JSON is
|
|
10
|
+
* refused with code "INVALID_NOTEBOOK".
|
|
11
|
+
*/
|
|
12
|
+
export declare function notebookConverter(options?: NotebookConverterOptions): FileConverter<unknown>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isRecord } from "../core/input.js";
|
|
2
|
+
import { collect, isUnsupportedWithCode } from "./shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Accepts classification code "NOTEBOOK". Renders cells as text: a
|
|
5
|
+
* `[cell N: type]` line, the source, and for code cells the text outputs.
|
|
6
|
+
* Rich outputs are named, not shown. A file that is not notebook JSON is
|
|
7
|
+
* refused with code "INVALID_NOTEBOOK".
|
|
8
|
+
*/
|
|
9
|
+
export function notebookConverter(options = {}) {
|
|
10
|
+
const outputs = options.outputs ?? true;
|
|
11
|
+
if (typeof outputs !== "boolean")
|
|
12
|
+
throw new TypeError("notebookConverter outputs must be a boolean");
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
id: "notebook",
|
|
15
|
+
target: "file",
|
|
16
|
+
accepts: (match) => isUnsupportedWithCode(match.classification, "NOTEBOOK"),
|
|
17
|
+
async convert(input) {
|
|
18
|
+
const cells = parseCells(await collect(input.bytes()));
|
|
19
|
+
if (cells === null) {
|
|
20
|
+
return { kind: "refuse", code: "INVALID_NOTEBOOK", note: invalid(input.info.displayPath) };
|
|
21
|
+
}
|
|
22
|
+
const blocks = cells.map((cell, index) => renderCell(cell, index + 1, outputs));
|
|
23
|
+
return { kind: "text", text: blocks.join("\n\n"), mimeType: "text/plain" };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function parseCells(bytes) {
|
|
28
|
+
let notebook;
|
|
29
|
+
try {
|
|
30
|
+
notebook = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(notebook) || !Array.isArray(notebook.cells))
|
|
36
|
+
return null;
|
|
37
|
+
const cells = [];
|
|
38
|
+
for (const cell of notebook.cells) {
|
|
39
|
+
if (!isRecord(cell) || typeof cell.cell_type !== "string")
|
|
40
|
+
return null;
|
|
41
|
+
const source = jupyterText(cell.source);
|
|
42
|
+
if (source === null)
|
|
43
|
+
return null;
|
|
44
|
+
cells.push({
|
|
45
|
+
type: cell.cell_type,
|
|
46
|
+
source,
|
|
47
|
+
outputs: Array.isArray(cell.outputs) ? cell.outputs : [],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return cells;
|
|
51
|
+
}
|
|
52
|
+
function renderCell(cell, number, outputs) {
|
|
53
|
+
const lines = [`[cell ${number}: ${cell.type}]`];
|
|
54
|
+
if (cell.source !== "")
|
|
55
|
+
lines.push(trimEnd(cell.source));
|
|
56
|
+
if (outputs && cell.type === "code") {
|
|
57
|
+
for (const output of cell.outputs) {
|
|
58
|
+
const text = renderOutput(output);
|
|
59
|
+
if (text !== null)
|
|
60
|
+
lines.push("[output]", trimEnd(text));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
function renderOutput(output) {
|
|
66
|
+
if (!isRecord(output))
|
|
67
|
+
return null;
|
|
68
|
+
switch (output.output_type) {
|
|
69
|
+
case "stream":
|
|
70
|
+
return jupyterText(output.text);
|
|
71
|
+
case "execute_result":
|
|
72
|
+
case "display_data": {
|
|
73
|
+
if (!isRecord(output.data))
|
|
74
|
+
return null;
|
|
75
|
+
const plain = output.data["text/plain"] === undefined ? null : jupyterText(output.data["text/plain"]);
|
|
76
|
+
if (plain !== null)
|
|
77
|
+
return plain;
|
|
78
|
+
const types = Object.keys(output.data);
|
|
79
|
+
return types.length === 0 ? null : `[${types.join(", ")} output not shown]`;
|
|
80
|
+
}
|
|
81
|
+
case "error":
|
|
82
|
+
return `${String(output.ename ?? "Error")}: ${String(output.evalue ?? "")}`;
|
|
83
|
+
default:
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function jupyterText(value) {
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return "";
|
|
90
|
+
if (typeof value === "string")
|
|
91
|
+
return value;
|
|
92
|
+
if (Array.isArray(value) && value.every((part) => typeof part === "string")) {
|
|
93
|
+
return value.join("");
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function trimEnd(text) {
|
|
98
|
+
return text.replace(/(?:\r?\n)+$/u, "");
|
|
99
|
+
}
|
|
100
|
+
function invalid(path) {
|
|
101
|
+
return {
|
|
102
|
+
code: "invalid-notebook",
|
|
103
|
+
severity: "warning",
|
|
104
|
+
message: `${path} looks like a Jupyter notebook but is not valid notebook JSON.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function collect(source) {
|
|
2
|
+
const chunks = [];
|
|
3
|
+
let total = 0;
|
|
4
|
+
for await (const chunk of source) {
|
|
5
|
+
chunks.push(chunk);
|
|
6
|
+
total += chunk.byteLength;
|
|
7
|
+
}
|
|
8
|
+
const bytes = new Uint8Array(total);
|
|
9
|
+
let offset = 0;
|
|
10
|
+
for (const chunk of chunks) {
|
|
11
|
+
bytes.set(chunk, offset);
|
|
12
|
+
offset += chunk.byteLength;
|
|
13
|
+
}
|
|
14
|
+
return bytes;
|
|
15
|
+
}
|
|
16
|
+
export function isUnsupportedWithCode(classification, code) {
|
|
17
|
+
return classification.kind === "unsupported" && classification.code === code;
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConverterMatch, FileConverter, ReadHookContext } from "../contract/extensions.ts";
|
|
2
|
+
export interface TextConverterOptions<THost = unknown> {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Sync. The only place the converter may decline. */
|
|
5
|
+
accepts: (match: ConverterMatch) => boolean;
|
|
6
|
+
/** Media type of the text. */
|
|
7
|
+
mimeType: string | null;
|
|
8
|
+
/** Turns the source bytes into text. The text may arrive in chunks. */
|
|
9
|
+
run: (source: AsyncIterable<Uint8Array>, ctx: ReadHookContext<THost>) => AsyncIterable<string>;
|
|
10
|
+
}
|
|
11
|
+
/** Builds a text converter from a stream function, for example a pdftotext process in the host. */
|
|
12
|
+
export declare function textConverter<THost = unknown>(options: TextConverterOptions<THost>): FileConverter<THost>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Builds a text converter from a stream function, for example a pdftotext process in the host. */
|
|
2
|
+
export function textConverter(options) {
|
|
3
|
+
if (options === null || typeof options !== "object") {
|
|
4
|
+
throw new TypeError("textConverter options must be an object");
|
|
5
|
+
}
|
|
6
|
+
const { id, accepts, mimeType, run } = options;
|
|
7
|
+
if (typeof id !== "string" || id === "")
|
|
8
|
+
throw new TypeError("textConverter id must be a string");
|
|
9
|
+
if (typeof accepts !== "function")
|
|
10
|
+
throw new TypeError("textConverter accepts must be a function");
|
|
11
|
+
if (typeof run !== "function")
|
|
12
|
+
throw new TypeError("textConverter run must be a function");
|
|
13
|
+
if (mimeType !== null && typeof mimeType !== "string") {
|
|
14
|
+
throw new TypeError("textConverter mimeType must be a string or null");
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
id,
|
|
18
|
+
target: "file",
|
|
19
|
+
accepts: (match) => accepts(match),
|
|
20
|
+
async convert(input, ctx) {
|
|
21
|
+
return { kind: "text", text: run(input.bytes(), ctx), mimeType };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ListOutcome } from "@better-fs-tools/fs";
|
|
2
|
+
import type { ReadAuthorizer } from "../contract/extensions.ts";
|
|
3
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
4
|
+
import type { FileInfo } from "../contract/result.ts";
|
|
5
|
+
import type { CallScope } from "./call-scope.ts";
|
|
6
|
+
export declare function authorizeRead<THost>(authorizer: ReadAuthorizer<THost> | null, request: ReadRequest, file: FileInfo, scope: CallScope<THost>): Promise<void>;
|
|
7
|
+
/** null when the listing may go ahead. An authorizer failure is held on the scope, not thrown. */
|
|
8
|
+
export declare function authorizeList<THost>(authorizer: ReadAuthorizer<THost> | null, request: ReadRequest, dir: string, display: string, scope: CallScope<THost>): Promise<ListOutcome | null>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AbortReadError, raceAbort } from "./cursor.js";
|
|
2
|
+
import { extensionId } from "./extension-error.js";
|
|
3
|
+
import { isRecord } from "./input.js";
|
|
4
|
+
import { ReadStop, denied, isNote } from "./outcomes.js";
|
|
5
|
+
export async function authorizeRead(authorizer, request, file, scope) {
|
|
6
|
+
if (authorizer === null)
|
|
7
|
+
return;
|
|
8
|
+
scope.enter("authorize");
|
|
9
|
+
const target = Object.freeze({
|
|
10
|
+
action: "read",
|
|
11
|
+
requestedPath: request.path,
|
|
12
|
+
resolvedPath: file.resolvedPath,
|
|
13
|
+
displayPath: file.displayPath,
|
|
14
|
+
size: file.size,
|
|
15
|
+
mtimeMs: file.mtimeMs,
|
|
16
|
+
});
|
|
17
|
+
const result = await runAuthorizer(authorizer, target, scope);
|
|
18
|
+
if (!result.allow)
|
|
19
|
+
throw new ReadStop(denied(scope.deps.messages, request, result.note));
|
|
20
|
+
}
|
|
21
|
+
/** null when the listing may go ahead. An authorizer failure is held on the scope, not thrown. */
|
|
22
|
+
export async function authorizeList(authorizer, request, dir, display, scope) {
|
|
23
|
+
if (authorizer === null)
|
|
24
|
+
return null;
|
|
25
|
+
const target = Object.freeze({
|
|
26
|
+
action: "list",
|
|
27
|
+
requestedPath: request.path,
|
|
28
|
+
resolvedPath: dir,
|
|
29
|
+
displayPath: display,
|
|
30
|
+
size: null,
|
|
31
|
+
mtimeMs: null,
|
|
32
|
+
});
|
|
33
|
+
try {
|
|
34
|
+
const result = await runAuthorizer(authorizer, target, scope);
|
|
35
|
+
if (result.allow)
|
|
36
|
+
return null;
|
|
37
|
+
return { ok: false, error: { reason: "denied", detail: "refused by the authorizer" } };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof AbortReadError)
|
|
41
|
+
return { ok: false, error: { reason: "aborted" } };
|
|
42
|
+
if (!(error instanceof ReadStop))
|
|
43
|
+
throw error;
|
|
44
|
+
scope.hold(error);
|
|
45
|
+
return { ok: false, error: { reason: "io", detail: "the authorizer failed" } };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function runAuthorizer(authorizer, target, scope) {
|
|
49
|
+
let decision;
|
|
50
|
+
try {
|
|
51
|
+
decision = await raceAbort(() => authorizer.authorize(target, scope.hookContext()), scope.signal);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
scope.checkAbort();
|
|
55
|
+
throw scope.extensionFailure("authorize", extensionId(authorizer, error));
|
|
56
|
+
}
|
|
57
|
+
scope.checkAbort();
|
|
58
|
+
const malformed = () => scope.extensionFailure("authorize", extensionId(authorizer));
|
|
59
|
+
if (!isRecord(decision))
|
|
60
|
+
throw malformed();
|
|
61
|
+
if (decision.allow === true) {
|
|
62
|
+
const { notes } = decision;
|
|
63
|
+
if (notes === undefined)
|
|
64
|
+
return { allow: true };
|
|
65
|
+
if (!Array.isArray(notes) || !notes.every(isNote))
|
|
66
|
+
throw malformed();
|
|
67
|
+
scope.allowNotes.push(...notes);
|
|
68
|
+
return { allow: true };
|
|
69
|
+
}
|
|
70
|
+
if (decision.allow !== false)
|
|
71
|
+
throw malformed();
|
|
72
|
+
const { note } = decision;
|
|
73
|
+
if (note !== undefined && !isNote(note))
|
|
74
|
+
throw malformed();
|
|
75
|
+
return { allow: false, note: note ?? null };
|
|
76
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CallScope } from "./call-scope.ts";
|
|
2
|
+
export interface ScanBudget {
|
|
3
|
+
readonly max: number;
|
|
4
|
+
measure(text: string): number;
|
|
5
|
+
}
|
|
6
|
+
/** The host view budget, with a throwing or invalid measure failing the read as EXTENSION_FAILED. */
|
|
7
|
+
export declare function scanBudget<THost>(scope: CallScope<THost>): ScanBudget | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { extensionId } from "./extension-error.js";
|
|
2
|
+
/** The host view budget, with a throwing or invalid measure failing the read as EXTENSION_FAILED. */
|
|
3
|
+
export function scanBudget(scope) {
|
|
4
|
+
const { budget } = scope.deps;
|
|
5
|
+
if (budget === null)
|
|
6
|
+
return null;
|
|
7
|
+
return {
|
|
8
|
+
max: budget.max,
|
|
9
|
+
measure(text) {
|
|
10
|
+
let cost;
|
|
11
|
+
try {
|
|
12
|
+
cost = budget.measure(text);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
throw scope.extensionFailure("budget", extensionId(budget, error));
|
|
16
|
+
}
|
|
17
|
+
if (typeof cost !== "number" || !Number.isFinite(cost) || cost < 0) {
|
|
18
|
+
throw scope.extensionFailure("budget", extensionId(budget));
|
|
19
|
+
}
|
|
20
|
+
return cost;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FileSystem, ListOutcome } from "@better-fs-tools/fs";
|
|
2
|
+
import type { ReadContext } from "../contract/context.ts";
|
|
3
|
+
import type { ReadDependencies } from "../contract/deps.ts";
|
|
4
|
+
import type { ReadHookContext } from "../contract/extensions.ts";
|
|
5
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
6
|
+
import type { ReadPhase } from "../contract/messages.ts";
|
|
7
|
+
import type { ReadNote } from "../contract/result.ts";
|
|
8
|
+
import type { ReadStateStore } from "../contract/state.ts";
|
|
9
|
+
import { ReadStop } from "./outcomes.ts";
|
|
10
|
+
/** `resolver`: the resolver's `ctx.list`. `open`: suggestions on a miss, or a directory converter. */
|
|
11
|
+
export type ListingSlot = "resolver" | "open";
|
|
12
|
+
export declare class CallScope<THost> {
|
|
13
|
+
readonly deps: ReadDependencies<THost>;
|
|
14
|
+
private readonly request;
|
|
15
|
+
readonly call: ReadContext<THost>;
|
|
16
|
+
phase: ReadPhase;
|
|
17
|
+
readonly allowNotes: ReadNote[];
|
|
18
|
+
private heldFailure;
|
|
19
|
+
private resolvedFs;
|
|
20
|
+
private resolvedState;
|
|
21
|
+
private hook;
|
|
22
|
+
private readonly listed;
|
|
23
|
+
constructor(deps: ReadDependencies<THost>, request: ReadRequest, call: ReadContext<THost>);
|
|
24
|
+
get signal(): AbortSignal | undefined;
|
|
25
|
+
enter(phase: ReadPhase): void;
|
|
26
|
+
checkAbort(): void;
|
|
27
|
+
fileSystem(): FileSystem;
|
|
28
|
+
stateStore(): ReadStateStore | null;
|
|
29
|
+
hookContext(): ReadHookContext<THost>;
|
|
30
|
+
/** One bounded, authorized listing per slot. Never throws: every failure is an error outcome. */
|
|
31
|
+
list(slot: ListingSlot, dir: string, displayPath?: string): Promise<ListOutcome>;
|
|
32
|
+
/** Keeps a listing failure out of reach of host code that might catch and ignore it. */
|
|
33
|
+
hold(stop: ReadStop): void;
|
|
34
|
+
throwHeld(): void;
|
|
35
|
+
extensionFailure(extension: string, id?: string | null): ReadStop;
|
|
36
|
+
}
|