functionalscript 0.33.0 → 0.34.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 (70) hide show
  1. package/README.md +1 -1
  2. package/fs/asserts/module.f.d.ts +1 -1
  3. package/fs/asserts/module.f.js +4 -1
  4. package/fs/bnf/data/module.f.d.ts +0 -103
  5. package/fs/bnf/data/module.f.js +10 -246
  6. package/fs/bnf/data/proof.f.d.ts +0 -10
  7. package/fs/bnf/data/proof.f.js +2 -642
  8. package/fs/bnf/descent/module.f.d.ts +56 -0
  9. package/fs/bnf/descent/module.f.js +119 -0
  10. package/fs/bnf/descent/proof.f.d.ts +5 -0
  11. package/fs/bnf/descent/proof.f.js +296 -0
  12. package/fs/bnf/ll1/module.f.d.ts +72 -0
  13. package/fs/bnf/ll1/module.f.js +153 -0
  14. package/fs/bnf/ll1/proof.f.d.ts +9 -0
  15. package/fs/bnf/ll1/proof.f.js +358 -0
  16. package/fs/cas/cli/module.f.d.ts +6 -0
  17. package/fs/cas/cli/module.f.js +56 -0
  18. package/fs/cas/cli/proof.f.d.ts +14 -0
  19. package/fs/cas/cli/proof.f.js +149 -0
  20. package/fs/cas/mcp/module.f.d.ts +9 -17
  21. package/fs/cas/mcp/module.f.js +116 -84
  22. package/fs/cas/mcp/proof.f.d.ts +5 -1
  23. package/fs/cas/mcp/proof.f.js +74 -32
  24. package/fs/cas/module.f.d.ts +35 -15
  25. package/fs/cas/module.f.js +168 -131
  26. package/fs/cas/proof.f.d.ts +16 -13
  27. package/fs/cas/proof.f.js +255 -121
  28. package/fs/ci/config/module.f.d.ts +14 -14
  29. package/fs/ci/config/module.f.js +14 -14
  30. package/fs/djs/tokenizer-new/module.f.js +1 -1
  31. package/fs/djs/tokenizer-new/proof.f.d.ts +2 -0
  32. package/fs/djs/tokenizer-new/proof.f.js +357 -229
  33. package/fs/effects/list/module.f.d.ts +31 -0
  34. package/fs/effects/list/module.f.js +16 -0
  35. package/fs/effects/module.f.d.ts +11 -5
  36. package/fs/effects/module.f.js +16 -6
  37. package/fs/effects/node/module.f.d.ts +29 -3
  38. package/fs/effects/node/module.f.js +35 -1
  39. package/fs/effects/node/module.js +32 -18
  40. package/fs/effects/node/virtual/module.f.js +57 -0
  41. package/fs/effects/proof.f.d.ts +4 -0
  42. package/fs/effects/proof.f.js +18 -1
  43. package/fs/fjs/module.f.js +2 -8
  44. package/fs/html/module.f.js +6 -8
  45. package/fs/mcp/module.f.d.ts +1 -1
  46. package/fs/mime/module.f.d.ts +101 -5
  47. package/fs/mime/module.f.js +159 -6
  48. package/fs/mime/proof.f.d.ts +25 -0
  49. package/fs/mime/proof.f.js +193 -3
  50. package/fs/text/code_point/module.f.d.ts +39 -2
  51. package/fs/text/code_point/module.f.js +74 -2
  52. package/fs/text/code_point/proof.f.d.ts +8 -0
  53. package/fs/text/code_point/proof.f.js +72 -0
  54. package/fs/text/utf16/module.f.js +4 -26
  55. package/fs/text/utf8/module.f.d.ts +27 -0
  56. package/fs/text/utf8/module.f.js +8 -14
  57. package/fs/types/function/operator/module.f.d.ts +26 -0
  58. package/fs/types/list/module.f.d.ts +3 -0
  59. package/fs/types/result/module.d.ts +2 -4
  60. package/fs/types/result/module.js +9 -3
  61. package/package.json +3 -3
  62. package/issues/demo/data/data.f.js +0 -12
  63. package/issues/demo/data/shared.f.js +0 -3
  64. package/issues/demo/fs/app.js +0 -4
  65. package/issues/demo/fs/math.f.js +0 -4
  66. package/issues/demo/sample/proof.f.js +0 -13
  67. /package/{issues → todo}/031-json.f.d.ts +0 -0
  68. /package/{issues → todo}/031-json.f.js +0 -0
  69. /package/{issues → todo}/proof.f.d.ts +0 -0
  70. /package/{issues → todo}/proof.f.js +0 -0
