@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,7 @@
1
+ import type { ReadAuthorizer, ReadAuthorizeTarget, ReadHookContext } from "../contract/extensions.ts";
2
+ /**
3
+ * Asks `prompt` before each file read. Anything but `true`, or a throw, denies.
4
+ * Directory listings are allowed without asking. To remember answers, keep
5
+ * them in ctx.call.host. An abort ends the read even when the prompt ignores it.
6
+ */
7
+ export declare function askUser<THost>(prompt: (target: ReadAuthorizeTarget, ctx: ReadHookContext<THost>) => Promise<boolean>): ReadAuthorizer<THost>;
@@ -0,0 +1,25 @@
1
+ import { ALLOW, deny } from "./shared.js";
2
+ /**
3
+ * Asks `prompt` before each file read. Anything but `true`, or a throw, denies.
4
+ * Directory listings are allowed without asking. To remember answers, keep
5
+ * them in ctx.call.host. An abort ends the read even when the prompt ignores it.
6
+ */
7
+ export function askUser(prompt) {
8
+ if (typeof prompt !== "function")
9
+ throw new TypeError("askUser takes a prompt function");
10
+ return Object.freeze({
11
+ id: "ask-user",
12
+ async authorize(target, ctx) {
13
+ if (target.action !== "read")
14
+ return ALLOW;
15
+ let approved = false;
16
+ try {
17
+ approved = await prompt(target, ctx);
18
+ }
19
+ catch {
20
+ approved = false;
21
+ }
22
+ return approved === true ? ALLOW : deny(ctx, target, "the user did not approve the read");
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReadAuthorizer } from "../contract/extensions.ts";
2
+ /**
3
+ * Runs authorizers left to right. The first deny wins, with that step's note.
4
+ * Allow notes from every step are kept in order. An empty chain allows. A step
5
+ * that throws fails the read with EXTENSION_FAILED, naming the step's id.
6
+ */
7
+ export declare function readAuthorizers<THost = unknown>(...steps: readonly ReadAuthorizer<NoInfer<THost>>[]): ReadAuthorizer<THost>;
@@ -0,0 +1,34 @@
1
+ import { stepFailure } from "../core/extension-error.js";
2
+ /**
3
+ * Runs authorizers left to right. The first deny wins, with that step's note.
4
+ * Allow notes from every step are kept in order. An empty chain allows. A step
5
+ * that throws fails the read with EXTENSION_FAILED, naming the step's id.
6
+ */
7
+ export function readAuthorizers(...steps) {
8
+ for (const step of steps) {
9
+ if (step === null || typeof step !== "object" || typeof step.authorize !== "function") {
10
+ throw new TypeError("readAuthorizers takes authorizers");
11
+ }
12
+ }
13
+ const chain = Object.freeze([...steps]);
14
+ return Object.freeze({
15
+ id: chain.length === 0 ? "allow" : chain.map((step) => step.id).join("+"),
16
+ async authorize(target, ctx) {
17
+ const notes = [];
18
+ for (const step of chain) {
19
+ let decision;
20
+ try {
21
+ decision = await step.authorize(target, ctx);
22
+ if (!decision.allow)
23
+ return decision;
24
+ }
25
+ catch (error) {
26
+ throw stepFailure(step, error);
27
+ }
28
+ if (decision.notes !== undefined)
29
+ notes.push(...decision.notes);
30
+ }
31
+ return notes.length === 0 ? { allow: true } : { allow: true, notes };
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,7 @@
1
+ import type { ToolAuthorizer } from "../contract/base.ts";
2
+ /**
3
+ * Denies any action, in any tool, on a resolvedPath that matches one of the
4
+ * globs ("**", "*", "?"). For a read the resolvedPath is the realpath, so a
5
+ * symlink to a denied file is denied.
6
+ */
7
+ export declare function denyPaths(patterns: readonly string[]): ToolAuthorizer<unknown>;
@@ -0,0 +1,25 @@
1
+ import { compileGlob } from "./glob.js";
2
+ import { ALLOW, deny } from "./shared.js";
3
+ /**
4
+ * Denies any action, in any tool, on a resolvedPath that matches one of the
5
+ * globs ("**", "*", "?"). For a read the resolvedPath is the realpath, so a
6
+ * symlink to a denied file is denied.
7
+ */
8
+ export function denyPaths(patterns) {
9
+ if (!Array.isArray(patterns) ||
10
+ !patterns.every((pattern) => typeof pattern === "string" && pattern !== "")) {
11
+ throw new TypeError("denyPaths takes an array of non-empty glob strings");
12
+ }
13
+ const compiled = patterns.map((pattern) => ({ pattern, test: compileGlob(pattern) }));
14
+ return Object.freeze({
15
+ id: "deny-paths",
16
+ authorize(target, ctx) {
17
+ const hit = compiled.find((entry) => entry.test(target.resolvedPath));
18
+ if (hit === undefined)
19
+ return ALLOW;
20
+ return deny(ctx, target, "the path matches a denied pattern", {
21
+ data: { pattern: hit.pattern },
22
+ });
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Compiles a glob into a matcher for "/"-separated paths. A "**" segment
3
+ * matches zero or more whole segments. "*" matches any run of characters within
4
+ * a segment, and "?" one character. Dot names get no special case. Every other
5
+ * character matches itself. Matching time is linear in pattern and path length.
6
+ */
7
+ export declare function compileGlob(pattern: string): (path: string) => boolean;
@@ -0,0 +1,46 @@
1
+ const GLOBSTAR = Symbol("globstar");
2
+ /**
3
+ * Compiles a glob into a matcher for "/"-separated paths. A "**" segment
4
+ * matches zero or more whole segments. "*" matches any run of characters within
5
+ * a segment, and "?" one character. Dot names get no special case. Every other
6
+ * character matches itself. Matching time is linear in pattern and path length.
7
+ */
8
+ export function compileGlob(pattern) {
9
+ const segments = pattern
10
+ .split("/")
11
+ .map((segment) => (segment === "**" ? GLOBSTAR : segmentPattern(segment)));
12
+ return (path) => matches(segments, path.split("/"));
13
+ }
14
+ function segmentPattern(segment) {
15
+ let source = "";
16
+ for (const char of segment) {
17
+ if (char === "*")
18
+ source += "[^/]*";
19
+ else if (char === "?")
20
+ source += "[^/]";
21
+ else
22
+ source += char.replace(/[\\^$.|+()[\]{}]/g, "\\$&");
23
+ }
24
+ return new RegExp(`^${source}$`, "u");
25
+ }
26
+ function matches(segments, parts) {
27
+ let segmentsMatchFirstParts = parts.map(() => false).concat(false);
28
+ segmentsMatchFirstParts[0] = true;
29
+ for (const segment of segments) {
30
+ const next = segmentsMatchFirstParts.map(() => false);
31
+ if (segment === GLOBSTAR) {
32
+ let anyEarlierMatched = false;
33
+ for (let j = 0; j <= parts.length; j += 1) {
34
+ anyEarlierMatched ||= segmentsMatchFirstParts[j] === true;
35
+ next[j] = anyEarlierMatched;
36
+ }
37
+ }
38
+ else {
39
+ for (let j = 1; j <= parts.length; j += 1) {
40
+ next[j] = segmentsMatchFirstParts[j - 1] === true && segment.test(parts[j - 1] ?? "");
41
+ }
42
+ }
43
+ segmentsMatchFirstParts = next;
44
+ }
45
+ return segmentsMatchFirstParts[parts.length] === true;
46
+ }
@@ -0,0 +1,5 @@
1
+ export { askUser } from "./ask-user.ts";
2
+ export { readAuthorizers } from "./authorizers.ts";
3
+ export { denyPaths } from "./deny-paths.ts";
4
+ export { compileGlob } from "./glob.ts";
5
+ export { sizeCeiling } from "./size-ceiling.ts";
@@ -0,0 +1,5 @@
1
+ export { askUser } from "./ask-user.js";
2
+ export { readAuthorizers } from "./authorizers.js";
3
+ export { denyPaths } from "./deny-paths.js";
4
+ export { compileGlob } from "./glob.js";
5
+ export { sizeCeiling } from "./size-ceiling.js";
@@ -0,0 +1,9 @@
1
+ import type { AccessTarget, ToolHookContext } from "../contract/base.ts";
2
+ import type { ReadAuthorizeDecision } from "../contract/extensions.ts";
3
+ import type { JsonObject } from "../contract/json.ts";
4
+ import type { ReadInput } from "../contract/input.ts";
5
+ export declare const ALLOW: ReadAuthorizeDecision;
6
+ export declare function deny(ctx: ToolHookContext<unknown>, target: AccessTarget, detail: string, extra?: {
7
+ readonly data?: JsonObject;
8
+ readonly retry?: ReadInput;
9
+ }): ReadAuthorizeDecision;
@@ -0,0 +1,12 @@
1
+ export const ALLOW = Object.freeze({ allow: true });
2
+ export function deny(ctx, target, detail, extra = {}) {
3
+ return {
4
+ allow: false,
5
+ note: {
6
+ code: "denied",
7
+ severity: "warning",
8
+ message: ctx.messages.denied({ path: target.requestedPath, detail }),
9
+ ...extra,
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReadAuthorizer } from "../contract/extensions.ts";
2
+ /**
3
+ * Denies reading a file larger than `maxBytes`. With `unrangedOnly`, denies
4
+ * only whole-file reads, and the note offers a ranged retry. Unknown sizes and
5
+ * directory listings are allowed.
6
+ */
7
+ export declare function sizeCeiling(options: {
8
+ maxBytes: number;
9
+ unrangedOnly?: boolean;
10
+ }): ReadAuthorizer<unknown>;
@@ -0,0 +1,34 @@
1
+ import { ALLOW, deny } from "./shared.js";
2
+ /**
3
+ * Denies reading a file larger than `maxBytes`. With `unrangedOnly`, denies
4
+ * only whole-file reads, and the note offers a ranged retry. Unknown sizes and
5
+ * directory listings are allowed.
6
+ */
7
+ export function sizeCeiling(options) {
8
+ const { maxBytes, unrangedOnly = false } = options ?? {};
9
+ if (typeof maxBytes !== "number" || !Number.isSafeInteger(maxBytes) || maxBytes < 0) {
10
+ throw new TypeError("sizeCeiling maxBytes must be a non-negative safe integer");
11
+ }
12
+ if (typeof unrangedOnly !== "boolean") {
13
+ throw new TypeError("sizeCeiling unrangedOnly must be a boolean");
14
+ }
15
+ return Object.freeze({
16
+ id: "size-ceiling",
17
+ authorize(target, ctx) {
18
+ if (target.action !== "read" || target.size === null || target.size <= maxBytes) {
19
+ return ALLOW;
20
+ }
21
+ const data = { size: target.size, maxBytes };
22
+ if (!unrangedOnly) {
23
+ return deny(ctx, target, `${target.size} bytes is over the ${maxBytes}-byte ceiling`, {
24
+ data,
25
+ });
26
+ }
27
+ if (ctx.request.ranged)
28
+ return ALLOW;
29
+ const { path, offset, limit } = ctx.request;
30
+ const retry = { path, offset, limit };
31
+ return deny(ctx, target, `${target.size} bytes is over the ${maxBytes}-byte ceiling for a whole-file read; a ranged read such as ${ctx.messages.retry(retry)} is allowed`, { data, retry });
32
+ },
33
+ });
34
+ }
@@ -0,0 +1,9 @@
1
+ import type { ViewBudget } from "../contract/extensions.ts";
2
+ export interface CharsPerTokenOptions {
3
+ /** Characters per token. A positive number, for example 4. */
4
+ ratio: number;
5
+ /** Tokens in one view. A positive number. */
6
+ max: number;
7
+ }
8
+ /** Estimates tokens without a tokenizer: ceil(text.length / ratio) + 1 per line. */
9
+ export declare function charsPerToken(options: CharsPerTokenOptions): ViewBudget;
@@ -0,0 +1,15 @@
1
+ /** Estimates tokens without a tokenizer: ceil(text.length / ratio) + 1 per line. */
2
+ export function charsPerToken(options) {
3
+ const { ratio, max } = options ?? {};
4
+ if (typeof ratio !== "number" || !Number.isFinite(ratio) || ratio <= 0) {
5
+ throw new TypeError("charsPerToken ratio must be a positive number");
6
+ }
7
+ if (typeof max !== "number" || !Number.isFinite(max) || max <= 0) {
8
+ throw new TypeError("charsPerToken max must be a positive number");
9
+ }
10
+ return Object.freeze({
11
+ id: "chars-per-token",
12
+ max,
13
+ measure: (text) => Math.ceil(text.length / ratio) + 1,
14
+ });
15
+ }
@@ -0,0 +1,2 @@
1
+ export { charsPerToken } from "./chars-per-token.ts";
2
+ export type { CharsPerTokenOptions } from "./chars-per-token.ts";
@@ -0,0 +1 @@
1
+ export { charsPerToken } from "./chars-per-token.js";
@@ -0,0 +1,5 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ /** Refuses zip archives and samples that contain a NUL byte. */
3
+ export declare function binaryClassifier(options?: {
4
+ note?: NoteOverride;
5
+ }): Classifier;
@@ -0,0 +1,13 @@
1
+ import { classifier, isZip, unsupported } from "./shared.js";
2
+ /** Refuses zip archives and samples that contain a NUL byte. */
3
+ export function binaryClassifier(options = {}) {
4
+ return classifier("binary", (sample) => {
5
+ const suggestedTool = "a format-specific byte tool";
6
+ if (isZip(sample.bytes)) {
7
+ return unsupported("BINARY", "application/zip", ["zip-magic"], sample, options.note, suggestedTool);
8
+ }
9
+ if (!sample.bytes.includes(0))
10
+ return null;
11
+ return unsupported("BINARY", null, ["nul-byte"], sample, options.note, suggestedTool);
12
+ });
13
+ }
@@ -0,0 +1,17 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ export interface ExtensionClassifierOptions {
3
+ /** Keyed by extension, with or without the leading dot. Case-insensitive. */
4
+ readonly unsupported?: Readonly<Record<string, {
5
+ code: string;
6
+ mimeType?: string;
7
+ note?: NoteOverride;
8
+ }>>;
9
+ /** Extensions forced to text. */
10
+ readonly text?: readonly string[];
11
+ }
12
+ /**
13
+ * Classifies by file extension alone, as Deep Agents and Mastra do. Place it
14
+ * before defaultClassifiers(). Its confidence is "medium", and an extension it
15
+ * does not know gets no opinion.
16
+ */
17
+ export declare function extensionClassifier(options: ExtensionClassifierOptions): Classifier;
@@ -0,0 +1,45 @@
1
+ import { classifier, refusal } from "./shared.js";
2
+ /**
3
+ * Classifies by file extension alone, as Deep Agents and Mastra do. Place it
4
+ * before defaultClassifiers(). Its confidence is "medium", and an extension it
5
+ * does not know gets no opinion.
6
+ */
7
+ export function extensionClassifier(options) {
8
+ const unsupported = new Map(Object.entries(options.unsupported ?? {}).map(([key, value]) => [normalize(key), value]));
9
+ const text = new Set((options.text ?? []).map(normalize));
10
+ return classifier("extension", (sample) => {
11
+ const extension = extensionOf(sample.path);
12
+ if (extension === null)
13
+ return null;
14
+ const refused = unsupported.get(extension);
15
+ if (refused !== undefined) {
16
+ return refusal({
17
+ code: refused.code,
18
+ mimeType: refused.mimeType ?? null,
19
+ confidence: "medium",
20
+ reasons: [`extension:${extension}`],
21
+ sample,
22
+ override: refused.note,
23
+ message: `Files with the .${extension} extension are not supported as UTF-8 text.`,
24
+ });
25
+ }
26
+ if (!text.has(extension))
27
+ return null;
28
+ return {
29
+ kind: "text",
30
+ mimeType: sample.mimeType ?? "text/plain",
31
+ confidence: "medium",
32
+ reasons: [`extension:${extension}`],
33
+ };
34
+ });
35
+ }
36
+ function normalize(extension) {
37
+ return extension.replace(/^\./u, "").toLowerCase();
38
+ }
39
+ function extensionOf(path) {
40
+ const name = path.slice(path.lastIndexOf("/") + 1);
41
+ const index = name.lastIndexOf(".");
42
+ if (index <= 0 || index === name.length - 1)
43
+ return null;
44
+ return name.slice(index + 1).toLowerCase();
45
+ }
@@ -0,0 +1,5 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ /** Refuses PNG, JPEG, GIF, and WebP files, detected by their magic bytes. */
3
+ export declare function imageClassifier(options?: {
4
+ note?: NoteOverride;
5
+ }): Classifier;
@@ -0,0 +1,22 @@
1
+ import { asciiProjection, classifier, startsWith, startsWithAscii, unsupported } from "./shared.js";
2
+ /** Refuses PNG, JPEG, GIF, and WebP files, detected by their magic bytes. */
3
+ export function imageClassifier(options = {}) {
4
+ return classifier("image", (sample) => {
5
+ const mimeType = imageSignature(sample.bytes);
6
+ if (mimeType === null)
7
+ return null;
8
+ return unsupported("IMAGE", mimeType, ["image-magic"], sample, options.note, "an image or vision tool");
9
+ });
10
+ }
11
+ function imageSignature(bytes) {
12
+ if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
13
+ return "image/png";
14
+ if (startsWith(bytes, [0xff, 0xd8, 0xff]))
15
+ return "image/jpeg";
16
+ if (startsWithAscii(bytes, "GIF87a") || startsWithAscii(bytes, "GIF89a"))
17
+ return "image/gif";
18
+ if (startsWithAscii(bytes, "RIFF") && asciiProjection(bytes.subarray(8, 12)) === "WEBP") {
19
+ return "image/webp";
20
+ }
21
+ return null;
22
+ }
@@ -0,0 +1,16 @@
1
+ import type { Classifier, DefaultClassifierOptions } from "../contract/classify.ts";
2
+ export { binaryClassifier } from "./binary.ts";
3
+ export { extensionClassifier } from "./extension.ts";
4
+ export type { ExtensionClassifierOptions } from "./extension.ts";
5
+ export { imageClassifier } from "./image.ts";
6
+ export { notebookClassifier } from "./notebook.ts";
7
+ export { officeClassifier } from "./office.ts";
8
+ export { pdfClassifier } from "./pdf.ts";
9
+ export { utf8Classifier } from "./utf8.ts";
10
+ /**
11
+ * The built-in chain in evaluation order: image, pdf, office, binary, utf8.
12
+ * The first classifier with an opinion wins. utf8Classifier runs last and is
13
+ * the one that returns text. A notebook is JSON text here. Put
14
+ * notebookClassifier() before the chain to refuse or convert notebooks.
15
+ */
16
+ export declare function defaultClassifiers(options?: DefaultClassifierOptions): Classifier[];
@@ -0,0 +1,31 @@
1
+ import { binaryClassifier } from "./binary.js";
2
+ import { imageClassifier } from "./image.js";
3
+ import { officeClassifier } from "./office.js";
4
+ import { pdfClassifier } from "./pdf.js";
5
+ import { utf8Classifier } from "./utf8.js";
6
+ export { binaryClassifier } from "./binary.js";
7
+ export { extensionClassifier } from "./extension.js";
8
+ export { imageClassifier } from "./image.js";
9
+ export { notebookClassifier } from "./notebook.js";
10
+ export { officeClassifier } from "./office.js";
11
+ export { pdfClassifier } from "./pdf.js";
12
+ export { utf8Classifier } from "./utf8.js";
13
+ /**
14
+ * The built-in chain in evaluation order: image, pdf, office, binary, utf8.
15
+ * The first classifier with an opinion wins. utf8Classifier runs last and is
16
+ * the one that returns text. A notebook is JSON text here. Put
17
+ * notebookClassifier() before the chain to refuse or convert notebooks.
18
+ */
19
+ export function defaultClassifiers(options = {}) {
20
+ const notes = options.notes ?? {};
21
+ return [
22
+ imageClassifier(withNote(notes.IMAGE)),
23
+ pdfClassifier(withNote(notes.PDF)),
24
+ officeClassifier(withNote(notes.OFFICE_DOCUMENT)),
25
+ binaryClassifier(withNote(notes.BINARY)),
26
+ utf8Classifier(withNote(notes.UNKNOWN_ENCODING)),
27
+ ];
28
+ }
29
+ function withNote(note) {
30
+ return note === undefined ? {} : { note };
31
+ }
@@ -0,0 +1,5 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ /** Refuses Jupyter notebooks: JSON with nbformat and a cells array, plus an .ipynb name or metadata. */
3
+ export declare function notebookClassifier(options?: {
4
+ note?: NoteOverride;
5
+ }): Classifier;
@@ -0,0 +1,14 @@
1
+ import { classifier, unsupported } from "./shared.js";
2
+ /** Refuses Jupyter notebooks: JSON with nbformat and a cells array, plus an .ipynb name or metadata. */
3
+ export function notebookClassifier(options = {}) {
4
+ return classifier("notebook", (sample) => {
5
+ const text = new TextDecoder("utf-8").decode(sample.bytes.subarray(0, 8_192));
6
+ const evidence = /^\s*\{/u.test(text) && /"nbformat"\s*:/u.test(text) && /"cells"\s*:\s*\[/u.test(text);
7
+ if (!evidence)
8
+ return null;
9
+ if (!sample.path.toLowerCase().endsWith(".ipynb") && !/"metadata"\s*:/u.test(text)) {
10
+ return null;
11
+ }
12
+ return unsupported("NOTEBOOK", "application/x-ipynb+json", ["notebook-json-evidence"], sample, options.note, "a notebook handler");
13
+ });
14
+ }
@@ -0,0 +1,5 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ /** Refuses Office documents: a zip with an Office marker or an Office extension. */
3
+ export declare function officeClassifier(options?: {
4
+ note?: NoteOverride;
5
+ }): Classifier;
@@ -0,0 +1,30 @@
1
+ import { asciiProjection, classifier, isZip, unsupported } from "./shared.js";
2
+ /** Refuses Office documents: a zip with an Office marker or an Office extension. */
3
+ export function officeClassifier(options = {}) {
4
+ return classifier("office", (sample) => {
5
+ if (!isZip(sample.bytes))
6
+ return null;
7
+ const path = sample.path.toLowerCase();
8
+ const marker = /\[Content_Types\]\.xml|word\/|xl\/|ppt\//u.test(asciiProjection(sample.bytes));
9
+ const extension = /\.(docx|xlsx|pptx)$/u.test(path);
10
+ if (!marker && !extension)
11
+ return null;
12
+ const reasons = [
13
+ ...(marker ? ["office-zip-marker"] : []),
14
+ ...(extension ? ["office-extension"] : []),
15
+ ];
16
+ return unsupported("OFFICE_DOCUMENT", officeMime(path), reasons, sample, options.note, "an office document handler");
17
+ });
18
+ }
19
+ function officeMime(path) {
20
+ if (path.endsWith(".docx")) {
21
+ return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
22
+ }
23
+ if (path.endsWith(".xlsx")) {
24
+ return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
25
+ }
26
+ if (path.endsWith(".pptx")) {
27
+ return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
28
+ }
29
+ return "application/vnd.openxmlformats-officedocument";
30
+ }
@@ -0,0 +1,4 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
2
+ export declare function pdfClassifier(options?: {
3
+ note?: NoteOverride;
4
+ }): Classifier;
@@ -0,0 +1,8 @@
1
+ import { classifier, startsWithAscii, unsupported } from "./shared.js";
2
+ export function pdfClassifier(options = {}) {
3
+ return classifier("pdf", (sample) => {
4
+ if (!startsWithAscii(sample.bytes, "%PDF-"))
5
+ return null;
6
+ return unsupported("PDF", "application/pdf", ["pdf-magic"], sample, options.note, "a PDF extraction tool");
7
+ });
8
+ }
@@ -0,0 +1,17 @@
1
+ import type { BuiltInUnsupportedCode, Classification, ClassificationSample, Classifier, NoteOverride, UnsupportedClassification } from "../contract/classify.ts";
2
+ import type { Confidence } from "../contract/result.ts";
3
+ export declare function classifier(id: string, classify: (sample: ClassificationSample) => Classification | null): Classifier;
4
+ export declare function unsupported(code: BuiltInUnsupportedCode, mimeType: string | null, reasons: readonly string[], sample: ClassificationSample, override: NoteOverride | undefined, suggestedTool: string): UnsupportedClassification;
5
+ export declare function refusal(options: {
6
+ code: string;
7
+ mimeType: string | null;
8
+ confidence: Confidence;
9
+ reasons: readonly string[];
10
+ sample: ClassificationSample;
11
+ override: NoteOverride | undefined;
12
+ message: string;
13
+ }): UnsupportedClassification;
14
+ export declare function startsWith(bytes: Uint8Array, prefix: readonly number[]): boolean;
15
+ export declare function startsWithAscii(bytes: Uint8Array, prefix: string): boolean;
16
+ export declare function asciiProjection(bytes: Uint8Array): string;
17
+ export declare function isZip(bytes: Uint8Array): boolean;
@@ -0,0 +1,57 @@
1
+ export function classifier(id, classify) {
2
+ return Object.freeze({ id, classify });
3
+ }
4
+ export function unsupported(code, mimeType, reasons, sample, override, suggestedTool) {
5
+ return refusal({
6
+ code,
7
+ mimeType,
8
+ confidence: "high",
9
+ reasons,
10
+ sample,
11
+ override,
12
+ message: `This content is not supported as UTF-8 text. Use ${suggestedTool}.`,
13
+ });
14
+ }
15
+ export function refusal(options) {
16
+ const { code, mimeType, confidence, reasons, sample, override } = options;
17
+ const base = {
18
+ code: `unsupported-${code.toLowerCase().replaceAll("_", "-")}`,
19
+ severity: "warning",
20
+ message: options.message,
21
+ };
22
+ return {
23
+ kind: "unsupported",
24
+ code: override?.code ?? code,
25
+ mimeType,
26
+ confidence,
27
+ reasons,
28
+ note: applyOverride(base, override, { path: sample.path, mimeType, reasons }),
29
+ };
30
+ }
31
+ function applyOverride(note, override, context) {
32
+ if (override === undefined)
33
+ return note;
34
+ const message = typeof override.message === "function" ? override.message(context) : override.message;
35
+ return {
36
+ code: override.code ?? note.code,
37
+ severity: override.severity ?? note.severity,
38
+ message: message ?? note.message,
39
+ ...(override.data === undefined ? {} : { data: override.data }),
40
+ };
41
+ }
42
+ export function startsWith(bytes, prefix) {
43
+ return prefix.every((byte, index) => bytes[index] === byte);
44
+ }
45
+ export function startsWithAscii(bytes, prefix) {
46
+ return startsWith(bytes, [...prefix].map((character) => character.charCodeAt(0)));
47
+ }
48
+ export function asciiProjection(bytes) {
49
+ let result = "";
50
+ for (const byte of bytes) {
51
+ result += byte >= 0x20 && byte <= 0x7e ? String.fromCharCode(byte) : " ";
52
+ }
53
+ return result;
54
+ }
55
+ export function isZip(bytes) {
56
+ return startsWith(bytes, [0x50, 0x4b, 0x03, 0x04]);
57
+ }
@@ -0,0 +1,8 @@
1
+ import type { Classifier, NoteOverride } from "../contract/classify.ts";
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 declare function utf8Classifier(options?: {
7
+ note?: NoteOverride;
8
+ }): Classifier;