@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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +807 -6
  3. package/dist/authorize/ask-user.d.ts +7 -0
  4. package/dist/authorize/ask-user.js +25 -0
  5. package/dist/authorize/authorizers.d.ts +7 -0
  6. package/dist/authorize/authorizers.js +34 -0
  7. package/dist/authorize/deny-paths.d.ts +7 -0
  8. package/dist/authorize/deny-paths.js +25 -0
  9. package/dist/authorize/glob.d.ts +7 -0
  10. package/dist/authorize/glob.js +46 -0
  11. package/dist/authorize/index.d.ts +5 -0
  12. package/dist/authorize/index.js +5 -0
  13. package/dist/authorize/shared.d.ts +9 -0
  14. package/dist/authorize/shared.js +12 -0
  15. package/dist/authorize/size-ceiling.d.ts +10 -0
  16. package/dist/authorize/size-ceiling.js +34 -0
  17. package/dist/budget/chars-per-token.d.ts +9 -0
  18. package/dist/budget/chars-per-token.js +15 -0
  19. package/dist/budget/index.d.ts +2 -0
  20. package/dist/budget/index.js +1 -0
  21. package/dist/classifiers/binary.d.ts +5 -0
  22. package/dist/classifiers/binary.js +13 -0
  23. package/dist/classifiers/extension.d.ts +17 -0
  24. package/dist/classifiers/extension.js +45 -0
  25. package/dist/classifiers/image.d.ts +5 -0
  26. package/dist/classifiers/image.js +22 -0
  27. package/dist/classifiers/index.d.ts +16 -0
  28. package/dist/classifiers/index.js +31 -0
  29. package/dist/classifiers/notebook.d.ts +5 -0
  30. package/dist/classifiers/notebook.js +14 -0
  31. package/dist/classifiers/office.d.ts +5 -0
  32. package/dist/classifiers/office.js +30 -0
  33. package/dist/classifiers/pdf.d.ts +4 -0
  34. package/dist/classifiers/pdf.js +8 -0
  35. package/dist/classifiers/shared.d.ts +17 -0
  36. package/dist/classifiers/shared.js +57 -0
  37. package/dist/classifiers/utf8.d.ts +8 -0
  38. package/dist/classifiers/utf8.js +61 -0
  39. package/dist/contract/base.d.ts +126 -0
  40. package/dist/contract/base.js +0 -0
  41. package/dist/contract/classify.d.ts +50 -0
  42. package/dist/contract/classify.js +0 -0
  43. package/dist/contract/context.d.ts +13 -0
  44. package/dist/contract/context.js +0 -0
  45. package/dist/contract/deps.d.ts +60 -0
  46. package/dist/contract/deps.js +0 -0
  47. package/dist/contract/digest.d.ts +13 -0
  48. package/dist/contract/digest.js +0 -0
  49. package/dist/contract/extensions.d.ts +115 -0
  50. package/dist/contract/extensions.js +0 -0
  51. package/dist/contract/format.d.ts +16 -0
  52. package/dist/contract/format.js +0 -0
  53. package/dist/contract/input.d.ts +16 -0
  54. package/dist/contract/input.js +0 -0
  55. package/dist/contract/json.d.ts +6 -0
  56. package/dist/contract/json.js +0 -0
  57. package/dist/contract/limits.d.ts +20 -0
  58. package/dist/contract/limits.js +0 -0
  59. package/dist/contract/messages.d.ts +110 -0
  60. package/dist/contract/messages.js +0 -0
  61. package/dist/contract/result.d.ts +153 -0
  62. package/dist/contract/result.js +0 -0
  63. package/dist/contract/state.d.ts +36 -0
  64. package/dist/contract/state.js +0 -0
  65. package/dist/converters/directory-listing.d.ts +13 -0
  66. package/dist/converters/directory-listing.js +63 -0
  67. package/dist/converters/image.d.ts +7 -0
  68. package/dist/converters/image.js +25 -0
  69. package/dist/converters/index.d.ts +8 -0
  70. package/dist/converters/index.js +4 -0
  71. package/dist/converters/notebook.d.ts +12 -0
  72. package/dist/converters/notebook.js +106 -0
  73. package/dist/converters/shared.d.ts +5 -0
  74. package/dist/converters/shared.js +18 -0
  75. package/dist/converters/text.d.ts +12 -0
  76. package/dist/converters/text.js +24 -0
  77. package/dist/core/authorize.d.ts +8 -0
  78. package/dist/core/authorize.js +76 -0
  79. package/dist/core/budget.d.ts +7 -0
  80. package/dist/core/budget.js +23 -0
  81. package/dist/core/call-scope.d.ts +36 -0
  82. package/dist/core/call-scope.js +112 -0
  83. package/dist/core/classify.d.ts +9 -0
  84. package/dist/core/classify.js +27 -0
  85. package/dist/core/convert-source.d.ts +29 -0
  86. package/dist/core/convert-source.js +85 -0
  87. package/dist/core/convert.d.ts +23 -0
  88. package/dist/core/convert.js +132 -0
  89. package/dist/core/converted.d.ts +18 -0
  90. package/dist/core/converted.js +111 -0
  91. package/dist/core/create-read-tool.d.ts +7 -0
  92. package/dist/core/create-read-tool.js +18 -0
  93. package/dist/core/cursor.d.ts +14 -0
  94. package/dist/core/cursor.js +93 -0
  95. package/dist/core/deps.d.ts +6 -0
  96. package/dist/core/deps.js +121 -0
  97. package/dist/core/directory.d.ts +16 -0
  98. package/dist/core/directory.js +113 -0
  99. package/dist/core/extension-error.d.ts +9 -0
  100. package/dist/core/extension-error.js +18 -0
  101. package/dist/core/format.d.ts +8 -0
  102. package/dist/core/format.js +29 -0
  103. package/dist/core/hooks.d.ts +3 -0
  104. package/dist/core/hooks.js +140 -0
  105. package/dist/core/input.d.ts +10 -0
  106. package/dist/core/input.js +45 -0
  107. package/dist/core/limits.d.ts +9 -0
  108. package/dist/core/limits.js +43 -0
  109. package/dist/core/messages.d.ts +8 -0
  110. package/dist/core/messages.js +78 -0
  111. package/dist/core/notes.d.ts +14 -0
  112. package/dist/core/notes.js +94 -0
  113. package/dist/core/observation.d.ts +11 -0
  114. package/dist/core/observation.js +51 -0
  115. package/dist/core/open.d.ts +16 -0
  116. package/dist/core/open.js +33 -0
  117. package/dist/core/outcomes.d.ts +27 -0
  118. package/dist/core/outcomes.js +172 -0
  119. package/dist/core/pipeline.d.ts +4 -0
  120. package/dist/core/pipeline.js +175 -0
  121. package/dist/core/record.d.ts +3 -0
  122. package/dist/core/record.js +37 -0
  123. package/dist/core/resolve.d.ts +15 -0
  124. package/dist/core/resolve.js +43 -0
  125. package/dist/core/same.d.ts +2 -0
  126. package/dist/core/same.js +22 -0
  127. package/dist/core/sample.d.ts +6 -0
  128. package/dist/core/sample.js +24 -0
  129. package/dist/core/scan.d.ts +26 -0
  130. package/dist/core/scan.js +95 -0
  131. package/dist/core/scanner.d.ts +29 -0
  132. package/dist/core/scanner.js +124 -0
  133. package/dist/core/suggest.d.ts +6 -0
  134. package/dist/core/suggest.js +41 -0
  135. package/dist/core/text-outcome.d.ts +19 -0
  136. package/dist/core/text-outcome.js +72 -0
  137. package/dist/core/verify.d.ts +7 -0
  138. package/dist/core/verify.js +15 -0
  139. package/dist/digest/sha256.d.ts +7 -0
  140. package/dist/digest/sha256.js +131 -0
  141. package/dist/formats/deep-agents.d.ts +7 -0
  142. package/dist/formats/deep-agents.js +23 -0
  143. package/dist/formats/hashline.d.ts +6 -0
  144. package/dist/formats/hashline.js +16 -0
  145. package/dist/formats/hermes.d.ts +9 -0
  146. package/dist/formats/hermes.js +62 -0
  147. package/dist/formats/index.d.ts +4 -0
  148. package/dist/formats/index.js +4 -0
  149. package/dist/formats/opencode.d.ts +7 -0
  150. package/dist/formats/opencode.js +35 -0
  151. package/dist/formats/shared.d.ts +4 -0
  152. package/dist/formats/shared.js +9 -0
  153. package/dist/formatters/eof.d.ts +7 -0
  154. package/dist/formatters/eof.js +20 -0
  155. package/dist/formatters/file-hash.d.ts +7 -0
  156. package/dist/formatters/file-hash.js +13 -0
  157. package/dist/formatters/hashline.d.ts +11 -0
  158. package/dist/formatters/hashline.js +18 -0
  159. package/dist/formatters/index.d.ts +10 -0
  160. package/dist/formatters/index.js +7 -0
  161. package/dist/formatters/json.d.ts +17 -0
  162. package/dist/formatters/json.js +36 -0
  163. package/dist/formatters/layout.d.ts +15 -0
  164. package/dist/formatters/layout.js +33 -0
  165. package/dist/formatters/line-number.d.ts +11 -0
  166. package/dist/formatters/line-number.js +23 -0
  167. package/dist/formatters/plain.d.ts +7 -0
  168. package/dist/formatters/plain.js +14 -0
  169. package/dist/hooks/index.d.ts +4 -0
  170. package/dist/hooks/index.js +2 -0
  171. package/dist/hooks/redact.d.ts +14 -0
  172. package/dist/hooks/redact.js +53 -0
  173. package/dist/hooks/repeat-read-guard.d.ts +13 -0
  174. package/dist/hooks/repeat-read-guard.js +49 -0
  175. package/dist/index.d.ts +35 -0
  176. package/dist/index.js +15 -0
  177. package/dist/resolve/expand-home.d.ts +5 -0
  178. package/dist/resolve/expand-home.js +17 -0
  179. package/dist/resolve/index.d.ts +5 -0
  180. package/dist/resolve/index.js +5 -0
  181. package/dist/resolve/path-resolvers.d.ts +7 -0
  182. package/dist/resolve/path-resolvers.js +38 -0
  183. package/dist/resolve/reanchor-leading-slash.d.ts +9 -0
  184. package/dist/resolve/reanchor-leading-slash.js +23 -0
  185. package/dist/resolve/strip-prefixes.d.ts +6 -0
  186. package/dist/resolve/strip-prefixes.js +30 -0
  187. package/dist/resolve/unicode-repair.d.ts +9 -0
  188. package/dist/resolve/unicode-repair.js +38 -0
  189. package/dist/signature/contract.d.ts +18 -0
  190. package/dist/signature/contract.js +0 -0
  191. package/dist/signature/index.d.ts +4 -0
  192. package/dist/signature/index.js +3 -0
  193. package/dist/signature/line-range.d.ts +7 -0
  194. package/dist/signature/line-range.js +59 -0
  195. package/dist/signature/messages.d.ts +4 -0
  196. package/dist/signature/messages.js +9 -0
  197. package/dist/signature/offset-limit.d.ts +13 -0
  198. package/dist/signature/offset-limit.js +59 -0
  199. package/dist/signature/schema.d.ts +10 -0
  200. package/dist/signature/schema.js +80 -0
  201. package/dist/state/index.d.ts +15 -0
  202. package/dist/state/index.js +77 -0
  203. package/dist/suggest/canonicalize.d.ts +6 -0
  204. package/dist/suggest/canonicalize.js +12 -0
  205. package/dist/suggest/default-suggest.d.ts +3 -0
  206. package/dist/suggest/default-suggest.js +12 -0
  207. package/dist/suggest/index.d.ts +3 -0
  208. package/dist/suggest/index.js +3 -0
  209. package/dist/suggest/suggest-file-names.d.ts +2 -0
  210. package/dist/suggest/suggest-file-names.js +46 -0
  211. package/docs/architecture.md +250 -0
  212. package/docs/result-schema.md +222 -0
  213. package/package.json +51 -2