@@ -0,0 +1,149 @@
1
+ import { commands } from "./module.f.js";
2
+ import { computeSync, sha256 } from "../../crypto/sha2/module.f.js";
3
+ import { maxLength, vec, vec8 } from "../../types/bit_vec/module.f.js";
4
+ import { defaultNodeProgramOptions, emptyState, virtual } from "../../effects/node/virtual/module.f.js";
5
+ import {} from "../../effects/node/module.f.js";
6
+ import { dispatch } from "../../cli/module.f.js";
7
+ import { vecToCBase32 } from "../../cbase32/module.f.js";
8
+ import { assert, assertEq } from "../../asserts/module.f.js";
9
+ const makeOptions = (args) => ({ ...defaultNodeProgramOptions, args });
10
+ const main = dispatch(commands);
11
+ export const proof = {
12
+ mainAdd: () => {
13
+ const content = vec8(0x2an);
14
+ const state = { ...emptyState, root: { myfile: [content] } };
15
+ const [finalState, exitCode] = virtual(state)(main(makeOptions(['add', 'myfile'])));
16
+ if (exitCode !== 0) {
17
+ throw ['expected exit 0', exitCode];
18
+ }
19
+ if (finalState.stdout.length === 0) {
20
+ throw 'expected hash in stdout';
21
+ }
22
+ },
23
+ mainAddGetBig: () => {
24
+ const chunk = vec(maxLength)(1n);
25
+ const content = [chunk, chunk];
26
+ const state = { ...emptyState, root: { myfile: content } };
27
+ //
28
+ const [finalState, exitCode] = virtual(state)(main(makeOptions(['add', 'myfile'])));
29
+ assertEq(exitCode, 0);
30
+ const stdout = finalState.stdout;
31
+ assert(stdout.length !== 0);
32
+ //
33
+ const h = computeSync(sha256)(content);
34
+ const hs = vecToCBase32(h);
35
+ assertEq(stdout, `${hs}\n`);
36
+ //
37
+ const [finalState2, exitCode2] = virtual(finalState)(main(makeOptions(['get', hs, 'myfile2'])));
38
+ // console.log(finalState2.stderr)
39
+ assertEq(exitCode2, 0, 'e2');
40
+ const { myfile2 } = finalState2.root;
41
+ assert(myfile2 instanceof Array);
42
+ const h2 = computeSync(sha256)(myfile2);
43
+ assertEq(h, h2, 'h');
44
+ },
45
+ mainAddWrongArgs: () => {
46
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['add'])));
47
+ assertEq(exitCode, 1);
48
+ assert(finalState.stderr.length !== 0);
49
+ },
50
+ mainGetFound: () => {
51
+ const content = vec8(0x2an);
52
+ const state = { ...emptyState, root: { myfile: [content] } };
53
+ const [state1, exitCode1] = virtual(state)(main(makeOptions(['add', 'myfile'])));
54
+ if (exitCode1 !== 0) {
55
+ throw ['expected add exit 0', exitCode1];
56
+ }
57
+ const hashStr = state1.stdout.trim();
58
+ const [, exitCode2] = virtual(state1)(main(makeOptions(['get', hashStr, 'output'])));
59
+ if (exitCode2 !== 0) {
60
+ throw ['expected get exit 0', exitCode2];
61
+ }
62
+ },
63
+ mainGetNotFound: () => {
64
+ // valid cBase32 hash that has not been stored
65
+ const content = vec8(0x2an);
66
+ const state = { ...emptyState, root: { myfile: [content] } };
67
+ const [state1] = virtual(state)(main(makeOptions(['add', 'myfile'])));
68
+ const hashStr = state1.stdout.trim();
69
+ // use an empty store so the hash is not found
70
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['get', hashStr, 'output'])));
71
+ if (exitCode !== 1) {
72
+ throw ['expected exit 1', exitCode];
73
+ }
74
+ if (finalState.stderr.length === 0) {
75
+ throw 'expected error in stderr';
76
+ }
77
+ },
78
+ mainGetWrongArgs: () => {
79
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['get'])));
80
+ if (exitCode !== 1) {
81
+ throw ['expected exit 1', exitCode];
82
+ }
83
+ if (finalState.stderr.length === 0) {
84
+ throw 'expected error in stderr';
85
+ }
86
+ },
87
+ mainGetInvalidHash: () => {
88
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['get', 'not-a-valid-hash', 'output'])));
89
+ if (exitCode !== 1) {
90
+ throw ['expected exit 1', exitCode];
91
+ }
92
+ if (finalState.stderr.length === 0) {
93
+ throw 'expected error in stderr';
94
+ }
95
+ },
96
+ mainList: () => {
97
+ const content = vec8(0x2an);
98
+ const state = { ...emptyState, root: { myfile: [content] } };
99
+ const [state1] = virtual(state)(main(makeOptions(['add', 'myfile'])));
100
+ const [, exitCode] = virtual(state1)(main(makeOptions(['list'])));
101
+ if (exitCode !== 0) {
102
+ throw ['expected exit 0', exitCode];
103
+ }
104
+ },
105
+ mainListEmptyStore: () => {
106
+ // A fresh directory has no `.cas` yet; listing must succeed (empty),
107
+ // not crash unwrapping a readdir ENOENT.
108
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['list'])));
109
+ if (exitCode !== 0) {
110
+ throw ['expected exit 0', exitCode];
111
+ }
112
+ if (finalState.stdout !== '') {
113
+ throw ['expected empty stdout', finalState.stdout];
114
+ }
115
+ },
116
+ mainListCorruptStore: () => {
117
+ // `.cas` exists but is a file, not a directory: a real storage error
118
+ // that must surface, not be masked as an empty list.
119
+ const state = { ...emptyState, root: { '.cas': [vec8(0x2an)] } };
120
+ let threw = false;
121
+ try {
122
+ virtual(state)(main(makeOptions(['list'])));
123
+ }
124
+ catch {
125
+ threw = true;
126
+ }
127
+ if (!threw) {
128
+ throw 'expected list to surface the storage error';
129
+ }
130
+ },
131
+ mainNoCmd: () => {
132
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions([])));
133
+ if (exitCode !== 1) {
134
+ throw ['expected exit 1', exitCode];
135
+ }
136
+ if (finalState.stderr.length === 0) {
137
+ throw 'expected error in stderr';
138
+ }
139
+ },
140
+ mainUnknownCmd: () => {
141
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['bogus'])));
142
+ if (exitCode !== 1) {
143
+ throw ['expected exit 1', exitCode];
144
+ }
145
+ if (finalState.stderr.length === 0) {
146
+ throw 'expected error in stderr';
147
+ }
148
+ },
149
+ };
@@ -1,13 +1,12 @@
1
- import { type Effect, type Operation } from '../../effects/module.f.ts';
1
+ import { type Effect } from '../../effects/module.f.ts';
2
2
  import { type MemOp } from '../../effects/memory/module.f.ts';
