@better-fs-tools/read 0.0.0-stage → 0.1.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 +799 -2
  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 +15 -0
  28. package/dist/classifiers/index.js +32 -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 +120 -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 +133 -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 +43 -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 +81 -0
  111. package/dist/core/notes.d.ts +16 -0
  112. package/dist/core/notes.js +114 -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 +73 -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 +249 -0
  212. package/docs/result-schema.md +222 -0
  213. package/package.json +52 -4
@@ -0,0 +1,33 @@
1
+ export function defaultNoteLine(note) {
2
+ return `[read:${note.code}] ${note.message}`;
3
+ }
4
+ export function layout(outcome, ctx, body, options) {
5
+ if (ctx.mode === "view")
6
+ return body;
7
+ const header = options.header?.(outcome, ctx) ?? null;
8
+ const footer = options.footer?.(outcome, ctx) ?? null;
9
+ const main = [header, body, footer].filter((part) => part !== null && part !== "").join("\n");
10
+ return join(main, noteLines(outcome.notes, options));
11
+ }
12
+ export function noteLines(notes, options) {
13
+ const filter = options.notes ?? ((note) => note);
14
+ const line = options.noteLine ?? defaultNoteLine;
15
+ const lines = [];
16
+ for (const note of notes) {
17
+ const shown = filter(note);
18
+ if (shown !== null)
19
+ lines.push(line(shown));
20
+ }
21
+ return lines;
22
+ }
23
+ export function join(main, lines) {
24
+ const notes = lines.join("\n");
25
+ if (main && notes)
26
+ return `${main}\n\n${notes}`;
27
+ return main || notes;
28
+ }
29
+ export function withParts(outcome, text) {
30
+ if (outcome.status !== "media")
31
+ return text;
32
+ return text === "" ? [...outcome.parts] : [{ type: "text", text }, ...outcome.parts];
33
+ }
@@ -0,0 +1,11 @@
1
+ import type { ReadFormatContext, ReadFormatter } from "../contract/format.ts";
2
+ import type { ReadLine } from "../contract/result.ts";
3
+ import type { LayoutOptions } from "./layout.ts";
4
+ export interface LineNumberFormatterOptions extends LayoutOptions {
5
+ /** Default: `${line.number}|`. */
6
+ gutter?: (line: ReadLine, ctx: ReadFormatContext<unknown>) => string;
7
+ /** Default: `… [line truncated at ${n} chars]`. */
8
+ clampMarker?: (line: ReadLine) => string;
9
+ }
10
+ /** Numbered lines, then notes. A media outcome gives a text part with the notes, then the media parts. */
11
+ export declare function lineNumberFormatter(options?: LineNumberFormatterOptions): ReadFormatter<unknown>;
@@ -0,0 +1,23 @@
1
+ import { layout, withParts } from "./layout.js";
2
+ /** Numbered lines, then notes. A media outcome gives a text part with the notes, then the media parts. */
3
+ export function lineNumberFormatter(options = {}) {
4
+ const gutter = options.gutter ?? defaultGutter;
5
+ const clampMarker = options.clampMarker ?? defaultClampMarker;
6
+ return Object.freeze({
7
+ id: "line-number",
8
+ format(outcome, ctx) {
9
+ const body = outcome.status === "ok"
10
+ ? outcome.view.lines
11
+ .map((line) => `${gutter(line, ctx)}${line.text}${line.clamped ? clampMarker(line) : ""}`)
12
+ .join("\n")
13
+ : "";
14
+ return withParts(outcome, layout(outcome, ctx, body, options));
15
+ },
16
+ });
17
+ }
18
+ function defaultGutter(line) {
19
+ return `${line.number}|`;
20
+ }
21
+ function defaultClampMarker(line) {
22
+ return `… [line truncated at ${line.text.length} chars]`;
23
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReadFormatter } from "../contract/format.ts";
2
+ import type { LineNumberFormatterOptions } from "./line-number.ts";
3
+ /**
4
+ * Source text with no gutter and no clamp marker, then notes. It trades line
5
+ * numbers for exact text, for hosts whose model pastes output into an edit call.
6
+ */
7
+ export declare function plainFormatter(options?: Omit<LineNumberFormatterOptions, "gutter" | "clampMarker">): ReadFormatter<unknown>;
@@ -0,0 +1,14 @@
1
+ import { layout, withParts } from "./layout.js";
2
+ /**
3
+ * Source text with no gutter and no clamp marker, then notes. It trades line
4
+ * numbers for exact text, for hosts whose model pastes output into an edit call.
5
+ */
6
+ export function plainFormatter(options = {}) {
7
+ return Object.freeze({
8
+ id: "plain",
9
+ format(outcome, ctx) {
10
+ const body = outcome.status === "ok" ? outcome.view.lines.map((line) => line.text).join("\n") : "";
11
+ return withParts(outcome, layout(outcome, ctx, body, options));
12
+ },
13
+ });
14
+ }
@@ -0,0 +1,4 @@
1
+ export { redact } from "./redact.ts";
2
+ export type { RedactOptions } from "./redact.ts";
3
+ export { repeatReadGuard } from "./repeat-read-guard.ts";
4
+ export type { RepeatReadGuardOptions } from "./repeat-read-guard.ts";
@@ -0,0 +1,2 @@
1
+ export { redact } from "./redact.js";
2
+ export { repeatReadGuard } from "./repeat-read-guard.js";
@@ -0,0 +1,14 @@
1
+ import type { ReadHook } from "../contract/extensions.ts";
2
+ export interface RedactOptions {
3
+ /** Each pattern must have the "g" flag. */
4
+ patterns: readonly RegExp[];
5
+ /** Default "[REDACTED]". Inserted as is: "$" has no special meaning. */
6
+ replacement?: string;
7
+ }
8
+ /**
9
+ * Replaces matches in view line text and in text parts. A match must lie
10
+ * inside one line, and a clamped line holds only its first
11
+ * limits.maxCharsPerLine characters. When nothing matches, the outcome is
12
+ * unchanged and gets no view-modified note.
13
+ */
14
+ export declare function redact(options: RedactOptions): ReadHook<unknown>;
@@ -0,0 +1,53 @@
1
+ const ENCODER = new TextEncoder();
2
+ /**
3
+ * Replaces matches in view line text and in text parts. A match must lie
4
+ * inside one line, and a clamped line holds only its first
5
+ * limits.maxCharsPerLine characters. When nothing matches, the outcome is
6
+ * unchanged and gets no view-modified note.
7
+ */
8
+ export function redact(options) {
9
+ const { patterns, replacement = "[REDACTED]" } = options ?? {};
10
+ if (!Array.isArray(patterns))
11
+ throw new TypeError("redact patterns must be an array");
12
+ for (const pattern of patterns) {
13
+ if (!(pattern instanceof RegExp) || !pattern.flags.includes("g")) {
14
+ throw new TypeError(`redact pattern ${String(pattern)} must be a RegExp with the "g" flag`);
15
+ }
16
+ }
17
+ if (typeof replacement !== "string")
18
+ throw new TypeError("redact replacement must be a string");
19
+ const list = Object.freeze([...patterns]);
20
+ const scrub = (text) => list.reduce((current, pattern) => current.replace(pattern, () => replacement), text);
21
+ return Object.freeze({
22
+ id: "redact",
23
+ afterRead(outcome) {
24
+ if (outcome.status === "ok") {
25
+ const lines = outcome.view.lines.map((line) => {
26
+ const text = scrub(line.text);
27
+ return text === line.text ? line : { ...line, text };
28
+ });
29
+ if (lines.every((line, index) => line === outcome.view.lines[index]))
30
+ return outcome;
31
+ return { ...outcome, view: { ...outcome.view, lines, bytes: viewBytes(lines) } };
32
+ }
33
+ if (outcome.status === "media") {
34
+ const parts = outcome.parts.map((part) => {
35
+ if (part.type !== "text")
36
+ return part;
37
+ const text = scrub(part.text);
38
+ return text === part.text ? part : { ...part, text };
39
+ });
40
+ if (parts.every((part, index) => part === outcome.parts[index]))
41
+ return outcome;
42
+ return { ...outcome, parts };
43
+ }
44
+ return outcome;
45
+ },
46
+ });
47
+ }
48
+ function viewBytes(lines) {
49
+ let total = Math.max(0, lines.length - 1);
50
+ for (const line of lines)
51
+ total += ENCODER.encode(line.text).byteLength;
52
+ return total;
53
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReadHook } from "../contract/extensions.ts";
2
+ import type { ReadRecord } from "../contract/state.ts";
3
+ export interface RepeatReadGuardOptions {
4
+ /** The repeat-read note text. Default: names the path and when it was read. */
5
+ message?: (previous: ReadRecord) => string;
6
+ }
7
+ /**
8
+ * Empties the view and adds a "repeat-read" note when the same range of the
9
+ * same content was already read. Needs a digest and a state store. A record a
10
+ * write tool stored does not count. The stored record then says the whole file
11
+ * was not visible.
12
+ */
13
+ export declare function repeatReadGuard(options?: RepeatReadGuardOptions): ReadHook<unknown>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Empties the view and adds a "repeat-read" note when the same range of the
3
+ * same content was already read. Needs a digest and a state store. A record a
4
+ * write tool stored does not count. The stored record then says the whole file
5
+ * was not visible.
6
+ */
7
+ export function repeatReadGuard(options = {}) {
8
+ const { message } = options ?? {};
9
+ if (message !== undefined && typeof message !== "function") {
10
+ throw new TypeError("repeatReadGuard message must be a function");
11
+ }
12
+ return Object.freeze({
13
+ id: "repeat-read-guard",
14
+ afterRead(outcome, ctx) {
15
+ const { previous } = ctx;
16
+ if (outcome.status !== "ok" ||
17
+ outcome.observation === null ||
18
+ previous === null ||
19
+ previous.origin !== "read" ||
20
+ previous.request === null) {
21
+ return outcome;
22
+ }
23
+ const { contentId } = outcome.observation;
24
+ const { request, view } = outcome;
25
+ if (contentId === null ||
26
+ previous.contentId !== contentId ||
27
+ previous.request.offset !== request.offset ||
28
+ previous.request.limit !== request.limit ||
29
+ view.lines.length === 0) {
30
+ return outcome;
31
+ }
32
+ return {
33
+ ...outcome,
34
+ view: { ...view, lines: [], endLine: view.startLine - 1, bytes: 0, partial: true },
35
+ notes: [
36
+ ...outcome.notes,
37
+ {
38
+ code: "repeat-read",
39
+ severity: "info",
40
+ message: message === undefined
41
+ ? `${request.path} has not changed since it was read with the same range at ${previous.observedAt}. That output is still current, so it is not repeated.`
42
+ : message(previous),
43
+ data: { observationId: previous.observationId, observedAt: previous.observedAt },
44
+ },
45
+ ],
46
+ };
47
+ },
48
+ });
49
+ }
@@ -0,0 +1,35 @@
1
+ export type { Classification, ClassificationSample, Classifier } from "./contract/classify.ts";
2
+ export type { BuiltInUnsupportedCode, DefaultClassifierOptions, NoteContext, NoteOverride, TextClassification, UnsupportedClassification, } from "./contract/classify.ts";
3
+ export type { AccessDecision, AccessTarget, Note, SignatureDocs, ToolAuthorizer, ToolCallContext, ToolError, ToolHookContext, ToolMessages, ToolName, ToolResolveContext, ToolSignature, } from "./contract/base.ts";
4
+ export type { ReadContext, ReadTool } from "./contract/context.ts";
5
+ export type { ReadDependencies, ReadToolDeps, StateNeedsDigest, StateNeedsDigestOrDefault, } from "./contract/deps.ts";
6
+ export type { Clock, Digest, DigestStream } from "./contract/digest.ts";
7
+ export type { AfterReadContext, Converter, ConverterMatch, ConvertOutcome, DirectoryConverter, DirectoryConvertInput, FileConverter, FileConvertInput, PathResolver, ReadAuthorizeDecision, ReadAuthorizer, ReadAuthorizeTarget, ReadHook, ReadHookContext, ReadResolveContext, ResolveOutcome, Suggest, SuggestContext, ViewBudget, } from "./contract/extensions.ts";
8
+ export type { ReadFormatContext, ReadFormatter } from "./contract/format.ts";
9
+ export type { ReadInput, ReadRequest } from "./contract/input.ts";
10
+ export type { JsonObject, JsonValue } from "./contract/json.ts";
11
+ export type { ReadLimits } from "./contract/limits.ts";
12
+ export type { ReadMessageCatalog, ReadPhase } from "./contract/messages.ts";
13
+ export type { ClassificationInfo, Confidence, ContentPart, ConversionInfo, FileInfo, MediaPart, ReadContinuation, ReadError, ReadErrorCode, ReadFailure, ReadLine, ReadMedia, ReadNote, ReadObservation, ReadOk, ReadReport, ReadResult, ReadTotals, ReadTruncation, ReadUnsupported, ReadView, TextPart, TruncationReason, } from "./contract/result.ts";
14
+ export type { ReadRecord, ReadStateStore } from "./contract/state.ts";
15
+ export { createReadTool } from "./core/create-read-tool.ts";
16
+ export { parseReadInput } from "./core/input.ts";
17
+ export { defaultReadLimits, resolveReadLimits } from "./core/limits.ts";
18
+ export { defaultReadMessages, resolveReadMessages } from "./core/messages.ts";
19
+ export { textOf } from "./core/format.ts";
20
+ export { sha256Digest } from "./digest/sha256.ts";
21
+ export { memoryStore } from "./state/index.ts";
22
+ export type { MemoryStoreOptions } from "./state/index.ts";
23
+ export { binaryClassifier, defaultClassifiers, extensionClassifier, imageClassifier, notebookClassifier, officeClassifier, pdfClassifier, utf8Classifier, } from "./classifiers/index.ts";
24
+ export type { ExtensionClassifierOptions } from "./classifiers/index.ts";
25
+ export { askUser, compileGlob, denyPaths, readAuthorizers, sizeCeiling, } from "./authorize/index.ts";
26
+ export { directoryListing, imageConverter, notebookConverter, textConverter, } from "./converters/index.ts";
27
+ export type { DirectoryListingOptions, ImageConverterOptions, NotebookConverterOptions, TextConverterOptions, } from "./converters/index.ts";
28
+ export { expandHome, pathResolvers, reanchorLeadingSlash, stripPrefixes, unicodeRepair, } from "./resolve/index.ts";
29
+ export { redact, repeatReadGuard } from "./hooks/index.ts";
30
+ export type { RedactOptions, RepeatReadGuardOptions } from "./hooks/index.ts";
31
+ export { charsPerToken } from "./budget/index.ts";
32
+ export type { CharsPerTokenOptions } from "./budget/index.ts";
33
+ export { canonicalizeFileName, defaultSuggest, suggestFileNames } from "./suggest/index.ts";
34
+ export { defaultNoteLine, eofFooter, fileHashHeader, hashlineGutter, jsonFormatter, lineNumberFormatter, plainFormatter, } from "./formatters/index.ts";
35
+ export type { HashlineGutterOptions, JsonFormatterOptions, LineNumberFormatterOptions, } from "./formatters/index.ts";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { createReadTool } from "./core/create-read-tool.js";
2
+ export { parseReadInput } from "./core/input.js";
3
+ export { defaultReadLimits, resolveReadLimits } from "./core/limits.js";
4
+ export { defaultReadMessages, resolveReadMessages } from "./core/messages.js";
5
+ export { textOf } from "./core/format.js";
6
+ export { sha256Digest } from "./digest/sha256.js";
7
+ export { memoryStore } from "./state/index.js";
8
+ export { binaryClassifier, defaultClassifiers, extensionClassifier, imageClassifier, notebookClassifier, officeClassifier, pdfClassifier, utf8Classifier, } from "./classifiers/index.js";
9
+ export { askUser, compileGlob, denyPaths, readAuthorizers, sizeCeiling, } from "./authorize/index.js";
10
+ export { directoryListing, imageConverter, notebookConverter, textConverter, } from "./converters/index.js";
11
+ export { expandHome, pathResolvers, reanchorLeadingSlash, stripPrefixes, unicodeRepair, } from "./resolve/index.js";
12
+ export { redact, repeatReadGuard } from "./hooks/index.js";
13
+ export { charsPerToken } from "./budget/index.js";
14
+ export { canonicalizeFileName, defaultSuggest, suggestFileNames } from "./suggest/index.js";
15
+ export { defaultNoteLine, eofFooter, fileHashHeader, hashlineGutter, jsonFormatter, lineNumberFormatter, plainFormatter, } from "./formatters/index.js";
@@ -0,0 +1,5 @@
1
+ import type { PathResolver } from "../contract/base.ts";
2
+ /** Expands "~" and "~/x" to `home`. Other "~user" forms pass through unchanged. */
3
+ export declare function expandHome(options: {
4
+ home: string;
5
+ }): PathResolver<unknown>;
@@ -0,0 +1,17 @@
1
+ /** Expands "~" and "~/x" to `home`. Other "~user" forms pass through unchanged. */
2
+ export function expandHome(options) {
3
+ const home = options?.home;
4
+ if (typeof home !== "string" || home === "") {
5
+ throw new TypeError("expandHome needs a non-empty home");
6
+ }
7
+ return Object.freeze({
8
+ id: "expand-home",
9
+ resolve(path, ctx) {
10
+ if (path === "~" || path === "~/")
11
+ return { kind: "path", path: home };
12
+ if (!path.startsWith("~/"))
13
+ return { kind: "path", path };
14
+ return { kind: "path", path: ctx.paths.join(home, path.slice(2)) };
15
+ },
16
+ });
17
+ }
@@ -0,0 +1,5 @@
1
+ export { expandHome } from "./expand-home.ts";
2
+ export { pathResolvers } from "./path-resolvers.ts";
3
+ export { reanchorLeadingSlash } from "./reanchor-leading-slash.ts";
4
+ export { stripPrefixes } from "./strip-prefixes.ts";
5
+ export { unicodeRepair } from "./unicode-repair.ts";
@@ -0,0 +1,5 @@
1
+ export { expandHome } from "./expand-home.js";
2
+ export { pathResolvers } from "./path-resolvers.js";
3
+ export { reanchorLeadingSlash } from "./reanchor-leading-slash.js";
4
+ export { stripPrefixes } from "./strip-prefixes.js";
5
+ export { unicodeRepair } from "./unicode-repair.js";
@@ -0,0 +1,7 @@
1
+ import type { PathResolver } from "../contract/base.ts";
2
+ /**
3
+ * Runs resolvers left to right, each on the previous path. The first not-found
4
+ * stops the chain. All steps share one ctx.list budget, and the last note wins.
5
+ * A step that throws fails the read with EXTENSION_FAILED, naming the step's id.
6
+ */
7
+ export declare function pathResolvers<THost = unknown>(...steps: readonly PathResolver<NoInfer<THost>>[]): PathResolver<THost>;
@@ -0,0 +1,38 @@
1
+ import { stepFailure } from "../core/extension-error.js";
2
+ /**
3
+ * Runs resolvers left to right, each on the previous path. The first not-found
4
+ * stops the chain. All steps share one ctx.list budget, and the last note wins.
5
+ * A step that throws fails the read with EXTENSION_FAILED, naming the step's id.
6
+ */
7
+ export function pathResolvers(...steps) {
8
+ for (const step of steps) {
9
+ if (step === null || typeof step !== "object" || typeof step.resolve !== "function") {
10
+ throw new TypeError("pathResolvers takes path resolvers");
11
+ }
12
+ }
13
+ const chain = Object.freeze([...steps]);
14
+ return Object.freeze({
15
+ id: chain.length === 0 ? "identity" : chain.map((step) => step.id).join("+"),
16
+ async resolve(path, ctx) {
17
+ let current = path;
18
+ let note;
19
+ for (const step of chain) {
20
+ let outcome;
21
+ try {
22
+ outcome = await step.resolve(current, ctx);
23
+ note = outcome.note ?? note;
24
+ }
25
+ catch (error) {
26
+ throw stepFailure(step, error);
27
+ }
28
+ if (outcome.kind === "not-found") {
29
+ return note === undefined ? { kind: "not-found" } : { kind: "not-found", note };
30
+ }
31
+ current = outcome.path;
32
+ }
33
+ return note === undefined
34
+ ? { kind: "path", path: current }
35
+ : { kind: "path", path: current, note };
36
+ },
37
+ });
38
+ }
@@ -0,0 +1,9 @@
1
+ import type { PathResolver } from "../contract/base.ts";
2
+ /**
3
+ * Rewrites "/src/x.ts" to "src/x.ts" when `firstSegmentExists("src")` is true.
4
+ * It should return true only for a segment that exists under the tool root and
5
+ * not at "/".
6
+ */
7
+ export declare function reanchorLeadingSlash(options: {
8
+ firstSegmentExists: (segment: string) => boolean;
9
+ }): PathResolver<unknown>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Rewrites "/src/x.ts" to "src/x.ts" when `firstSegmentExists("src")` is true.
3
+ * It should return true only for a segment that exists under the tool root and
4
+ * not at "/".
5
+ */
6
+ export function reanchorLeadingSlash(options) {
7
+ const exists = options?.firstSegmentExists;
8
+ if (typeof exists !== "function") {
9
+ throw new TypeError("reanchorLeadingSlash needs a firstSegmentExists function");
10
+ }
11
+ return Object.freeze({
12
+ id: "reanchor-leading-slash",
13
+ resolve(path) {
14
+ if (!path.startsWith("/") || path.startsWith("//"))
15
+ return { kind: "path", path };
16
+ const rest = path.slice(1);
17
+ const segment = rest.split("/", 1)[0] ?? "";
18
+ if (segment === "" || !exists(segment))
19
+ return { kind: "path", path };
20
+ return { kind: "path", path: rest };
21
+ },
22
+ });
23
+ }
@@ -0,0 +1,6 @@
1
+ import type { PathResolver } from "../contract/base.ts";
2
+ /** Strips "file://" (percent-decoded) and a leading "@". Both default true. */
3
+ export declare function stripPrefixes(options?: {
4
+ fileUrl?: boolean;
5
+ at?: boolean;
6
+ }): PathResolver<unknown>;
@@ -0,0 +1,30 @@
1
+ const FILE_URL = "file://";
2
+ /** Strips "file://" (percent-decoded) and a leading "@". Both default true. */
3
+ export function stripPrefixes(options = {}) {
4
+ const fileUrl = options.fileUrl ?? true;
5
+ const at = options.at ?? true;
6
+ return Object.freeze({
7
+ id: "strip-prefixes",
8
+ resolve(path) {
9
+ let next = path;
10
+ if (fileUrl && next.startsWith(FILE_URL))
11
+ next = fromFileUrl(next) ?? next;
12
+ if (at && next.length > 1 && next.startsWith("@"))
13
+ next = next.slice(1);
14
+ return { kind: "path", path: next };
15
+ },
16
+ });
17
+ }
18
+ function fromFileUrl(url) {
19
+ let rest = url.slice(FILE_URL.length);
20
+ if (rest.startsWith("localhost/"))
21
+ rest = rest.slice("localhost".length);
22
+ let decoded;
23
+ try {
24
+ decoded = decodeURIComponent(rest);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ return decoded.trim() === "" || decoded.includes("\0") ? null : decoded;
30
+ }
@@ -0,0 +1,9 @@
1
+ import type { PathResolver } from "../contract/base.ts";
2
+ /**
3
+ * Repairs a file name that matches exactly one entry in its directory up to
4
+ * Unicode equivalence. An exact match, several matches, or a failed listing
5
+ * leave the path unchanged. `note` (default true) adds a path-repaired note.
6
+ */
7
+ export declare function unicodeRepair(options?: {
8
+ note?: boolean;
9
+ }): PathResolver<unknown>;
@@ -0,0 +1,38 @@
1
+ import { canonicalizeFileName } from "../suggest/canonicalize.js";
2
+ /**
3
+ * Repairs a file name that matches exactly one entry in its directory up to
4
+ * Unicode equivalence. An exact match, several matches, or a failed listing
5
+ * leave the path unchanged. `note` (default true) adds a path-repaired note.
6
+ */
7
+ export function unicodeRepair(options = {}) {
8
+ const withNote = options.note ?? true;
9
+ return Object.freeze({
10
+ id: "unicode-repair",
11
+ async resolve(path, ctx) {
12
+ const dir = ctx.paths.dirname(path);
13
+ const name = ctx.paths.basename(path);
14
+ const listed = await ctx.list(dir);
15
+ if (!listed.ok || listed.entries.some((entry) => entry.name === name)) {
16
+ return { kind: "path", path };
17
+ }
18
+ const canonical = canonicalizeFileName(name);
19
+ const matches = listed.entries.filter((entry) => canonicalizeFileName(entry.name) === canonical);
20
+ const [match] = matches;
21
+ if (matches.length !== 1 || match === undefined)
22
+ return { kind: "path", path };
23
+ const repaired = ctx.paths.join(dir, match.name);
24
+ if (!withNote)
25
+ return { kind: "path", path: repaired };
26
+ return {
27
+ kind: "path",
28
+ path: repaired,
29
+ note: {
30
+ code: "path-repaired",
31
+ severity: "warning",
32
+ message: ctx.messages.pathRepaired({ from: path, to: repaired }),
33
+ data: { from: path, to: repaired },
34
+ },
35
+ };
36
+ },
37
+ });
38
+ }
@@ -0,0 +1,18 @@
1
+ import type { ToolSignature } from "../contract/base.ts";
2
+ import type { ReadInput } from "../contract/input.ts";
3
+ import type { JsonObject } from "../contract/json.ts";
4
+ /** The read tool's signature. `fromInput` lets notes tell the model how to continue. */
5
+ export interface ReadSignature<TModel extends JsonObject = JsonObject> extends ToolSignature<ReadInput, ReadParam> {
6
+ /** Maps a canonical retry to the object the model sends next. */
7
+ fromInput(retry: ReadInput): TModel;
8
+ }
9
+ /** The canonical read parameters, and the parameter names of `defaultReadSignature`. */
10
+ export type ReadParam = "path" | "offset" | "limit";
11
+ /** The parameter names of `lineRangeSignature`. */
12
+ export type LineRangeParam = "path" | "start" | "end";
13
+ /** The model input of `defaultReadSignature`. */
14
+ export type DefaultReadModelInput = {
15
+ readonly path: string;
16
+ readonly offset?: number;
17
+ readonly limit?: number;
18
+ };
File without changes
@@ -0,0 +1,4 @@
1
+ export type { DefaultReadModelInput, LineRangeParam, ReadParam, ReadSignature, } from "./contract.ts";
2
+ export { lineRangeSignature } from "./line-range.ts";
3
+ export { readSignatureMessages } from "./messages.ts";
4
+ export { defaultReadSignature } from "./offset-limit.ts";
@@ -0,0 +1,3 @@
1
+ export { lineRangeSignature } from "./line-range.js";
2
+ export { readSignatureMessages } from "./messages.js";
3
+ export { defaultReadSignature } from "./offset-limit.js";
@@ -0,0 +1,7 @@
1
+ import type { SignatureDocs } from "../contract/base.ts";
2
+ import type { LineRangeParam, ReadSignature } from "./contract.ts";
3
+ /**
4
+ * A read({ path, start?, end? }) signature with one-based, inclusive start and
5
+ * end lines. param("limit") is "", because no parameter holds a line count.
6
+ */
7
+ export declare function lineRangeSignature(options?: SignatureDocs<LineRangeParam>): ReadSignature;
@@ -0,0 +1,59 @@
1
+ import { canonical } from "./offset-limit.js";
2
+ import { DEFAULT_NAME, lineSchema, objectSchema, pathSchema, readLine, readObject, readPath, resolveNames, } from "./schema.js";
3
+ const PARAMS = ["path", "start", "end"];
4
+ /**
5
+ * A read({ path, start?, end? }) signature with one-based, inclusive start and
6
+ * end lines. param("limit") is "", because no parameter holds a line count.
7
+ */
8
+ export function lineRangeSignature(options = {}) {
9
+ if (options === null || typeof options !== "object") {
10
+ throw new TypeError("lineRangeSignature options must be an object");
11
+ }
12
+ const name = resolveNames(PARAMS, options.names);
13
+ const keys = PARAMS.map((param) => name[param]);
14
+ const describe = options.describe ?? {};
15
+ const schema = objectSchema([
16
+ [
17
+ name.path,
18
+ pathSchema(describe.path ??
19
+ "Path to read, relative to the working directory or absolute within an allowed root."),
20
+ ],
21
+ [name.start, lineSchema(describe.start ?? "First one-based line to read. Defaults to 1.")],
22
+ [
23
+ name.end,
24
+ lineSchema(describe.end ??
25
+ `Last line to read, inclusive. Must not be less than ${name.start}. Defaults to the end of the view limits.`),
26
+ ],
27
+ ], name.path);
28
+ return Object.freeze({
29
+ name: options.name ?? DEFAULT_NAME,
30
+ description: options.description ??
31
+ `Read a UTF-8 text file. \`${name.start}\` and \`${name.end}\` are one-based, inclusive ` +
32
+ "source lines. Output is bounded by line, byte, and line-length limits. When the result " +
33
+ "reports truncation, follow the continuation it returns instead of guessing the next " +
34
+ "range. Paths outside the allowed roots, binary content, and special files are refused.",
35
+ schema,
36
+ toInput(input) {
37
+ const record = readObject(input, keys);
38
+ const path = readPath(record[name.path], name.path);
39
+ const start = readLine(record[name.start], name.start);
40
+ const end = readLine(record[name.end], name.end);
41
+ if (end === undefined)
42
+ return canonical(path, start);
43
+ const first = start ?? 1;
44
+ if (end < first) {
45
+ throw new TypeError(`${name.end} (${end}) must not be less than ${name.start} (${first})`);
46
+ }
47
+ return canonical(path, start, end - first + 1);
48
+ },
49
+ param: (canonical) => canonical === "path" ? name.path : canonical === "offset" ? name.start : "",
50
+ fromInput(retry) {
51
+ const model = { [name.path]: retry.path };
52
+ if (retry.offset !== undefined)
53
+ model[name.start] = retry.offset;
54
+ if (retry.limit !== undefined)
55
+ model[name.end] = (retry.offset ?? 1) + retry.limit - 1;
56
+ return model;
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,4 @@
1
+ import type { ReadMessageCatalog } from "../contract/messages.ts";
2
+ import type { ReadSignature } from "./contract.ts";
3
+ /** Retry text in the signature's own names. Merge it under the host's messages. */
4
+ export declare function readSignatureMessages(signature: ReadSignature): Pick<ReadMessageCatalog, "retry">;
@@ -0,0 +1,9 @@
1
+ /** Retry text in the signature's own names. Merge it under the host's messages. */
2
+ export function readSignatureMessages(signature) {
3
+ if (signature === null ||
4
+ typeof signature !== "object" ||
5
+ typeof signature.fromInput !== "function") {
6
+ throw new TypeError("readSignatureMessages needs a ReadSignature");
7
+ }
8
+ return Object.freeze({ retry: (next) => JSON.stringify(signature.fromInput(next)) });
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReadInput } from "../contract/input.ts";
2
+ import type { SignatureDocs } from "../contract/base.ts";
3
+ import type { DefaultReadModelInput, ReadParam, ReadSignature } from "./contract.ts";
4
+ /**
5
+ * A read({ path, offset?, limit? }) signature: a non-blank path without NUL,
6
+ * and positive integer offset and limit. `names` renames any of the three
7
+ * parameters.
8
+ */
9
+ export declare function defaultReadSignature(options?: SignatureDocs<ReadParam> & {
10
+ readonly names?: undefined;
11
+ }): ReadSignature<DefaultReadModelInput>;
12
+ export declare function defaultReadSignature(options: SignatureDocs<ReadParam>): ReadSignature;
13
+ export declare function canonical(path: string, offset?: number, limit?: number): ReadInput;