functionalscript 0.30.0 → 0.31.1

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 (181) hide show
  1. package/fs/asn.1/module.f.js +18 -22
  2. package/fs/asn.1/proof.f.d.ts +5 -0
  3. package/fs/asn.1/proof.f.js +38 -2
  4. package/fs/base64/module.f.d.ts +9 -0
  5. package/fs/base64/module.f.js +60 -0
  6. package/fs/base64/proof.f.d.ts +11 -0
  7. package/fs/base64/proof.f.js +117 -0
  8. package/fs/base_n/module.f.d.ts +44 -0
  9. package/fs/base_n/module.f.js +54 -0
  10. package/fs/base_n/proof.f.d.ts +10 -0
  11. package/fs/base_n/proof.f.js +86 -0
  12. package/fs/bnf/data/module.f.d.ts +4 -9
  13. package/fs/bnf/data/module.f.js +4 -3
  14. package/fs/bnf/module.f.d.ts +3 -4
  15. package/fs/bnf/module.f.js +3 -2
  16. package/fs/bnf/proof.f.d.ts +9 -0
  17. package/fs/bnf/proof.f.js +98 -1
  18. package/fs/cas/mcp/module.f.d.ts +40 -0
  19. package/fs/cas/mcp/module.f.js +223 -0
  20. package/fs/cas/mcp/proof.f.d.ts +30 -0
  21. package/fs/cas/mcp/proof.f.js +310 -0
  22. package/fs/cas/module.f.d.ts +9 -3
  23. package/fs/cas/module.f.js +104 -69
  24. package/fs/cas/proof.f.d.ts +2 -0
  25. package/fs/cas/proof.f.js +30 -1
  26. package/fs/cbase32/module.f.d.ts +1 -1
  27. package/fs/cbase32/module.f.js +19 -40
  28. package/fs/cbase32/proof.f.d.ts +1 -0
  29. package/fs/cbase32/proof.f.js +15 -0
  30. package/fs/ci/config/module.f.d.ts +6 -6
  31. package/fs/ci/config/module.f.js +6 -6
  32. package/fs/ci/deno/module.f.js +2 -2
  33. package/fs/ci/module.f.d.ts +5 -0
  34. package/fs/ci/module.f.js +2 -3
  35. package/fs/ci/proof.f.d.ts +1 -0
  36. package/fs/ci/proof.f.js +9 -3
  37. package/fs/cli/module.f.d.ts +3 -1
  38. package/fs/cli/module.f.js +22 -12
  39. package/fs/cli/proof.f.d.ts +5 -0
  40. package/fs/cli/proof.f.js +76 -0
  41. package/fs/crypto/hmac/proof.f.d.ts +1 -0
  42. package/fs/crypto/hmac/proof.f.js +8 -0
  43. package/fs/crypto/secp/proof.f.d.ts +1 -0
  44. package/fs/crypto/secp/proof.f.js +10 -0
  45. package/fs/crypto/sign/module.f.js +1 -4
  46. package/fs/dev/module.f.d.ts +5 -3
  47. package/fs/dev/module.f.js +13 -2
  48. package/fs/dev/package_json/proof.f.d.ts +1 -0
  49. package/fs/dev/package_json/proof.f.js +5 -0
  50. package/fs/dev/proof.f.d.ts +1 -0
  51. package/fs/dev/proof.f.js +12 -1
  52. package/fs/djs/ast/module.f.d.ts +1 -1
  53. package/fs/djs/ast/proof.f.d.ts +5 -0
  54. package/fs/djs/ast/proof.f.js +17 -0
  55. package/fs/djs/module.f.d.ts +1 -1
  56. package/fs/djs/module.f.js +2 -3
  57. package/fs/djs/serializer/module.f.js +24 -11
  58. package/fs/djs/transpiler/module.f.js +3 -4
  59. package/fs/effects/memory/module.f.d.ts +37 -0
  60. package/fs/effects/memory/module.f.js +24 -0
  61. package/fs/effects/memory/proof.f.d.ts +6 -0
  62. package/fs/effects/memory/proof.f.js +64 -0
  63. package/fs/effects/mock/module.f.d.ts +7 -2
  64. package/fs/effects/mock/module.f.js +12 -8
  65. package/fs/effects/module.d.ts +1 -1
  66. package/fs/effects/module.f.d.ts +41 -0
  67. package/fs/effects/module.f.js +25 -0
  68. package/fs/effects/module.js +6 -7
  69. package/fs/effects/node/memory/module.d.ts +20 -0
  70. package/fs/effects/node/memory/module.js +44 -0
  71. package/fs/effects/node/memory/proof.d.ts +10 -0
  72. package/fs/effects/node/memory/proof.js +35 -0
  73. package/fs/effects/node/module.d.ts +1 -1
  74. package/fs/effects/node/module.f.d.ts +54 -8
  75. package/fs/effects/node/module.f.js +55 -4
  76. package/fs/effects/node/module.js +167 -137
  77. package/fs/effects/node/proof.f.d.ts +10 -0
  78. package/fs/effects/node/proof.f.js +88 -21
  79. package/fs/effects/node/virtual/module.f.d.ts +6 -0
  80. package/fs/effects/node/virtual/module.f.js +48 -16
  81. package/fs/effects/node/virtual/proof.f.d.ts +17 -0
  82. package/fs/effects/node/virtual/proof.f.js +79 -0
  83. package/fs/effects/proof.f.d.ts +5 -0
  84. package/fs/effects/proof.f.js +54 -34
  85. package/fs/emergent_testing/module.f.d.ts +6 -1
  86. package/fs/emergent_testing/module.f.js +8 -3
  87. package/fs/emergent_testing/proof.f.d.ts +11 -0
  88. package/fs/emergent_testing/proof.f.js +52 -16
  89. package/fs/fjs/module.f.js +2 -2
  90. package/fs/fjs/proof.f.d.ts +8 -0
  91. package/fs/fjs/proof.f.js +39 -0
  92. package/fs/fsc/module.f.d.ts +5 -0
  93. package/fs/fsc/module.f.js +17 -27
  94. package/fs/fsc/proof.f.d.ts +8 -0
  95. package/fs/fsc/proof.f.js +48 -2
  96. package/fs/fsm/module.f.d.ts +2 -3
  97. package/fs/fsm/module.f.js +1 -1
  98. package/fs/html/module.f.d.ts +2 -3
  99. package/fs/html/module.f.js +7 -6
  100. package/fs/html/proof.f.d.ts +1 -0
  101. package/fs/html/proof.f.js +7 -0
  102. package/fs/js/tokenizer/module.f.js +1 -1
  103. package/fs/js/tokenizer/proof.f.js +7 -0
  104. package/fs/json/module.f.d.ts +9 -2
  105. package/fs/json/module.f.js +9 -7
  106. package/fs/json/parser/module.f.js +4 -6
  107. package/fs/json/parser/proof.f.js +19 -10
  108. package/fs/json/proof.f.d.ts +2 -1
  109. package/fs/json/proof.f.js +33 -6
  110. package/fs/json/rpc/module.f.d.ts +5 -0
  111. package/fs/json/rpc/module.f.js +16 -11
  112. package/fs/json/schema/module.f.d.ts +15 -14
  113. package/fs/json/schema/proof.f.d.ts +3 -0
  114. package/fs/json/schema/proof.f.js +16 -1
  115. package/fs/mcp/module.f.d.ts +103 -5
  116. package/fs/mcp/module.f.js +139 -2
  117. package/fs/mcp/proof.f.d.ts +34 -0
  118. package/fs/mcp/proof.f.js +207 -0
  119. package/fs/mcp/stdio/module.f.d.ts +39 -0
  120. package/fs/mcp/stdio/module.f.js +54 -0
  121. package/fs/mcp/stdio/proof.f.d.ts +10 -0
  122. package/fs/mcp/stdio/proof.f.js +92 -0
  123. package/fs/mime/module.f.d.ts +39 -0
  124. package/fs/mime/module.f.js +78 -0
  125. package/fs/mime/proof.f.d.ts +15 -0
  126. package/fs/mime/proof.f.js +30 -0
  127. package/fs/path/module.f.d.ts +8 -0
  128. package/fs/path/module.f.js +10 -3
  129. package/fs/path/proof.f.d.ts +1 -0
  130. package/fs/path/proof.f.js +40 -2
  131. package/fs/text/sgr/proof.f.js +30 -2
  132. package/fs/text/utf8/module.f.d.ts +7 -0
  133. package/fs/text/utf8/module.f.js +85 -31
  134. package/fs/text/utf8/proof.f.d.ts +1 -0
  135. package/fs/text/utf8/proof.f.js +60 -1
  136. package/fs/types/bigfloat/module.f.d.ts +3 -0
  137. package/fs/types/bigfloat/module.f.js +30 -31
  138. package/fs/types/bigint/module.f.d.ts +24 -2
  139. package/fs/types/bigint/module.f.js +30 -2
  140. package/fs/types/bigint/proof.f.d.ts +7 -0
  141. package/fs/types/bigint/proof.f.js +47 -16
  142. package/fs/types/bit_vec/module.f.d.ts +18 -0
  143. package/fs/types/bit_vec/module.f.js +18 -0
  144. package/fs/types/bit_vec/proof.f.d.ts +1 -0
  145. package/fs/types/bit_vec/proof.f.js +13 -1
  146. package/fs/types/function/operator/proof.f.d.ts +2 -0
  147. package/fs/types/function/operator/proof.f.js +20 -2
  148. package/fs/types/list/module.f.js +1 -4
  149. package/fs/types/list/proof.f.d.ts +1 -1
  150. package/fs/types/list/proof.f.js +14 -6
  151. package/fs/types/map/proof.f.d.ts +1 -0
  152. package/fs/types/map/proof.f.js +10 -1
  153. package/fs/types/nibble_set/module.f.d.ts +13 -0
  154. package/fs/types/nibble_set/module.f.js +16 -0
  155. package/fs/types/object/module.f.d.ts +12 -8
  156. package/fs/types/object/module.f.js +9 -2
  157. package/fs/types/object/proof.f.d.ts +14 -0
  158. package/fs/types/object/proof.f.js +4 -0
  159. package/fs/types/ordered_map/proof.f.d.ts +1 -0
  160. package/fs/types/ordered_map/proof.f.js +39 -79
  161. package/fs/types/phantom/module.f.d.ts +22 -0
  162. package/fs/types/phantom/module.f.js +11 -0
  163. package/fs/types/prime_field/proof.f.d.ts +7 -0
  164. package/fs/types/prime_field/proof.f.js +63 -122
  165. package/fs/types/rtti/common/module.f.d.ts +4 -4
  166. package/fs/types/rtti/common/module.f.js +1 -1
  167. package/fs/types/rtti/module.f.d.ts +2 -3
  168. package/fs/types/rtti/parse/module.f.js +0 -1
  169. package/fs/types/rtti/ts/module.f.d.ts +26 -32
  170. package/fs/types/rtti/ts/module.f.js +2 -2
  171. package/fs/types/rtti/ts/proof.f.js +3 -3
  172. package/fs/types/rtti/validate/module.f.js +0 -1
  173. package/fs/types/sorted_list/module.f.d.ts +6 -0
  174. package/fs/types/sorted_list/module.f.js +11 -0
  175. package/fs/types/sorted_set/module.f.js +2 -8
  176. package/fs/types/ts/module.f.d.ts +1 -1
  177. package/fs/types/ts/module.f.js +3 -1
  178. package/fs/types/ts/proof.f.js +3 -2
  179. package/fs/website/proof.f.d.ts +1 -0
  180. package/fs/website/proof.f.js +8 -1
  181. package/package.json +3 -3