3
- import { type Vec } from '../../types/bit_vec/module.f.ts';
4
- import { type Mkdir, type RandomInt, type Read, type ReadBytes, type Rename, type Write } from '../../effects/node/module.f.ts';
3
+ import { type Read, type Rm, type Write } from '../../effects/node/module.f.ts';
5
4
  import { type McpConfig, type McpHandlers } from '../../mcp/module.f.ts';
6
- import { type Cas } from '../module.f.ts';
5
+ import { type FileCasOperation } from '../module.f.ts';
7
6
  /** Arguments for `cas_add`: content to store, with optional encoding type. */
8
7
  export declare const casAddArgs: {
9
8
  readonly content: import("../../types/rtti/module.f.ts").String;
10
- readonly type: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Or<[import("../../types/rtti/module.f.ts").Or<["text", "base64"]>, "url"]>, undefined]>;
9
+ readonly type: import("../../types/rtti/module.f.ts").Or<["text", "base64", "url", undefined]>;
11
10
  };
12
11
  /** Arguments for `cas_get`: the cBase32 hash to look up; optionally request inline content. */
13
12
  export declare const casGetArgs: {
@@ -17,24 +16,17 @@ export declare const casGetArgs: {
17
16
  /** Arguments for `cas_list`: none. */
18
17
  export declare const casListArgs: {};
19
18
  /**
20
- * MCP handlers for an injected `Cas<O>` — generic in `O` exactly like `Cas`
21
- * itself, so the same handlers run over `Fs` (production) or memory (tests).
22
- *
23
- * When `toUrl` is provided, `cas_get` includes the `url` field pointing to
24
- * the blob on the local filesystem. When absent (e.g. memory-backed tests),
25
- * `url` is omitted.
19
+ * MCP handlers for `FileCas`.
26
20
  */
27
- export declare const casMcpHandlers: <O extends Operation>(c: Cas<O>, home: string, toUrl?: (hash: Vec) => string) => McpHandlers<Mkdir | Rename | RandomInt | ReadBytes | O>;
21
+ export declare const casMcpHandlers: (home: string) => McpHandlers<FileCasOperation | Rm>;
28
22
  /**
29
23
  * Static MCP configuration for the CAS server: advertises the `tools`
30
24
  * capability, identifies the server, and pins the protocol version.
31
25
  */
32
26
  export declare const casConfig: McpConfig;
33
27
  /**
34
- * Runs the CAS MCP server over stdio: allocates the session-state slot, builds
28
+ * Runs the file CAS MCP server over stdio: allocates the session-state slot, builds
35
29
  * the `mcpStep` for `c`, and drives the read → parse → dispatch → write loop
36
- * until stdin EOF. Generic in `O` so it composes with any `Cas<O>` backing.
37
- *
38
- * When `toUrl` is provided, `cas_get` includes the blob's filesystem URL.
30
+ * until stdin EOF.
39
31
  */
40
- export declare const casMcpServer: <O extends Operation>(c: Cas<O>, home: string, toUrl?: (hash: Vec) => string) => Effect<Read | Write | MemOp | Mkdir | Rename | RandomInt | ReadBytes | O, void>;
32
+ export declare const casMcpServer: (home: string) => Effect<Read | Write | MemOp | FileCasOperation | Rm, void>;
@@ -30,18 +30,28 @@
30
30
  * ## `cas_get` output
31
31
  *
32
32
  * Always returns a JSON object `{ length, mime_type, type[, url][, content] }`.
33
- * `type` is always present (`'text'` or `'base64'`). When `content: true` is
34
- * requested, the inline payload is also included. Two-phase MIME detection
35
- * determines the encoding:
36
- *
37
- * 1. **Magic-byte sniffing** (`fs/mime` `detect`): PNG/JPEG/GIF/WebP/PDF/ZIP →
38
- * `type: 'base64'` with the detected `mime_type`.
39
- * 2. **UTF-8 validation** (`fs/text/utf8` `fromVec`): valid UTF-8 →
40
- * `type: 'text'`, `mime_type: 'text/plain'`.
41
- * 3. **Fallback**: `type: 'base64'`, `mime_type: 'application/octet-stream'`.
42
- *
43
- * When `content: false` (default), only `{ length, mime_type, type[, url] }` is
44
- * returned no content transfer.
33
+ * `type` is always present (`'text'` or `'base64'`). A single classifier — the
34
+ * `fs/mime` detector state machine (length × magic-byte eliminator × UTF-8 DFA)
35
+ * produces the same three-way verdict on both paths, so there is no second,
36
+ * divergent copy of the rules:
37
+ *
38
+ * 1. **Magic-byte hit** (PNG/JPEG/GIF/WebP/PDF/ZIP) → `type: 'base64'` with the
39
+ * detected `mime_type`.
40
+ * 2. **Whole-blob-valid UTF-8** → `type: 'text'`, `mime_type: 'text/plain'`.
41
+ * 3. **Fallback** `type: 'base64'`, `mime_type: 'application/octet-stream'`.
42
+ *
43
+ * **Metadata-only (`content: false`, the default) is size-independent.** It folds
44
+ * the read stream through `fs/mime` `detectStream` and never buffers the blob.
45
+ * Because a single `Vec` caps at `maxLength` bits (128 KiB), the old
46
+ * drain-into-one-`Vec` approach failed on any blob larger than one chunk even when
47
+ * only metadata was asked for; streaming detection returns correct
48
+ * `{ length, mime_type, type[, url] }` regardless of size.
49
+ *
50
+ * **`content: true`** materializes the bytes (via `collectRead`, bounded by
51
+ * `maxLength`), classifies them with the same machine via `fs/mime` `detectVec`,
52
+ * then encodes the inline payload by `type` — a `fs/text/utf8` `fromVec` string for
53
+ * `text`, base64 for `base64`. A blob larger than `maxLength` is unsupported here
54
+ * and should be fetched via `url`.
45
55
  *
46
56
  * ## Encoding split: hashes vs. content
47
57
  *
@@ -64,40 +74,80 @@
64
74
  * @module
65
75
  */
66
76
  import { string, option, or, boolean } from "../../types/rtti/module.f.js";
77
+ import { stringify } from "../../json/module.f.js";
67
78
  import { pure } from "../../effects/module.f.js";
68
79
  import { create } from "../../effects/memory/module.f.js";
69
80
  import { cBase32ToVec, vecToCBase32 } from "../../cbase32/module.f.js";
70
81
  import { decode as base64Decode, encode as base64Encode } from "../../base64/module.f.js";
71
82
  import { utf8 } from "../../text/module.f.js";
72
- import { detect } from "../../mime/module.f.js";
73
- import { length as bitVecLength } from "../../types/bit_vec/module.f.js";
74
- import {} from "../../effects/node/module.f.js";
83
+ import { detectVec, detectStream } from "../../mime/module.f.js";
84
+ import { empty, length as bitVecLength, maxLength, msb } from "../../types/bit_vec/module.f.js";
85
+ import { ok, error } from "../../types/result/module.f.js";
86
+ import { rm } from "../../effects/node/module.f.js";
75
87
  import { stdioTransport } from "../../mcp/stdio/module.f.js";
76
88
  import { mcpStep, uninitializedState, toolEntry, fromRegistry, errorResult, } from "../../mcp/module.f.js";
77
- import { casUpload } from "../module.f.js";
89
+ import { casAddFile, fileCas } from "../module.f.js";
78
90
  import { fromVec } from "../../text/utf8/module.f.js";
91
+ import { identity } from "../../types/function/module.f.js";
92
+ import { sha256 } from "../../crypto/sha2/module.f.js";
93
+ import { nonEmpty, empty as elEmpty } from "../../effects/list/module.f.js";
79
94
  // ── Argument schemas (declared once, used for both inputSchema and validate) ─────
80
95
  /** Arguments for `cas_add`: content to store, with optional encoding type. */
81
- export const casAddArgs = { content: string, type: option(or(or('text', 'base64'), 'url')) };
96
+ export const casAddArgs = {
97
+ content: string,
98
+ type: or('text', 'base64', 'url', undefined)
99
+ };
82
100
  /** Arguments for `cas_get`: the cBase32 hash to look up; optionally request inline content. */
83
- export const casGetArgs = { hash: string, content: option(boolean) };
101
+ export const casGetArgs = {
102
+ hash: string,
103
+ content: option(boolean)
104
+ };
84
105
  /** Arguments for `cas_list`: none. */
85
106
  export const casListArgs = {};
107
+ // ── Stream helper ────────────────────────────────────────────────────────────────
108
+ /**
109
+ * Drains a CAS read stream into a single `Vec`. Used only on the `content: true`
110
+ * path, where the whole blob is needed for inline base64 / UTF-8 transfer; the
111
+ * chunk stream is concatenated and an error item is surfaced as the result.
112
+ * Metadata-only `cas_get` avoids this entirely via `fs/mime` `detectStream`,
113
+ * which is why it is not bound by the `maxLength` ceiling below.
114
+ */
115
+ const collectRead = (stream) => {
116
+ const loop = (acc) => (s) => s.step((node) => {
117
+ if (node === undefined) {
118
+ return pure(ok(acc));
119
+ }
120
+ const { first, tail } = node;
121
+ const [t, v] = first;
122
+ if (t === 'error') {
123
+ return pure(first);
124
+ }
125
+ // A single `Vec` cannot exceed `maxLength` bits; concatenating past it would
126
+ // overflow the runtime's `bigint` constraint. Surface that as an error item
127
+ // so the tool reports a failure rather than crashing the process.
128
+ if (bitVecLength(acc) + bitVecLength(v) > maxLength) {
129
+ return pure(error(`cas blob exceeds maximum vector length of ${maxLength} bits`));
130
+ }
131
+ return loop(msb.concat(acc)(v))(tail);
132
+ });
133
+ return loop(empty)(stream);
134
+ };
86
135
  // ── Tool registry ──────────────────────────────────────────────────────────────
136
+ const toJson = stringify(identity);
87
137
  /** Registry of all CAS tools. */
88
- const casToolRegistry = (c, home, toUrl) => {
138
+ const casToolRegistry = (home) => {
139
+ const c = fileCas(sha256)(home);
89
140
  const casUploadDir = `${home}/cas_upload`;
90
141
  return [
91
142
  toolEntry('cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; type:"url" to stream a file from $HOME/cas_upload/ (no size limit); omit or pass type:"text" for UTF-8 text (default).', casAddArgs, ({ type, content }) => {
92
- // type:'url' — streaming move-hash-move pipeline (no size limit)
143
+ // type:'url' — stream the file into cas, then delete the source on success
93
144
  if (type === 'url') {
94
145
  if (!content.startsWith(`${casUploadDir}/`) || content.includes('..')) {
95
146
  return pure(errorResult(`cas_add type:url paths must be within ${casUploadDir}/ — got: ${content}`));
96
147
  }
97
- const fileName = content.slice(`${casUploadDir}/`.length);
98
- return casUpload(home)(fileName).step(result => pure(result[0] === 'error'
99
- ? errorResult(`upload failed: ${result[1]}`)
100
- : okResult(vecToCBase32(result[1]))));
148
+ return casAddFile(c)(content).step(([t, v]) => t === 'error'
149
+ ? pure(errorResult(`upload failed: ${v}`))
150
+ : rm(content).step(() => pure(okResult(vecToCBase32(v)))));
101
151
  }
102
152
  // type:'text' or 'base64' — resolve content to Vec, store via c.write()
103
153
  let x;
@@ -112,65 +162,54 @@ const casToolRegistry = (c, home, toUrl) => {
112
162
  }
113
163
  return x.step(value => typeof value === 'string'
114
164
  ? pure(errorResult(value))
115
- : c.write(value).step(hash => pure(hash === undefined
165
+ // The resolved content fits in one chunk; feed it as a single-item stream.
166
+ : c.write(nonEmpty(ok(value), elEmpty())).step(hashResult => pure(hashResult[0] === 'error'
116
167
  ? errorResult('write')
117
- : okResult(vecToCBase32(hash)))));
168
+ : okResult(vecToCBase32(hashResult[1])))));
118
169
  }),
119
170
  toolEntry('cas_get', 'Inspect a blob by hash. Always returns JSON {length,mime_type,type[,url]} where type is "text" or "base64". Pass content:true to also include the inline content string.', casGetArgs, r => {
120
171
  const key = cBase32ToVec(r.hash);
121
172
  if (key === null) {
122
173
  return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
123
174
  }
124
- return c.read(key).step(value => {
125
- if (value === undefined) {
126
- return pure(errorResult(`no such hash: ${r.hash}`));
127
- }
128
- const byteLength = Number(bitVecLength(value) / 8n);
129
- // Phase 1: magic-byte sniffing for known binary formats.
130
- const detectedMime = detect(value);
131
- if (detectedMime !== null) {
132
- const url = toUrl?.(key);
133
- const meta = {
134
- length: byteLength,
135
- mime_type: detectedMime,
136
- type: 'base64',
137
- ...(url !== undefined && { url })
138
- };
139
- if (r.content === true) {
140
- const blob = base64Encode(value);
141
- return pure(blob === null
142
- ? errorResult(`content is not byte-aligned: ${r.hash}`)
143
- : okResult(JSON.stringify({ ...meta, content: blob })));
175
+ const url = c.url(key);
176
+ // Metadata-only (the default): derive `{ length, mime_type, type }` by
177
+ // streaming the blob through the detector state machine. Never buffers
178
+ // the blob, so this is size-independent — large blobs that overflow a
179
+ // single `Vec` still return correct metadata instead of an error.
180
+ if (r.content !== true) {
181
+ return detectStream(c.read(key)).step(result => {
182
+ if (result[0] === 'error') {
183
+ return pure(errorResult(`no such hash: ${r.hash}`));
144
184
  }
145
- return pure(okResult(JSON.stringify(meta)));
146
- }
147
- // Phase 2: UTF-8 validation — text if valid, octet-stream otherwise.
148
- const str = fromVec(value);
149
- const url = toUrl?.(key);
150
- if (str !== null) {
151
- const meta = {
152
- length: byteLength,
153
- mime_type: 'text/plain',
154
- type: 'text',
155
- ...(url !== undefined && { url })
156
- };
157
- return pure(r.content === true
158
- ? okResult(JSON.stringify({ ...meta, content: str }))
159
- : okResult(JSON.stringify(meta)));
185
+ const { length, mime_type, type } = result[1];
186
+ const meta = { length: Number(length), mime_type, type, url };
187
+ return pure(okResult(toJson(meta)));
188
+ });
189
+ }
190
+ // content:true — collect the bytes (bounded by `maxLength`) so they can
191
+ // be encoded inline. Classification uses the *same* `detectVec` state
192
+ // machine as the metadata-only path; `type` then selects the encoding —
193
+ // a UTF-8 string for `text`, base64 for `base64`.
194
+ return collectRead(c.read(key)).step(result => {
195
+ if (result[0] === 'error') {
196
+ return pure(errorResult(`no such hash: ${r.hash}`));
160
197
  }
161
- const meta = {
162
- length: byteLength,
163
- mime_type: 'application/octet-stream',
164
- type: 'base64',
165
- ...(url !== undefined && { url })
166
- };
167
- if (r.content === true) {
168
- const blob = base64Encode(value);
169
- return pure(blob === null
198
+ const value = result[1];
199
+ const { length, mime_type, type } = detectVec(value);
200
+ const meta = { length: Number(length), mime_type, type, url };
201
+ if (type === 'text') {
202
+ // `type: 'text'` means the detector validated `value` as UTF-8, so
203
+ // `fromVec` is non-null here; guard defensively regardless.
204
+ const str = fromVec(value);
205
+ return pure(str === null
170
206
  ? errorResult(`content is not byte-aligned: ${r.hash}`)
171
- : okResult(JSON.stringify({ ...meta, content: blob })));
207
+ : okResult(toJson({ ...meta, content: str })));
172
208
  }
173
- return pure(okResult(JSON.stringify(meta)));
209
+ const blob = base64Encode(value);
210
+ return pure(blob === null
211
+ ? errorResult(`content is not byte-aligned: ${r.hash}`)
212
+ : okResult(toJson({ ...meta, content: blob })));
174
213
  });
175
214
  }),
176
215
  toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
@@ -181,14 +220,9 @@ const casToolRegistry = (c, home, toUrl) => {
181
220
  const okResult = (text) => ({ content: [{ type: 'text', text }] });
182
221
  // ── Handlers ────────────────────────────────────────────────────────────────────
183
222
  /**
184
- * MCP handlers for an injected `Cas<O>` — generic in `O` exactly like `Cas`
185
- * itself, so the same handlers run over `Fs` (production) or memory (tests).
186
- *
187
- * When `toUrl` is provided, `cas_get` includes the `url` field pointing to
188
- * the blob on the local filesystem. When absent (e.g. memory-backed tests),
189
- * `url` is omitted.
223
+ * MCP handlers for `FileCas`.
190
224
  */
191
- export const casMcpHandlers = (c, home, toUrl) => fromRegistry(casToolRegistry(c, home, toUrl));
225
+ export const casMcpHandlers = (home) => fromRegistry(casToolRegistry(home));
192
226
  // ── Session configuration ───────────────────────────────────────────────────────
193
227
  /**
194
228
  * Static MCP configuration for the CAS server: advertises the `tools`
@@ -201,10 +235,8 @@ export const casConfig = {
201
235
  };
202
236
  // ── Server ──────────────────────────────────────────────────────────────────────
203
237
  /**
204
- * Runs the CAS MCP server over stdio: allocates the session-state slot, builds
238
+ * Runs the file CAS MCP server over stdio: allocates the session-state slot, builds
205
239
  * the `mcpStep` for `c`, and drives the read → parse → dispatch → write loop
206
- * until stdin EOF. Generic in `O` so it composes with any `Cas<O>` backing.
207
- *
208
- * When `toUrl` is provided, `cas_get` includes the blob's filesystem URL.
240
+ * until stdin EOF.
209
241
  */
210
- export const casMcpServer = (c, home, toUrl) => create(uninitializedState).step(key => stdioTransport(mcpStep(casConfig)(casMcpHandlers(c, home, toUrl))(key)));
242
+ export const casMcpServer = (home) => create(uninitializedState).step(key => stdioTransport(mcpStep(casConfig)(casMcpHandlers(home))(key)));
@@ -1,4 +1,7 @@
1
1
  export declare const proof: {
2
+ getMetaLargeMultiChunkBlobNoError: () => void;
3
+ getMetaLargeMultiChunkUtf8Symbols: () => void;
4
+ getMetaLargeMultiChunkTextThenBinary: () => void;
2
5
  toolsListAdvertisesThreeTools: () => void;
3
6
  addReturnsHash: () => void;
4
7
  textAddGetMetaRoundTrips: () => void;
@@ -24,11 +27,12 @@ export declare const proof: {
24
27
  getMetaReturnsLengthAndMimeType: () => void;
25
28
  getMetaBinaryBlob: () => void;
26
29
  getMetaOctetStreamForUnknownBinary: () => void;
27
- getMetaNoUrlWhenToUrlAbsent: () => void;
30
+ getMetaOctetStreamForNulBlob: () => void;
28
31
  getMetaMissingHashIsError: () => void;
29
32
  getMetaInvalidHashIsError: () => void;
30
33
  addUrlFromSubdirectorySucceeds: () => void;
31
34
  addUrlFromApprovedDirectorySucceeds: () => void;
32
35
  addUrlFromRandomDirectoryIsRejected: () => void;
33
36
  addUrlWithPathTraversalIsRejected: () => void;
37
+ getOctetStreamWithContentIncludesBase64: () => void;
34
38
  };