@@ -0,0 +1,61 @@
1
+ import { startsWith, unsupported } from "./shared.js";
2
+ /**
3
+ * Accepts UTF-8 text and refuses invalid UTF-8 with UNKNOWN_ENCODING, also when
4
+ * the invalid bytes come after the sample. The only built-in that returns text.
5
+ */
6
+ export function utf8Classifier(options = {}) {
7
+ const refuse = (sample) => unsupported("UNKNOWN_ENCODING", null, ["invalid-utf8"], sample, options.note, "a byte or encoding-aware tool");
8
+ const classify = (sample) => {
9
+ const decoded = decodeStrict(sample.bytes);
10
+ if (decoded.kind === "invalid" || (decoded.kind === "incomplete" && sample.complete)) {
11
+ return refuse(sample);
12
+ }
13
+ const text = decoded.text.replace(/^/u, "");
14
+ const reasons = ["utf8"];
15
+ const hasBom = startsWith(sample.bytes, [0xef, 0xbb, 0xbf]);
16
+ if (hasBom)
17
+ reasons.push("utf8-bom");
18
+ if (decoded.kind === "incomplete")
19
+ reasons.push("incomplete-sample-suffix");
20
+ const svg = looksLikeSvg(text);
21
+ if (svg)
22
+ reasons.push("svg-text");
23
+ const notes = hasBom
24
+ ? [
25
+ {
26
+ code: "utf8-bom",
27
+ severity: "info",
28
+ message: "The file begins with a UTF-8 byte order mark, which is included in line 1.",
29
+ },
30
+ ]
31
+ : [];
32
+ return {
33
+ kind: "text",
34
+ mimeType: svg ? "image/svg+xml" : (sample.mimeType ?? "text/plain"),
35
+ confidence: decoded.kind === "incomplete" ? "medium" : "high",
36
+ reasons,
37
+ ...(notes.length > 0 ? { notes } : {}),
38
+ };
39
+ };
40
+ return Object.freeze({ id: "utf8", classify, encodingFailure: refuse });
41
+ }
42
+ function decodeStrict(bytes) {
43
+ const decoder = new TextDecoder("utf-8", { fatal: true });
44
+ let text;
45
+ try {
46
+ text = decoder.decode(bytes, { stream: true });
47
+ }
48
+ catch {
49
+ return { kind: "invalid", text: "" };
50
+ }
51
+ try {
52
+ decoder.decode();
53
+ }
54
+ catch {
55
+ return { kind: "incomplete", text };
56
+ }
57
+ return { kind: "complete", text };
58
+ }
59
+ function looksLikeSvg(text) {
60
+ return /^\s*(?:<\?xml[^>]*>\s*)?(?:<!--[\s\S]*?-->\s*)?<svg(?:\s|>)/iu.test(text.slice(0, 4_096));
61
+ }
@@ -0,0 +1,126 @@
1
+ import type { ListOutcome, PathOps } from "@better-fs-tools/fs";
2
+ import type { Clock, Digest } from "./digest.ts";
3
+ import type { JsonObject } from "./json.ts";
4
+ /** Per-call context. Tools pass `host` through to extensions and never read it. */
5
+ export interface ToolCallContext<THost = undefined> {
6
+ readonly signal?: AbortSignal;
7
+ /** The framework's tool call id, when it has one. */
8
+ readonly callId?: string;
9
+ readonly host: THost;
10
+ }
11
+ /** "read", "edit", "write", "apply_patch", "bash", or a host tool. */
12
+ export type ToolName = "read" | "edit" | "write" | "apply_patch" | "bash" | (string & {});
13
+ /** A note any tool can emit. */
14
+ export interface Note {
15
+ /** Stable identifier, in kebab case, for example "denied" or "clamped". */
16
+ readonly code: string;
17
+ readonly severity: "info" | "warning";
18
+ readonly message: string;
19
+ readonly data?: JsonObject;
20
+ }
21
+ /**
22
+ * The error of a result with status "error", in every tool. `code` is
23
+ * UPPER_SNAKE. The result's error note has the same code in kebab case, and
24
+ * `message` and `data` are copied from that note.
25
+ */
26
+ export interface ToolError<TCode extends string = string, TPhase extends string = string> {
27
+ readonly code: TCode;
28
+ /** The stage that failed. */
29
+ readonly phase: TPhase;
30
+ /** The error note's message. */
31
+ readonly message: string;
32
+ /** The error note's data, when it has any. */
33
+ readonly data?: JsonObject;
34
+ }
35
+ /** Wording that shared helpers use. Every tool's catalog extends it. */
36
+ export interface ToolMessages {
37
+ pathRepaired(c: {
38
+ from: string;
39
+ to: string;
40
+ }): string;
41
+ denied(c: {
42
+ path: string;
43
+ detail: string | null;
44
+ }): string;
45
+ }
46
+ /** Given to every tool-neutral host function. */
47
+ export interface ToolHookContext<THost = undefined> {
48
+ readonly tool: ToolName;
49
+ readonly messages: Readonly<ToolMessages>;
50
+ readonly digest: Digest | null;
51
+ readonly clock: Clock;
52
+ /** The same object for every stage of one call. */
53
+ readonly call: ToolCallContext<THost>;
54
+ }
55
+ export interface ToolResolveContext<THost = undefined> extends ToolHookContext<THost> {
56
+ readonly paths: PathOps;
57
+ /** Authorizes and lists a directory. A second call in the same stage returns an error outcome. */
58
+ list(dir: string): Promise<ListOutcome>;
59
+ }
60
+ export interface PathResolver<THost = undefined> {
61
+ readonly id: string;
62
+ resolve(path: string, ctx: ToolResolveContext<THost>): ResolveOutcome | Promise<ResolveOutcome>;
63
+ }
64
+ export type ResolveOutcome = {
65
+ readonly kind: "path";
66
+ readonly path: string;
67
+ readonly note?: Note;
68
+ } | {
69
+ readonly kind: "not-found";
70
+ readonly note?: Note;
71
+ };
72
+ /** The fields every authorize target has. */
73
+ export interface AccessTarget {
74
+ readonly action: string;
75
+ readonly requestedPath: string;
76
+ readonly resolvedPath: string;
77
+ readonly displayPath: string;
78
+ }
79
+ export type AccessDecision = {
80
+ readonly allow: true;
81
+ readonly notes?: readonly Note[];
82
+ } | {
83
+ readonly allow: false;
84
+ readonly note?: Note;
85
+ };
86
+ /** An authorizer that works for every tool. It sees only the fields every target has. */
87
+ export interface ToolAuthorizer<THost = undefined> {
88
+ readonly id: string;
89
+ readonly authorize: (target: AccessTarget, ctx: ToolHookContext<THost>) => AccessDecision | Promise<AccessDecision>;
90
+ }
91
+ /**
92
+ * The model-facing side of a tool: its name, description, JSON Schema, and the
93
+ * map from model input to the tool's canonical input. `TParam` is the tool's
94
+ * canonical parameter names.
95
+ */
96
+ export interface ToolSignature<TInput, TParam extends string = string> {
97
+ /** The tool name the model sees. */
98
+ readonly name: string;
99
+ readonly description: string;
100
+ /** Plain JSON Schema with a description on each parameter. */
101
+ readonly schema: JsonObject;
102
+ /** Validates model input and maps it to canonical input. Throws TypeError that names host parameters. */
103
+ toInput(input: unknown): TInput;
104
+ /**
105
+ * Host name for a canonical parameter, for messages. An empty string means
106
+ * the signature has no such parameter, and messages leave out the advice
107
+ * that names it.
108
+ */
109
+ param(name: TParam): string;
110
+ /** Set on freeform signatures. Hosts that support grammar tools use it. */
111
+ readonly grammar?: {
112
+ readonly lark: string;
113
+ };
114
+ }
115
+ /**
116
+ * The options every signature preset takes. `TParam` is the preset's own
117
+ * parameter names. `names` gives a parameter another name in the schema, and
118
+ * `describe` replaces its description. Both are keyed by the preset's names.
119
+ */
120
+ export interface SignatureDocs<TParam extends string> {
121
+ /** The tool name. */
122
+ readonly name?: string;
123
+ readonly description?: string;
124
+ readonly describe?: Partial<Record<TParam, string>>;
125
+ readonly names?: Partial<Record<TParam, string>>;
126
+ }
File without changes
@@ -0,0 +1,50 @@
1
+ import type { JsonObject } from "./json.ts";
2
+ import type { Confidence, ReadNote } from "./result.ts";
3
+ export interface Classifier {
4
+ readonly id: string;
5
+ classify(sample: ClassificationSample): Classification | null;
6
+ /** Refusal for invalid UTF-8 found after the sample was accepted as text. */
7
+ encodingFailure?(sample: ClassificationSample): UnsupportedClassification | null;
8
+ }
9
+ export interface ClassificationSample {
10
+ /** At most limits.sampleBytes. */
11
+ readonly bytes: Uint8Array;
12
+ /** True when the sample is the whole file. */
13
+ readonly complete: boolean;
14
+ /** Display path, for extension hints. */
15
+ readonly path: string;
16
+ /** MIME type reported by the backend, if any. */
17
+ readonly mimeType: string | null;
18
+ }
19
+ export type Classification = TextClassification | UnsupportedClassification;
20
+ export interface TextClassification {
21
+ readonly kind: "text";
22
+ readonly mimeType: string | null;
23
+ readonly confidence: Confidence;
24
+ readonly reasons: readonly string[];
25
+ readonly notes?: readonly ReadNote[];
26
+ }
27
+ export interface UnsupportedClassification {
28
+ readonly kind: "unsupported";
29
+ readonly code: string;
30
+ readonly mimeType: string | null;
31
+ readonly confidence: Confidence;
32
+ readonly reasons: readonly string[];
33
+ /** The refusal the model sees. */
34
+ readonly note: ReadNote;
35
+ }
36
+ export interface NoteOverride {
37
+ readonly code?: string;
38
+ readonly severity?: "info" | "warning";
39
+ readonly message?: string | ((context: NoteContext) => string);
40
+ readonly data?: JsonObject;
41
+ }
42
+ export interface NoteContext {
43
+ readonly path: string;
44
+ readonly mimeType: string | null;
45
+ readonly reasons: readonly string[];
46
+ }
47
+ export type BuiltInUnsupportedCode = "BINARY" | "IMAGE" | "PDF" | "NOTEBOOK" | "OFFICE_DOCUMENT" | "UNKNOWN_ENCODING";
48
+ export interface DefaultClassifierOptions {
49
+ readonly notes?: Partial<Record<BuiltInUnsupportedCode, NoteOverride>>;
50
+ }
File without changes
@@ -0,0 +1,13 @@
1
+ import type { ToolCallContext } from "./base.ts";
2
+ import type { ReadInput } from "./input.ts";
3
+ import type { ReadResult } from "./result.ts";
4
+ /** Per-call context for the read tool. The tool passes `host` through to extensions and never reads it. */
5
+ export interface ReadContext<THost = undefined> extends ToolCallContext<THost> {
6
+ }
7
+ /**
8
+ * The context argument is optional, and `host` may be left out, when THost
9
+ * includes undefined. Otherwise both are required.
10
+ */
11
+ export type ReadTool<THost = undefined> = undefined extends THost ? (input: ReadInput, ctx?: Omit<ReadContext<THost>, "host"> & {
12
+ readonly host?: THost;
13
+ }) => Promise<ReadResult> : (input: ReadInput, ctx: ReadContext<THost>) => Promise<ReadResult>;
File without changes
@@ -0,0 +1,60 @@
1
+ import type { FileSystem } from "@better-fs-tools/fs";
2
+ import type { Classifier } from "./classify.ts";
3
+ import type { ReadContext } from "./context.ts";
4
+ import type { Clock, Digest } from "./digest.ts";
5
+ import type { Converter, PathResolver, ReadAuthorizer, ReadHook, Suggest, ViewBudget } from "./extensions.ts";
6
+ import type { ReadFormatter } from "./format.ts";
7
+ import type { ReadLimits } from "./limits.ts";
8
+ import type { ReadMessageCatalog } from "./messages.ts";
9
+ import type { ReadStateStore } from "./state.ts";
10
+ export interface ReadDependencies<THost = undefined> {
11
+ /** A filesystem, or a factory called once for each read. */
12
+ readonly fs: FileSystem | ((call: ReadContext<THost>) => FileSystem);
13
+ readonly limits: Readonly<ReadLimits>;
14
+ readonly messages: Readonly<ReadMessageCatalog>;
15
+ /** Ordered. First classifier with an opinion wins. Non-empty. */
16
+ readonly classifiers: readonly Classifier[];
17
+ /** Rewrites the requested path before it is opened. null leaves it unchanged. */
18
+ readonly resolve: PathResolver<THost> | null;
19
+ /** Suggests names for a NOT_FOUND note from a listing of the parent directory. null turns suggestions off. */
20
+ readonly suggest: Suggest<THost> | null;
21
+ /** Host access policy for the opened file ("read") and before every listing ("list"). null allows everything. */
22
+ readonly authorize: ReadAuthorizer<THost> | null;
23
+ /** File converters in order: the first that accepts runs. The first directory converter lists directories. */
24
+ readonly converters: readonly Converter<THost>[];
25
+ /** Stops the view early, at a line boundary, when the next line would pass `max`. null has no budget. */
26
+ readonly budget: ViewBudget | null;
27
+ /** Run in order on every outcome that has a request, except ABORTED, before state is recorded. */
28
+ readonly hooks: readonly ReadHook<THost>[];
29
+ /** A store, or a factory called at most once for each read, and only when needed. Needs `digest`. */
30
+ readonly state: ReadStateStore | ((call: ReadContext<THost>) => ReadStateStore | null) | null;
31
+ readonly digest: Digest | null;
32
+ readonly clock: Clock;
33
+ readonly formatter: ReadFormatter<THost>;
34
+ }
35
+ /**
36
+ * `state` needs a `digest`, because a stored record names the digest that made
37
+ * it. `state` without `digest` does not type-check, and throws TypeError at run
38
+ * time.
39
+ */
40
+ export type StateNeedsDigest = {
41
+ readonly state?: null;
42
+ } | {
43
+ readonly digest: Digest;
44
+ };
45
+ /**
46
+ * The same pairing for a factory with its own default digest, such as the
47
+ * Node and Pi factories. Leave `digest` out to get the default. `digest: null`
48
+ * turns it off, and then `state` must be left out or null.
49
+ */
50
+ export type StateNeedsDigestOrDefault = {
51
+ readonly state?: null;
52
+ readonly digest: null;
53
+ } | {
54
+ readonly digest?: Digest;
55
+ };
56
+ export type ReadToolDeps<THost = undefined> = {
57
+ readonly fs: ReadDependencies<THost>["fs"];
58
+ readonly limits?: Partial<ReadLimits>;
59
+ readonly messages?: Partial<ReadMessageCatalog>;
60
+ } & Partial<Omit<ReadDependencies<THost>, "fs" | "limits" | "messages">>;
File without changes
@@ -0,0 +1,13 @@
1
+ export interface Digest {
2
+ readonly id: string;
3
+ /** Incremental hash over scanned bytes. */
4
+ create(): DigestStream;
5
+ /** One-shot hash of a string. */
6
+ hash(value: string): string;
7
+ }
8
+ export interface DigestStream {
9
+ update(bytes: Uint8Array): void;
10
+ digest(): string;
11
+ }
12
+ /** Returns the current time. Every tool and `memoryStore` take this clock type. */
13
+ export type Clock = () => Date;
File without changes
@@ -0,0 +1,115 @@
1
+ import type { DirectoryEntry, ListOutcome, OpenFileInfo } from "@better-fs-tools/fs";
2
+ import type { AccessTarget, ToolHookContext, ToolResolveContext } from "./base.ts";
3
+ import type { Classification, ClassificationSample } from "./classify.ts";
4
+ import type { ReadContext } from "./context.ts";
5
+ import type { ReadRequest } from "./input.ts";
6
+ import type { ReadLimits } from "./limits.ts";
7
+ import type { ReadMessageCatalog } from "./messages.ts";
8
+ import type { ContentPart, ReadNote, ReadReport } from "./result.ts";
9
+ import type { ReadRecord } from "./state.ts";
10
+ /** Given to every host function that runs during a call. */
11
+ export interface ReadHookContext<THost = undefined> extends ToolHookContext<THost> {
12
+ readonly tool: "read";
13
+ readonly request: ReadRequest;
14
+ readonly limits: Readonly<ReadLimits>;
15
+ readonly messages: Readonly<ReadMessageCatalog>;
16
+ /** Same object for every stage of one call. */
17
+ readonly call: ReadContext<THost>;
18
+ }
19
+ /** Given to a resolver. It fits ToolResolveContext, so one resolver works for every tool. */
20
+ export interface ReadResolveContext<THost = undefined> extends ReadHookContext<THost>, Pick<ToolResolveContext<THost>, "paths" | "list"> {
21
+ }
22
+ export type { PathResolver, ResolveOutcome } from "./base.ts";
23
+ export type Suggest<THost = undefined> = (ctx: SuggestContext<THost>) => readonly string[];
24
+ export interface SuggestContext<THost = undefined> {
25
+ /** The path that missed. */
26
+ readonly path: string;
27
+ readonly name: string;
28
+ readonly entries: readonly DirectoryEntry[];
29
+ readonly entriesTruncated: boolean;
30
+ /** limits.maxSuggestions. Names past it are dropped. */
31
+ readonly max: number;
32
+ readonly call: ReadContext<THost>;
33
+ }
34
+ export interface ReadAuthorizer<THost = undefined> {
35
+ readonly id: string;
36
+ readonly authorize: (target: ReadAuthorizeTarget, ctx: ReadHookContext<THost>) => ReadAuthorizeDecision | Promise<ReadAuthorizeDecision>;
37
+ }
38
+ export interface ReadAuthorizeTarget extends AccessTarget {
39
+ readonly action: "read" | "list";
40
+ /** Realpath for "read". Lexical directory path for "list". */
41
+ readonly resolvedPath: string;
42
+ readonly size: number | null;
43
+ readonly mtimeMs: number | null;
44
+ }
45
+ /** AccessDecision with read notes, so a denial can carry a retry. A ToolAuthorizer's decision fits it. */
46
+ export type ReadAuthorizeDecision = {
47
+ readonly allow: true;
48
+ readonly notes?: readonly ReadNote[];
49
+ } | {
50
+ readonly allow: false;
51
+ readonly note?: ReadNote;
52
+ };
53
+ export type Converter<THost = undefined> = FileConverter<THost> | DirectoryConverter<THost>;
54
+ export interface ConverterMatch {
55
+ readonly classification: Classification;
56
+ readonly classifier: string;
57
+ readonly sample: ClassificationSample;
58
+ }
59
+ export interface FileConverter<THost = undefined> {
60
+ readonly id: string;
61
+ readonly target: "file";
62
+ /** Whether this converter handles the match. The only place a converter may decline. */
63
+ accepts(match: ConverterMatch): boolean;
64
+ convert(input: FileConvertInput, ctx: ReadHookContext<THost>): Promise<ConvertOutcome>;
65
+ }
66
+ export interface FileConvertInput {
67
+ readonly info: Readonly<OpenFileInfo>;
68
+ readonly classification: Classification;
69
+ readonly sample: ClassificationSample;
70
+ /** The whole file from byte 0, capped at limits.maxConvertBytes. Call it once. */
71
+ bytes(): AsyncIterable<Uint8Array>;
72
+ }
73
+ export interface DirectoryConverter<THost = undefined> {
74
+ readonly id: string;
75
+ readonly target: "directory";
76
+ convert(input: DirectoryConvertInput, ctx: ReadHookContext<THost>): Promise<ConvertOutcome>;
77
+ }
78
+ export interface DirectoryConvertInput {
79
+ /** Lexical path that open() reported as a directory. */
80
+ readonly path: string;
81
+ readonly target: {
82
+ readonly resolvedPath: string;
83
+ readonly displayPath: string;
84
+ } | null;
85
+ /** Authorizes and lists the directory, up to limits.maxDirectoryEntries. Call it once. */
86
+ list(): Promise<ListOutcome>;
87
+ }
88
+ export type ConvertOutcome = {
89
+ readonly kind: "text";
90
+ readonly text: string | AsyncIterable<string>;
91
+ readonly mimeType: string | null;
92
+ readonly notes?: readonly ReadNote[];
93
+ } | {
94
+ readonly kind: "media";
95
+ readonly parts: readonly ContentPart[];
96
+ readonly notes?: readonly ReadNote[];
97
+ } | {
98
+ readonly kind: "refuse";
99
+ readonly code: string;
100
+ readonly note: ReadNote;
101
+ };
102
+ export interface ReadHook<THost = undefined> {
103
+ readonly id: string;
104
+ afterRead(outcome: ReadReport, ctx: AfterReadContext<THost>): ReadReport | Promise<ReadReport>;
105
+ }
106
+ export interface AfterReadContext<THost = undefined> extends ReadHookContext<THost> {
107
+ /** The stored record from before this read. null with no state or no record. */
108
+ readonly previous: ReadRecord | null;
109
+ }
110
+ export interface ViewBudget {
111
+ readonly id: string;
112
+ /** Cost of one clamped source line. Sync and cheap. */
113
+ measure(text: string): number;
114
+ readonly max: number;
115
+ }
File without changes
@@ -0,0 +1,16 @@
1
+ import type { ReadContext } from "./context.ts";
2
+ import type { Digest } from "./digest.ts";
3
+ import type { ReadLimits } from "./limits.ts";
4
+ import type { ContentPart, ReadReport } from "./result.ts";
5
+ export interface ReadFormatContext<THost = undefined> {
6
+ readonly digest: Digest | null;
7
+ readonly limits: Readonly<ReadLimits>;
8
+ /** "view" formats the body only, without notes. */
9
+ readonly mode: "model" | "view";
10
+ readonly call: ReadContext<THost>;
11
+ }
12
+ export interface ReadFormatter<THost = undefined> {
13
+ readonly id: string;
14
+ /** Sync and pure over the outcome. A string becomes one text part. */
15
+ format(outcome: ReadReport, ctx: ReadFormatContext<THost>): string | readonly ContentPart[];
16
+ }
File without changes
@@ -0,0 +1,16 @@
1
+ /** Canonical read input. A signature maps the model's parameter names to it. */
2
+ export interface ReadInput {
3
+ readonly path: string;
4
+ /** One-based source line. Default 1. */
5
+ readonly offset?: number;
6
+ /** Maximum source lines in the view. Default and ceiling: limits.maxLines. */
7
+ readonly limit?: number;
8
+ }
9
+ /** A validated request. `offset` and `limit` are concrete. */
10
+ export interface ReadRequest {
11
+ readonly path: string;
12
+ readonly offset: number;
13
+ readonly limit: number;
14
+ /** True when the input set `offset` or `limit`. */
15
+ readonly ranged: boolean;
16
+ }
File without changes
@@ -0,0 +1,6 @@
1
+ export type JsonValue = string | number | boolean | null | readonly JsonValue[] | {
2
+ readonly [key: string]: JsonValue;
3
+ };
4
+ export type JsonObject = {
5
+ readonly [key: string]: JsonValue;
6
+ };
File without changes
@@ -0,0 +1,20 @@
1
+ export interface ReadLimits {
2
+ /** Lines in the view. Default 2_000. */
3
+ readonly maxLines: number;
4
+ /** UTF-8 bytes of source text in the view. Default 50 KiB (51 200). */
5
+ readonly maxViewBytes: number;
6
+ /** Characters per line before clamping. Default 2_000. */
7
+ readonly maxCharsPerLine: number;
8
+ /** Bytes scanned before totals and content identity are abandoned. Default 64 MiB. */
9
+ readonly maxScanBytes: number;
10
+ /** Bytes given to classifiers. Default 8 KiB. Must not be more than maxScanBytes. */
11
+ readonly sampleBytes: number;
12
+ /** Entries in one directory listing. Default 200. */
13
+ readonly maxDirectoryEntries: number;
14
+ /** Suggested names on a miss. Default 5. */
15
+ readonly maxSuggestions: number;
16
+ /** Source bytes a converter may read. Default 64 MiB. */
17
+ readonly maxConvertBytes: number;
18
+ /** Total bytes of media parts in one result. Default 5 MiB. */
19
+ readonly maxMediaBytes: number;
20
+ }
File without changes
@@ -0,0 +1,110 @@
1
+ import type { NodeKind } from "@better-fs-tools/fs";
2
+ import type { ToolMessages } from "./base.ts";
3
+ import type { ReadInput, ReadRequest } from "./input.ts";
4
+ import type { TruncationReason } from "./result.ts";
5
+ /** The stage that was running. */
6
+ export type ReadPhase = "input" | "resolve" | "open" | "authorize" | "sampling" | "conversion" | "scan" | "verification" | "hooks";
7
+ /**
8
+ * Wording for every note the read tool writes. Classifiers word their own refusals.
9
+ * Every message that suggests a retry gets `retry`, the text from `retry(next)`.
10
+ */
11
+ export interface ReadMessageCatalog extends ToolMessages {
12
+ /** Prints a canonical retry the way the model must send it. Default: JSON.stringify(next). */
13
+ retry(next: ReadInput): string;
14
+ continuation(c: {
15
+ request: ReadRequest;
16
+ retry: string;
17
+ reason: TruncationReason;
18
+ }): string;
19
+ firstLineTooLong(c: {
20
+ line: number;
21
+ maxViewBytes: number;
22
+ retry: string;
23
+ }): string;
24
+ offsetPastEof(c: {
25
+ request: ReadRequest;
26
+ totalLines: number;
27
+ retry: string;
28
+ }): string;
29
+ offsetUnreached(c: {
30
+ request: ReadRequest;
31
+ reachedLine: number;
32
+ retry: string;
33
+ }): string;
34
+ lineClamped(c: {
35
+ lines: readonly number[];
36
+ total: number;
37
+ maxChars: number;
38
+ }): string;
39
+ /** The requested line count was over limits.maxLines, so the view uses the maximum. */
40
+ limitClamped(c: {
41
+ requested: number;
42
+ max: number;
43
+ }): string;
44
+ scanLimit(c: {
45
+ maxScanBytes: number;
46
+ }): string;
47
+ empty(c: {
48
+ path: string;
49
+ }): string;
50
+ notFound(c: {
51
+ request: ReadRequest;
52
+ suggestions: readonly string[];
53
+ }): string;
54
+ pathRepaired(c: {
55
+ from: string;
56
+ to: string;
57
+ }): string;
58
+ notAFile(c: {
59
+ request: ReadRequest;
60
+ kind: NodeKind;
61
+ }): string;
62
+ dangerousPath(c: {
63
+ request: ReadRequest;
64
+ detail: string | null;
65
+ }): string;
66
+ outsideAllowedRoots(c: {
67
+ request: ReadRequest;
68
+ detail: string | null;
69
+ }): string;
70
+ permissionDenied(c: {
71
+ request: ReadRequest;
72
+ detail: string | null;
73
+ }): string;
74
+ /** Used by shared authorizers such as denyPaths, for every tool. */
75
+ denied(c: {
76
+ path: string;
77
+ detail: string | null;
78
+ }): string;
79
+ /** `stage` "backend" means the filesystem backend refused the size before any byte was read. */
80
+ tooLarge(c: {
81
+ request: ReadRequest;
82
+ stage: "convert" | "media" | "backend";
83
+ limit: number;
84
+ }): string;
85
+ changedDuringRead(c: {
86
+ request: ReadRequest;
87
+ retry: string;
88
+ }): string;
89
+ aborted(c: {
90
+ phase: ReadPhase;
91
+ }): string;
92
+ invalidInput(c: {
93
+ detail: string;
94
+ }): string;
95
+ extensionFailed(c: {
96
+ request: ReadRequest;
97
+ extension: string;
98
+ phase: ReadPhase;
99
+ }): string;
100
+ ioError(c: {
101
+ request: ReadRequest | null;
102
+ }): string;
103
+ unsupportedBackend(c: {
104
+ request: ReadRequest;
105
+ detail: string | null;
106
+ }): string;
107
+ viewModified(c: {
108
+ hook: string;
109
+ }): string;
110
+ }
File without changes