@@ -0,0 +1,310 @@
1
+ import { assert, assertEq } from "../../asserts/module.f.js";
2
+ import { pure } from "../../effects/module.f.js";
3
+ import { run } from "../../effects/mock/module.f.js";
4
+ import { asBase, asNominal, create, read, write } from "../../effects/memory/module.f.js";
5
+ import { msb, u8ListToVec, vec8 } from "../../types/bit_vec/module.f.js";
6
+ import { vecToCBase32 } from "../../cbase32/module.f.js";
7
+ import { encode as base64Encode } from "../../base64/module.f.js";
8
+ import { sha256 } from "../../crypto/sha2/module.f.js";
9
+ import { utf8 } from "../../text/module.f.js";
10
+ import { cas } from "../module.f.js";
11
+ import { mcpStep, uninitializedState, } from "../../mcp/module.f.js";
12
+ import {} from "../../effects/node/module.f.js";
13
+ import { casConfig, casMcpHandlers } from "./module.f.js";
14
+ const initialTestState = { memory: { next: 0, values: {} }, files: {} };
15
+ const mock = {
16
+ memCreate: value => state => {
17
+ const id = `k${state.memory.next}`;
18
+ const key = asNominal(id);
19
+ return [
20
+ { ...state, memory: { next: state.memory.next + 1, values: { ...state.memory.values, [id]: value } } },
21
+ key,
22
+ ];
23
+ },
24
+ memRead: key => state => [state, state.memory.values[asBase(key)]],
25
+ memWrite: (key, value) => state => {
26
+ const id = asBase(key);
27
+ return [{ ...state, memory: { ...state.memory, values: { ...state.memory.values, [id]: value } } }, undefined];
28
+ },
29
+ readFile: path => state => {
30
+ const v = state.files[path];
31
+ return v !== undefined
32
+ ? [state, ['ok', v]]
33
+ : [state, ['error', new Error(`ENOENT: ${path}`)]];
34
+ },
35
+ };
36
+ const runMem = (effect) => run(mock)(initialTestState)(effect)[1];
37
+ const runMemWithFiles = (files) => (effect) => run(mock)({ memory: { next: 0, values: {} }, files })(effect)[1];
38
+ const memKvStore = (mapKey) => ({
39
+ read: (key) => read(mapKey).step(m => pure(m[vecToCBase32(key)]?.[1])),
40
+ write: (key, value) => read(mapKey).step(m => write(mapKey, { ...m, [vecToCBase32(key)]: [key, value] })),
41
+ list: () => read(mapKey).step(m => pure(Object.values(m).map(([k]) => k))),
42
+ });
43
+ // ── Session driver ──────────────────────────────────────────────────────────────
44
+ // Feeds each message to `step` in order, collecting every response.
45
+ const feed = (step) => (msgs) => {
46
+ const go = (i, acc) => i === msgs.length
47
+ ? pure(acc)
48
+ : step(msgs[i]).step(r => go(i + 1, [...acc, r]));
49
+ return go(0, []);
50
+ };
51
+ // Runs a full session over a fresh in-memory CAS, returning all responses.
52
+ const runSession = (msgs) => runMem(create({}).step(mapKey => create(uninitializedState).step(sessionKey => {
53
+ const c = cas(sha256)(memKvStore(mapKey));
54
+ const step = mcpStep(casConfig)(casMcpHandlers(c))(sessionKey);
55
+ return feed(step)(msgs);
56
+ })));
57
+ // Runs a session with a mocked filesystem (for cas_add with type:'url' tests).
58
+ const runSessionWithFiles = (files) => (msgs) => runMemWithFiles(files)(create({}).step(mapKey => create(uninitializedState).step(sessionKey => {
59
+ const c = cas(sha256)(memKvStore(mapKey));
60
+ const step = mcpStep(casConfig)(casMcpHandlers(c))(sessionKey);
61
+ return feed(step)(msgs);
62
+ })));
63
+ // ── Messages ────────────────────────────────────────────────────────────────────
64
+ const init = { jsonrpc: '2.0', method: 'initialize', id: 1,
65
+ params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'c', version: '0' } } };
66
+ const initialized = { jsonrpc: '2.0', method: 'notifications/initialized' };
67
+ const call = (id, name, args) => ({ jsonrpc: '2.0', method: 'tools/call', id, params: { name, arguments: args } });
68
+ const list = (id) => ({ jsonrpc: '2.0', method: 'tools/list', id });
69
+ // Runs `init`, `notifications/initialized`, then `msgs`; returns the tool responses.
70
+ const session = (...msgs) => runSession([init, initialized, ...msgs]).slice(2);
71
+ const resultOf = (resp) => resp.result;
72
+ const item0 = (resp) => resultOf(resp).content[0];
73
+ const textOf = (resp) => item0(resp).text;
74
+ // A plain text sample for text add→get round-trips.
75
+ const textSample = 'hello, world!';
76
+ // A base64-encoded binary payload for binary add→get round-trips.
77
+ const binarySample = base64Encode(vec8(0x2an));
78
+ // A base64 blob whose leading bytes are the PNG magic-byte signature, so
79
+ // `cas_get` detects its type and returns base64 with mime_type image/png.
80
+ const pngSample = base64Encode(u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01]));
81
+ // ── Tests ───────────────────────────────────────────────────────────────────────
82
+ export const proof = {
83
+ toolsListAdvertisesThreeTools: () => {
84
+ const [resp] = runSession([init, initialized, list(2)]).slice(2);
85
+ const tools = resp.result.tools;
86
+ assertEq(tools.length, 3);
87
+ assertEq(tools.map(t => t.name).join(','), 'cas_add,cas_get,cas_list');
88
+ const add = resp.result.tools[0];
89
+ assertEq(add.inputSchema.type, 'object');
90
+ },
91
+ addReturnsHash: () => {
92
+ const [resp] = session(call(2, 'cas_add', { content: textSample }));
93
+ assert(!resultOf(resp).isError);
94
+ assert(textOf(resp).length > 0);
95
+ },
96
+ // Text add→get round-trip: store as UTF-8 text, retrieve metadata.
97
+ textAddGetMetaRoundTrips: () => {
98
+ const [addResp] = session(call(2, 'cas_add', { content: textSample }));
99
+ const hash = textOf(addResp);
100
+ const [, getResp] = session(call(2, 'cas_add', { content: textSample }), call(3, 'cas_get', { hash }));
101
+ assert(!resultOf(getResp).isError);
102
+ const result = JSON.parse(textOf(getResp));
103
+ assertEq(result.mime_type, 'text/plain');
104
+ assertEq(result.type, 'text');
105
+ assertEq(result.length, textSample.length);
106
+ assertEq(result.content, undefined);
107
+ },
108
+ // cas_get with content:true returns inline content.
109
+ textAddGetContentRoundTrips: () => {
110
+ const [addResp] = session(call(2, 'cas_add', { content: textSample }));
111
+ const hash = textOf(addResp);
112
+ const [, getResp] = session(call(2, 'cas_add', { content: textSample }), call(3, 'cas_get', { hash, content: true }));
113
+ assert(!resultOf(getResp).isError);
114
+ const result = JSON.parse(textOf(getResp));
115
+ assertEq(result.type, 'text');
116
+ assertEq(result.mime_type, 'text/plain');
117
+ assertEq(result.content, textSample);
118
+ },
119
+ // Binary add→get round-trip: store as base64, retrieve metadata without content.
120
+ binaryAddGetMetaRoundTrips: () => {
121
+ const [addResp] = session(call(2, 'cas_add', { content: binarySample, type: 'base64' }));
122
+ const hash = textOf(addResp);
123
+ const [, getResp] = session(call(2, 'cas_add', { content: binarySample, type: 'base64' }), call(3, 'cas_get', { hash }));
124
+ assert(!resultOf(getResp).isError);
125
+ const result = JSON.parse(textOf(getResp));
126
+ assertEq(result.content, undefined);
127
+ assertEq(result.type, 'text');
128
+ assertEq(result.mime_type, 'text/plain');
129
+ },
130
+ // Binary add→get with content:true returns inline base64 content.
131
+ binaryAddGetContentRoundTrips: () => {
132
+ const [addResp] = session(call(2, 'cas_add', { content: binarySample, type: 'base64' }));
133
+ const hash = textOf(addResp);
134
+ const [, getResp] = session(call(2, 'cas_add', { content: binarySample, type: 'base64' }), call(3, 'cas_get', { hash, content: true }));
135
+ assert(!resultOf(getResp).isError);
136
+ const result = JSON.parse(textOf(getResp));
137
+ assertEq(result.type, 'text');
138
+ assertEq(result.content, '*');
139
+ },
140
+ addGetRoundTrips: () => {
141
+ const [addResp] = session(call(2, 'cas_add', { content: textSample }));
142
+ const hash = textOf(addResp);
143
+ const [, getResp] = session(call(2, 'cas_add', { content: textSample }), call(3, 'cas_get', { hash, content: true }));
144
+ assert(!resultOf(getResp).isError);
145
+ const result = JSON.parse(textOf(getResp));
146
+ assertEq(result.content, textSample);
147
+ },
148
+ // Text content (no magic-byte match, valid UTF-8) comes back with type:'text' when content requested.
149
+ getUntypedReturnsText: () => {
150
+ const [addResp] = session(call(2, 'cas_add', { content: textSample }));
151
+ const hash = textOf(addResp);
152
+ const [, getResp] = session(call(2, 'cas_add', { content: textSample }), call(3, 'cas_get', { hash, content: true }));
153
+ assertEq(item0(getResp) === null ? null : item0(getResp).type, 'text');
154
+ const result = JSON.parse(textOf(getResp));
155
+ assertEq(result.type, 'text');
156
+ assertEq(result.mime_type, 'text/plain');
157
+ },
158
+ // Bytes with a recognised PNG signature come back as base64 with mime_type image/png when content requested.
159
+ getTypedReturnsBinaryJson: () => {
160
+ const [addResp] = session(call(2, 'cas_add', { content: pngSample, type: 'base64' }));
161
+ const hash = textOf(addResp);
162
+ const [, getResp] = session(call(2, 'cas_add', { content: pngSample, type: 'base64' }), call(3, 'cas_get', { hash, content: true }));
163
+ assert(!resultOf(getResp).isError);
164
+ assertEq(item0(getResp).type, 'text');
165
+ const result = JSON.parse(textOf(getResp));
166
+ assertEq(result.type, 'base64');
167
+ assertEq(result.mime_type, 'image/png');
168
+ assertEq(result.content, pngSample);
169
+ },
170
+ listEnumeratesStoredHashes: () => {
171
+ const [addResp, listResp] = session(call(2, 'cas_add', { content: textSample }), call(3, 'cas_list', {}));
172
+ const hash = textOf(addResp);
173
+ assert(!resultOf(listResp).isError);
174
+ assertEq(textOf(listResp), hash);
175
+ },
176
+ addInvalidContentIsError: () => {
177
+ const [resp] = session(call(2, 'cas_add', { content: 'not valid!', type: 'base64' }));
178
+ assertEq(resultOf(resp).isError, true);
179
+ },
180
+ addBadLengthContentIsError: () => {
181
+ const [resp] = session(call(2, 'cas_add', { content: 'A', type: 'base64' }));
182
+ assertEq(resultOf(resp).isError, true);
183
+ },
184
+ getUnterminatedHashIsError: () => {
185
+ const [resp] = session(call(2, 'cas_get', { hash: '0' }));
186
+ assertEq(resultOf(resp).isError, true);
187
+ },
188
+ addMissingContentIsError: () => {
189
+ const [resp] = session(call(2, 'cas_add', {}));
190
+ assertEq(resultOf(resp).isError, true);
191
+ },
192
+ getMissingHashArgumentIsError: () => {
193
+ const [resp] = session(call(2, 'cas_get', {}));
194
+ assertEq(resultOf(resp).isError, true);
195
+ },
196
+ getInvalidHashIsError: () => {
197
+ const [resp] = session(call(2, 'cas_get', { hash: 'not valid!' }));
198
+ assertEq(resultOf(resp).isError, true);
199
+ },
200
+ getMissingHashIsError: () => {
201
+ const absent = vecToCBase32(vec8(0x99n));
202
+ const [resp] = session(call(2, 'cas_get', { hash: absent }));
203
+ assertEq(resultOf(resp).isError, true);
204
+ },
205
+ unknownToolIsError: () => {
206
+ const [resp] = session(call(2, 'cas_remove', { hash: binarySample }));
207
+ assertEq(resultOf(resp).isError, true);
208
+ },
209
+ toolErrorIsNotJsonRpcError: () => {
210
+ const [resp] = session(call(2, 'cas_add', { content: 'not valid!', type: 'base64' }));
211
+ assert(!('error' in resp));
212
+ assert('result' in resp);
213
+ },
214
+ // cas_add with type:'url' reads a file and stores it.
215
+ addUrlStoresFileAndReturnsHash: () => {
216
+ const fileContent = utf8('hello from file');
217
+ const [addUrlResp] = runSessionWithFiles({ '/tmp/hello.txt': fileContent })([
218
+ init, initialized,
219
+ call(2, 'cas_add', { content: '/tmp/hello.txt', type: 'url' }),
220
+ ]).slice(2);
221
+ assert(!resultOf(addUrlResp).isError);
222
+ assert(textOf(addUrlResp).length > 0);
223
+ },
224
+ addUrlRoundTrips: () => {
225
+ const fileContent = utf8('round-trip content');
226
+ const msgs = runSessionWithFiles({ '/tmp/rt.txt': fileContent })([
227
+ init, initialized,
228
+ call(2, 'cas_add', { content: '/tmp/rt.txt', type: 'url' }),
229
+ ]).slice(2);
230
+ const hash = textOf(msgs[0]);
231
+ const msgs2 = runSessionWithFiles({ '/tmp/rt.txt': fileContent })([
232
+ init, initialized,
233
+ call(2, 'cas_add', { content: '/tmp/rt.txt', type: 'url' }),
234
+ call(3, 'cas_get', { hash, content: true }),
235
+ ]).slice(2);
236
+ assert(!resultOf(msgs2[1]).isError);
237
+ const result = JSON.parse(textOf(msgs2[1]));
238
+ assertEq(result.type, 'text');
239
+ assertEq(result.content, 'round-trip content');
240
+ },
241
+ addUrlMissingFileIsError: () => {
242
+ const [resp] = runSessionWithFiles({})([
243
+ init, initialized,
244
+ call(2, 'cas_add', { content: '/nonexistent/path.txt', type: 'url' }),
245
+ ]).slice(2);
246
+ assertEq(resultOf(resp).isError, true);
247
+ },
248
+ // cas_get without content:true returns only metadata.
249
+ getMetaReturnsLengthAndMimeType: () => {
250
+ const fileContent = utf8('text content');
251
+ const [addResp] = runSessionWithFiles({ '/f': fileContent })([
252
+ init, initialized,
253
+ call(2, 'cas_add', { content: '/f', type: 'url' }),
254
+ ]).slice(2);
255
+ const hash = textOf(addResp);
256
+ const [, metaResp2] = runSessionWithFiles({ '/f': fileContent })([
257
+ init, initialized,
258
+ call(2, 'cas_add', { content: '/f', type: 'url' }),
259
+ call(3, 'cas_get', { hash }),
260
+ ]).slice(2);
261
+ assert(!resultOf(metaResp2).isError);
262
+ const meta = JSON.parse(textOf(metaResp2));
263
+ assertEq(meta.mime_type, 'text/plain');
264
+ assertEq(meta.type, 'text');
265
+ assertEq(meta.length, Number(BigInt(/* 'text content'.length */ 12)));
266
+ assertEq(meta.content, undefined);
267
+ },
268
+ getMetaBinaryBlob: () => {
269
+ const [addResp] = session(call(2, 'cas_add', { content: pngSample, type: 'base64' }));
270
+ const hash = textOf(addResp);
271
+ const [, metaResp] = session(call(2, 'cas_add', { content: pngSample, type: 'base64' }), call(3, 'cas_get', { hash }));
272
+ assert(!resultOf(metaResp).isError);
273
+ const meta = JSON.parse(textOf(metaResp));
274
+ assertEq(meta.mime_type, 'image/png');
275
+ assertEq(meta.type, 'base64');
276
+ assertEq(meta.length, 10);
277
+ assertEq(meta.content, undefined);
278
+ },
279
+ getMetaOctetStreamForUnknownBinary: () => {
280
+ const binaryContent = u8ListToVec(msb)([0xFF, 0xFE, 0x00, 0x01]);
281
+ const binaryB64 = base64Encode(binaryContent);
282
+ const [addResp] = session(call(2, 'cas_add', { content: binaryB64, type: 'base64' }));
283
+ const hash = textOf(addResp);
284
+ const [, metaResp] = session(call(2, 'cas_add', { content: binaryB64, type: 'base64' }), call(3, 'cas_get', { hash }));
285
+ assert(!resultOf(metaResp).isError);
286
+ const meta = JSON.parse(textOf(metaResp));
287
+ assertEq(meta.mime_type, 'application/octet-stream');
288
+ assertEq(meta.type, 'base64');
289
+ assertEq(meta.content, undefined);
290
+ },
291
+ getMetaNoUrlWhenToUrlAbsent: () => {
292
+ const content = utf8('no url');
293
+ const binaryB64 = base64Encode(content);
294
+ const [addResp] = session(call(2, 'cas_add', { content: binaryB64 }));
295
+ const hash = textOf(addResp);
296
+ const [, metaResp] = session(call(2, 'cas_add', { content: binaryB64 }), call(3, 'cas_get', { hash }));
297
+ assert(!resultOf(metaResp).isError);
298
+ const meta = JSON.parse(textOf(metaResp));
299
+ assertEq(meta.url, undefined);
300
+ },
301
+ getMetaMissingHashIsError: () => {
302
+ const absent = vecToCBase32(vec8(0x77n));
303
+ const [resp] = session(call(2, 'cas_get', { hash: absent }));
304
+ assertEq(resultOf(resp).isError, true);
305
+ },
306
+ getMetaInvalidHashIsError: () => {
307
+ const [resp] = session(call(2, 'cas_get', { hash: 'bad!' }));
308
+ assertEq(resultOf(resp).isError, true);
309
+ },
310
+ };
@@ -6,7 +6,9 @@
6
6
  import { type Sha2 } from '../crypto/sha2/module.f.ts';
7
7
  import type { Vec } from '../types/bit_vec/module.f.ts';
8
8
  import { type Effect, type Operation } from '../effects/module.f.ts';
9
- import { type Fs, type NodeOp, type NodeProgramOptions } from '../effects/node/module.f.ts';
9
+ import { type Access, type All, type Mkdir, type NodeProgramOptions, type Read, type Readdir, type ReadFile, type Write, type WriteFile } from '../effects/node/module.f.ts';
10
+ import { type Commands } from '../cli/module.f.ts';
11
+ import type { MemOp } from '../effects/memory/module.f.ts';
10
12
  export type KvStore<O extends Operation> = {
11
13
  /** Reads a value by key; returns `undefined` when the key does not exist. */
12
14
  readonly read: (key: Vec) => Effect<O, Vec | undefined>;
@@ -17,10 +19,13 @@ export type KvStore<O extends Operation> = {
17
19
  };
18
20
  /** A key/value tuple where index `0` is the key and index `1` is the value. */
19
21
  export type Kv = readonly [Vec, Vec];
22
+ /** Converts a content key to its sharded relative CAS file path. */
23
+ export declare const toPath: (key: Vec) => string;
24
+ export type FileKvStoreOperation = ReadFile | Mkdir | WriteFile | Access | Readdir;
20
25
  /**
21
26
  * Creates a filesystem-backed key/value store under the provided root path.
22
27
  */
23
- export declare const fileKvStore: (path: string) => KvStore<Fs>;
28
+ export declare const fileKvStore: (path: string) => KvStore<FileKvStoreOperation>;
24
29
  export type Cas<O extends Operation> = {
25
30
  /** Reads content by hash; returns `undefined` when not found. */
26
31
  readonly read: (key: Vec) => Effect<O, Vec | undefined>;
@@ -33,4 +38,5 @@ export type Cas<O extends Operation> = {
33
38
  * Builds a content-addressable storage facade from a SHA-2 implementation.
34
39
  */
35
40
  export declare const cas: (sha2: Sha2) => <O extends Operation>(_: KvStore<O>) => Cas<O>;
36
- export declare const main: (options: NodeProgramOptions) => Effect<NodeOp, number>;
41
+ export declare const commands: Commands<FileKvStoreOperation | Write | All | MemOp | Read>;
42
+ export declare const main: (options: NodeProgramOptions) => Effect<All | Read | Write | FileKvStoreOperation | MemOp, number>;
@@ -4,45 +4,70 @@
4
4
  * @module
5
5
  */
6
6
  import { computeSync, sha256 } from "../crypto/sha2/module.f.js";
7
- import { parse } from "../path/module.f.js";
7
+ import { join, normalize, parse } from "../path/module.f.js";
8
8
  import { cBase32ToVec, vecToCBase32 } from "../cbase32/module.f.js";
9
9
  import { forEachStep, pure } from "../effects/module.f.js";
10
- import { errorExit, log, mkdir, readdir, readFile, writeFile } from "../effects/node/module.f.js";
10
+ import { access, errorExit, isNotFound, log, mkdir, readdir, readFile, writeFile } from "../effects/node/module.f.js";
11
11
  import { dispatch } from "../cli/module.f.js";
12
+ import { casMcpServer } from "./mcp/module.f.js";
12
13
  import { toOption } from "../types/nullable/module.f.js";
13
14
  import { unwrap } from "../types/result/module.f.js";
15
+ import { splitAt } from "../types/string/module.f.js";
14
16
  const o = { withFileTypes: true };
15
- const split = (s) => [s.substring(0, 2), s.substring(2)];
17
+ const split2 = splitAt(2);
16
18
  const prefix = '.cas';
17
19
  /** Converts a content key to its sharded relative CAS file path. */
18
- const toPath = (key) => {
20
+ export const toPath = (key) => {
19
21
  const s = vecToCBase32(key);
20
- const [a, bc] = split(s);
21
- const [b, c] = split(bc);
22
- return `${prefix}/${a}/${b}/${c}`;
22
+ const [a, bc] = split2(s);
23
+ const [b, c] = split2(bc);
24
+ return join(prefix, a, b, c);
23
25
  };
24
26
  /**
25
27
  * Creates a filesystem-backed key/value store under the provided root path.
26
28
  */
27
- export const fileKvStore = (path) => ({
28
- read: (key) => readFile(toPath(key))
29
- .step(([status, data]) => pure(status === 'error' ? undefined : data)),
30
- write: (key, value) => {
31
- const p = toPath(key);
32
- const parts = parse(p);
33
- const dir = `${path}/${parts.slice(0, -1).join('/')}`;
34
- // TODO: error handling
35
- return mkdir(dir, { recursive: true })
36
- .step(() => writeFile(`${path}/${p}`, value))
37
- .step(() => pure(undefined));
38
- },
39
- list: () =>
40
- // TODO: remove unwrap
41
- readdir('.cas', { recursive: true })
42
- .step(r => pure(unwrap(r).flatMap(({ name, parentPath, isFile }) => toOption(isFile
43
- ? cBase32ToVec(parentPath.substring(prefix.length).replaceAll('/', '') + name)
44
- : null)))),
45
- });
29
+ export const fileKvStore = (path) => {
30
+ const storePrefix = join(path, prefix);
31
+ const normalizedStorePrefix = normalize(storePrefix);
32
+ return {
33
+ read: (key) => readFile(join(path, toPath(key))).step(r => {
34
+ if (r[0] === 'ok') {
35
+ return pure(r[1]);
36
+ }
37
+ // A missing file means "no such content"; surface anything else.
38
+ if (isNotFound(r[1])) {
39
+ return pure(undefined);
40
+ }
41
+ throw r[1];
42
+ }),
43
+ write: (key, value) => {
44
+ const p = toPath(key);
45
+ const parts = parse(p);
46
+ const dir = join(path, ...parts.slice(0, -1));
47
+ // TODO: error handling
48
+ return mkdir(dir, { recursive: true })
49
+ .step(() => writeFile(join(path, p), value))
50
+ .step(() => pure(undefined));
51
+ },
52
+ list: () =>
53
+ // A fresh store has no `.cas` directory yet. Treat *only* that case as an
54
+ // empty store, mirroring how `read` maps a missing file to `undefined`.
55
+ // A `.cas` that exists but cannot be read (permissions, corruption) is a
56
+ // genuine storage error and is surfaced, not masked as "no hashes".
57
+ access(storePrefix).step(a => {
58
+ if (a[0] === 'error') {
59
+ if (isNotFound(a[1])) {
60
+ return pure([]);
61
+ }
62
+ throw a[1];
63
+ }
64
+ return readdir(storePrefix, { recursive: true })
65
+ .step(r => pure(unwrap(r).flatMap(({ name, parentPath, isFile }) => toOption(isFile
66
+ ? cBase32ToVec(normalize(parentPath).substring(normalizedStorePrefix.length).replaceAll('/', '') + name)
67
+ : null))));
68
+ }),
69
+ };
70
+ };
46
71
  /**
47
72
  * Builds a content-addressable storage facade from a SHA-2 implementation.
48
73
  */
@@ -58,50 +83,60 @@ export const cas = (sha2) => {
58
83
  list,
59
84
  });
60
85
  };
61
- export const main = (options) => {
62
- const c = cas(sha256)(fileKvStore('.'));
63
- const commands = [
64
- {
65
- names: ['add'],
66
- description: 'Store file content and print its hash',
67
- handler: ({ args: [path, ...rest] }) => {
68
- if (path === undefined || rest.length !== 0) {
69
- return errorExit("'cas add' expects one parameter");
70
- }
71
- return readFile(path)
72
- .step(v => c.write(unwrap(v)))
73
- .step(hash => log(vecToCBase32(hash)))
74
- .step(() => pure(0));
75
- },
86
+ export const commands = [
87
+ {
88
+ names: ['add'],
89
+ description: 'Store file content and print its hash',
90
+ handler: ({ home, args: [path, ...rest] }) => {
91
+ if (path === undefined || rest.length !== 0) {
92
+ return errorExit("'cas add' expects one parameter");
93
+ }
94
+ const c = cas(sha256)(fileKvStore(home));
95
+ return readFile(path)
96
+ .step(v => c.write(unwrap(v)))
97
+ .step(hash => log(vecToCBase32(hash)))
98
+ .step(() => pure(0));
76
99
  },
77
- {
78
- names: ['get'],
79
- description: 'Restore content by hash into a file',
80
- handler: ({ args: [hashCBase32, path, ...rest] }) => {
81
- if (hashCBase32 === undefined || path === undefined || rest.length !== 0) {
82
- return errorExit("'cas get' expects two parameters");
83
- }
84
- const hash = cBase32ToVec(hashCBase32);
85
- if (hash === null) {
86
- return errorExit(`invalid hash format: ${hashCBase32}`);
87
- }
88
- return c.read(hash)
89
- .step(v => {
90
- const result = v === undefined
91
- ? errorExit(`no such hash: ${hashCBase32}`)
92
- : writeFile(path, v)
93
- .step(() => pure(0));
94
- return result;
95
- });
96
- },
100
+ },
101
+ {
102
+ names: ['get'],
103
+ description: 'Restore content by hash into a file',
104
+ handler: ({ home, args: [hashCBase32, path, ...rest] }) => {
105
+ if (hashCBase32 === undefined || path === undefined || rest.length !== 0) {
106
+ return errorExit("'cas get' expects two parameters");
107
+ }
108
+ const hash = cBase32ToVec(hashCBase32);
109
+ if (hash === null) {
110
+ return errorExit(`invalid hash format: ${hashCBase32}`);
111
+ }
112
+ const c = cas(sha256)(fileKvStore(home));
113
+ return c.read(hash)
114
+ .step(v => {
115
+ const result = v === undefined
116
+ ? errorExit(`no such hash: ${hashCBase32}`)
117
+ : writeFile(path, v)
118
+ .step(() => pure(0));
119
+ return result;
120
+ });
97
121
  },
98
- {
99
- names: ['list'],
100
- description: 'List all stored content hashes',
101
- handler: () => c.list()
122
+ },
123
+ {
124
+ names: ['list'],
125
+ description: 'List all stored content hashes',
126
+ handler: ({ home }) => {
127
+ const c = cas(sha256)(fileKvStore(home));
128
+ return c.list()
102
129
  .step(forEachStep(j => log(vecToCBase32(j))))
103
- .step(() => pure(0)),
130
+ .step(() => pure(0));
104
131
  },
105
- ];
106
- return dispatch(commands)(options);
107
- };
132
+ },
133
+ {
134
+ names: ['mcp'],
135
+ description: 'Run an MCP server over stdio exposing the CAS as tools',
136
+ handler: ({ home }) => {
137
+ const c = cas(sha256)(fileKvStore(home));
138
+ return casMcpServer(c, hash => join(home, toPath(hash))).step(() => pure(0));
139
+ },
140
+ },
141
+ ];
142
+ export const main = dispatch(commands);
@@ -6,6 +6,8 @@ export declare const proof: {
6
6
  mainGetWrongArgs: () => void;
7
7
  mainGetInvalidHash: () => void;
8
8
  mainList: () => void;
9
+ mainListEmptyStore: () => void;
10
+ mainListCorruptStore: () => void;
9
11
  mainNoCmd: () => void;
10
12
  mainUnknownCmd: () => void;
11
13
  casWrite: () => void;
package/fs/cas/proof.f.js CHANGED
@@ -1,18 +1,21 @@
1
- import { cas, main } from "./module.f.js";
1
+ import { cas, commands } from "./module.f.js";
2
2
  import { sha256 } from "../crypto/sha2/module.f.js";
3
3
  import { empty, length, vec8 } from "../types/bit_vec/module.f.js";
4
4
  import { pure } from "../effects/module.f.js";
5
5
  import { run } from "../effects/mock/module.f.js";
6
6
  import { emptyState, virtual } from "../effects/node/virtual/module.f.js";
7
+ import { dispatch } from "../cli/module.f.js";
7
8
  const makeOptions = (args) => ({
8
9
  args,
9
10
  env: {},
11
+ home: '.',
10
12
  std: { stdout: { isTTY: false }, stderr: { isTTY: false } },
11
13
  testContext: { test: async () => { } },
12
14
  bunTestContext: { test: async () => { } },
13
15
  playwrightTestContext: { test: async () => { } },
14
16
  engine: 'node',
15
17
  });
18
+ const main = dispatch(commands);
16
19
  export const proof = {
17
20
  mainAdd: () => {
18
21
  const content = vec8(0x2an);
@@ -89,6 +92,32 @@ export const proof = {
89
92
  throw ['expected exit 0', exitCode];
90
93
  }
91
94
  },
95
+ mainListEmptyStore: () => {
96
+ // A fresh directory has no `.cas` yet; listing must succeed (empty),
97
+ // not crash unwrapping a readdir ENOENT.
98
+ const [finalState, exitCode] = virtual(emptyState)(main(makeOptions(['list'])));
99
+ if (exitCode !== 0) {
100
+ throw ['expected exit 0', exitCode];
101
+ }
102
+ if (finalState.stdout !== '') {
103
+ throw ['expected empty stdout', finalState.stdout];
104
+ }
105
+ },
106
+ mainListCorruptStore: () => {
107
+ // `.cas` exists but is a file, not a directory: a real storage error
108
+ // that must surface, not be masked as an empty list.
109
+ const state = { ...emptyState, root: { '.cas': vec8(0x2an) } };
110
+ let threw = false;
111
+ try {
112
+ virtual(state)(main(makeOptions(['list'])));
113
+ }
114
+ catch {
115
+ threw = true;
116
+ }
117
+ if (!threw) {
118
+ throw 'expected list to surface the storage error';
119
+ }
120
+ },
92
121
  mainNoCmd: () => {
93
122
  const [finalState, exitCode] = virtual(emptyState)(main(makeOptions([])));
94
123
  if (exitCode !== 1) {
@@ -6,6 +6,6 @@
6
6
  import { type Vec } from "../types/bit_vec/module.f.ts";
7
7
  import type { Nullable } from "../types/nullable/module.f.ts";
8
8
  export declare const vec5xToCBase32: (v: Vec) => string;
9
- export declare const vecToCBase32: (v: Vec) => string;
10
9
  export declare const cBase32ToVec5x: (s: string) => Nullable<Vec>;
10
+ export declare const vecToCBase32: (v: Vec) => string;
11
11
  export declare const cBase32ToVec: (s: string) => Nullable<Vec>;