functionalscript 0.34.0 → 0.35.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.
- package/README.md +26 -13
- package/fs/base64/proof.f.d.ts +2 -0
- package/fs/base64/proof.f.js +50 -57
- package/fs/base_n/module.f.d.ts +4 -3
- package/fs/base_n/module.f.js +33 -15
- package/fs/base_n/proof.f.d.ts +1 -0
- package/fs/base_n/proof.f.js +12 -1
- package/fs/cas/mcp/module.f.js +52 -57
- package/fs/cas/mcp/proof.f.d.ts +9 -0
- package/fs/cas/mcp/proof.f.js +179 -0
- package/fs/mcp/stdio/module.f.d.ts +8 -0
- package/fs/mcp/stdio/module.f.js +33 -6
- package/fs/mcp/stdio/proof.f.d.ts +3 -0
- package/fs/mcp/stdio/proof.f.js +52 -1
- package/fs/text/module.f.d.ts +11 -1
- package/fs/text/module.f.js +13 -3
- package/fs/text/proof.f.d.ts +6 -0
- package/fs/text/proof.f.js +25 -2
- package/fs/types/bit_vec/module.f.d.ts +20 -2
- package/fs/types/bit_vec/module.f.js +77 -27
- package/fs/types/bit_vec/proof.f.d.ts +8 -0
- package/fs/types/bit_vec/proof.f.js +24 -6
- package/fs/types/nullable/module.f.d.ts +9 -5
- package/fs/types/nullable/module.f.js +19 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -28,22 +28,39 @@ Install FunctionalScript via npm:
|
|
|
28
28
|
npm install -g functionalscript
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
The
|
|
31
|
+
The `fjs` CLI provides several commands:
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|------------------|-----------------------------------------------------------|
|
|
35
|
+
| `fjs test` / `t` | Run the FunctionalScript test suite |
|
|
36
|
+
| `fjs compile` / `c` | Compile a `.f.ts` module to JavaScript |
|
|
37
|
+
| `fjs run` / `r` | Run a FunctionalScript module as a Node program |
|
|
38
|
+
| `fjs cas` / `s` | Content-addressable storage (`add`, `get`, `list`) |
|
|
39
|
+
| `fjs mcp` / `m` | Run an MCP server over stdio exposing the CAS as tools |
|
|
40
|
+
| `fjs ci` / `i` | Generate the GitHub Actions CI workflow |
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
### Content-Addressable Storage (CAS)
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
FunctionalScript ships a built-in CAS for storing and retrieving blobs by their cryptographic hash:
|
|
39
45
|
|
|
40
46
|
```bash
|
|
41
|
-
fjs
|
|
42
|
-
#
|
|
43
|
-
fjs
|
|
47
|
+
fjs cas add myfile.txt # store a file, print its cBase32 hash
|
|
48
|
+
fjs cas get <hash> # restore a blob by hash
|
|
49
|
+
fjs cas list # list all stored hashes
|
|
44
50
|
```
|
|
45
51
|
|
|
46
|
-
|
|
52
|
+
Blobs are stored under `~/.cas/` and addressed by their SHA-256 hash encoded in cBase32.
|
|
53
|
+
|
|
54
|
+
### MCP Server
|
|
55
|
+
|
|
56
|
+
The CAS is also exposed as an [MCP](https://modelcontextprotocol.io/) server so LLM agents can read and write blobs without a shell:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# register with Claude CLI
|
|
60
|
+
claude mcp add cas -- npx functionalscript m
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
See [`fs/cas/mcp/README.md`](fs/cas/mcp/README.md) for details on the `cas_add`, `cas_get`, and `cas_list` tools.
|
|
47
64
|
|
|
48
65
|
## Vision
|
|
49
66
|
|
|
@@ -67,10 +84,6 @@ In FunctionalScript:
|
|
|
67
84
|
- A module can depend only on another FunctionalScript module.
|
|
68
85
|
- It also has no standard library. Only a safe subset of standard JavaScript API can be used without referencing other modules.
|
|
69
86
|
|
|
70
|
-
## Our Next Step
|
|
71
|
-
|
|
72
|
-
[Re-architecture of NaNVM](https://medium.com/@sergeyshandar/nanvm-re-architecture-8097f766ec1c?sk=d14ec1daf73ac5442f12ce20b2bc037a).
|
|
73
|
-
|
|
74
87
|
## Sponsors
|
|
75
88
|
|
|
76
89
|
- [KirillOsenkov](https://github.com/KirillOsenkov),
|
package/fs/base64/proof.f.d.ts
CHANGED
package/fs/base64/proof.f.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertEq } from "../asserts/module.f.js";
|
|
2
|
+
import { empty, vec, repeat, vec8 } from "../types/bit_vec/module.f.js";
|
|
2
3
|
import { encode, decode } from "./module.f.js";
|
|
3
4
|
const check = (s, v) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
throw ['encode', sr, s];
|
|
7
|
-
}
|
|
8
|
-
const vr = decode(s);
|
|
9
|
-
if (vr !== v) {
|
|
10
|
-
throw ['decode', vr, v];
|
|
11
|
-
}
|
|
5
|
+
assertEq(encode(v), s);
|
|
6
|
+
assertEq(decode(s), v);
|
|
12
7
|
};
|
|
13
8
|
export const proof = {
|
|
14
9
|
empty: () => {
|
|
@@ -46,62 +41,29 @@ export const proof = {
|
|
|
46
41
|
},
|
|
47
42
|
nonOctet: () => {
|
|
48
43
|
// encode rejects bit vectors whose length is not a multiple of 8
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (encode(vec(6n)(0n)) !== null) {
|
|
53
|
-
throw '6-bit input should return null';
|
|
54
|
-
}
|
|
55
|
-
if (encode(vec(12n)(0n)) !== null) {
|
|
56
|
-
throw '12-bit input should return null';
|
|
57
|
-
}
|
|
44
|
+
assertEq(encode(vec(1n)(0n)), null);
|
|
45
|
+
assertEq(encode(vec(6n)(0n)), null);
|
|
46
|
+
assertEq(encode(vec(12n)(0n)), null);
|
|
58
47
|
},
|
|
59
48
|
invalidInput: () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw 'three equals signs should return null';
|
|
68
|
-
}
|
|
69
|
-
if (decode('A') !== null) {
|
|
70
|
-
throw 'length 1 (not multiple of 4) should return null';
|
|
71
|
-
}
|
|
72
|
-
if (decode('AA') !== null) {
|
|
73
|
-
throw 'length 2 (no padding) should return null';
|
|
74
|
-
}
|
|
75
|
-
if (decode('AAA') !== null) {
|
|
76
|
-
throw 'length 3 (no padding) should return null';
|
|
77
|
-
}
|
|
78
|
-
if (decode('=AAA') !== null) {
|
|
79
|
-
throw 'padding not at end should return null';
|
|
80
|
-
}
|
|
49
|
+
assertEq(decode('!'), null);
|
|
50
|
+
assertEq(decode('A!AA'), null);
|
|
51
|
+
assertEq(decode('A==='), null);
|
|
52
|
+
assertEq(decode('A'), null);
|
|
53
|
+
assertEq(decode('AA'), null);
|
|
54
|
+
assertEq(decode('AAA'), null);
|
|
55
|
+
assertEq(decode('=AAA'), null);
|
|
81
56
|
// Non-zero padding bits must be rejected (RFC 4648 §3.5)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
if (decode('AAB=') !== null) {
|
|
86
|
-
throw 'non-zero 2-pad-bit should return null';
|
|
87
|
-
}
|
|
57
|
+
assertEq(decode('AB=='), null);
|
|
58
|
+
assertEq(decode('AAB='), null);
|
|
88
59
|
},
|
|
89
60
|
validPadding: () => {
|
|
90
61
|
// "==" padding
|
|
91
|
-
|
|
92
|
-
if (v2 !== vec(8n)(0n)) {
|
|
93
|
-
throw ['AA== should decode to 8-bit zero', v2];
|
|
94
|
-
}
|
|
62
|
+
assertEq(decode('AA=='), vec(8n)(0n));
|
|
95
63
|
// "=" padding
|
|
96
|
-
|
|
97
|
-
if (v1 !== vec(16n)(0n)) {
|
|
98
|
-
throw ['AAA= should decode to 16-bit zero', v1];
|
|
99
|
-
}
|
|
64
|
+
assertEq(decode('AAA='), vec(16n)(0n));
|
|
100
65
|
// No padding
|
|
101
|
-
|
|
102
|
-
if (v0 !== vec(24n)(0n)) {
|
|
103
|
-
throw ['AAAA should decode to 24-bit zero', v0];
|
|
104
|
-
}
|
|
66
|
+
assertEq(decode('AAAA'), vec(24n)(0n));
|
|
105
67
|
},
|
|
106
68
|
knownVectors: () => {
|
|
107
69
|
// RFC 4648 §10 test vectors (byte-aligned, treating bytes as 8-bit MSB vectors)
|
|
@@ -114,4 +76,35 @@ export const proof = {
|
|
|
114
76
|
// 0x66 0x6f 0x6f ('foo') → Zm9v
|
|
115
77
|
check('Zm9v', vec(24n)(0x666f6fn));
|
|
116
78
|
},
|
|
79
|
+
decodeOverflow: () => {
|
|
80
|
+
// 174_764 base64 chars decode to 1_048_584 bits, 8 over `maxLength`;
|
|
81
|
+
// `decode` should return `null` instead of throwing on (or hanging
|
|
82
|
+
// while building) an oversized `bigint`.
|
|
83
|
+
const oversized = 'A'.repeat(174_764);
|
|
84
|
+
assertEq(decode(oversized), null);
|
|
85
|
+
},
|
|
86
|
+
// Regression guard: `encode` used to be quadratic in the input size, not
|
|
87
|
+
// linear. `baseN`'s `vecToString` (`fs/base_n/module.f.ts`) popped one
|
|
88
|
+
// 6-bit chunk off the front at a time via `popFront`, and each `popFront`
|
|
89
|
+
// re-masked the entire remaining bigint through `vec()`'s `m & ui` —
|
|
90
|
+
// O(remaining length) — so encoding a vector of `n` bits cost O(n²). A
|
|
91
|
+
// 90,000-byte input took ~5.6s on Node and ~18.4s on Bun (Bun's `bigint`
|
|
92
|
+
// has a much higher per-operation constant, see PR #1190), reliably
|
|
93
|
+
// blowing `bun test`'s 5s per-test timeout — the same class of failure
|
|
94
|
+
// that hit `cas_get content:true` on a comparably-sized binary blob in CI
|
|
95
|
+
// (PR #1201).
|
|
96
|
+
//
|
|
97
|
+
// Fixed by rewriting `vecToString` as a balanced recursive split
|
|
98
|
+
// (`unpackToString`), mirroring the `chunkList`/`unpackChunkList`
|
|
99
|
+
// divide-and-conquer already used by `u8List` and by concatenation
|
|
100
|
+
// (`msb.concat` / `unpackListToVec`, PR #1192) — true O(n log n) now,
|
|
101
|
+
// since each half is masked to its own length before recursing. The same
|
|
102
|
+
// call now takes ~50ms on Node and ~225ms on Bun. No timing assertion
|
|
103
|
+
// (duration varies by engine/machine) — this just exercises the call and
|
|
104
|
+
// relies on the test runner's own per-test timing to catch a regression.
|
|
105
|
+
encodeLargeVecIsSlow: () => {
|
|
106
|
+
const big = repeat(90000n)(vec8(0xffn));
|
|
107
|
+
const result = encode(big);
|
|
108
|
+
assertEq(result?.length, 120_000);
|
|
109
|
+
},
|
|
117
110
|
};
|
package/fs/base_n/module.f.d.ts
CHANGED
|
@@ -18,9 +18,10 @@ import type { Nullable } from '../types/nullable/module.f.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
export type BaseN = {
|
|
20
20
|
/**
|
|
21
|
-
* Encodes a bit vector by
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Encodes a bit vector by splitting it into `bits`-wide MSB chunks
|
|
22
|
+
* (`chunkList`, a balanced divide-and-conquer split, not a linear scan)
|
|
23
|
+
* and indexing each into the alphabet. A trailing partial chunk shorter
|
|
24
|
+
* than `bits` is left-padded with zeros.
|
|
24
25
|
*/
|
|
25
26
|
readonly vecToString: (v: Vec) => string;
|
|
26
27
|
/**
|
package/fs/base_n/module.f.js
CHANGED
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
-
import { msb,
|
|
15
|
-
|
|
14
|
+
import { msb, lsb, vec, chunkList, unpack } from "../types/bit_vec/module.f.js";
|
|
15
|
+
import { fold } from "../types/list/module.f.js";
|
|
16
|
+
import { compose } from "../types/function/module.f.js";
|
|
17
|
+
const { unpackSplit } = msb;
|
|
18
|
+
const { tryListToVec: reversedListToVec } = lsb;
|
|
19
|
+
// `chunkList(msb)` doesn't depend on `bits` or `v` — shared across every
|
|
20
|
+
// `baseN(...)` codec (base64, cbase32, ...).
|
|
21
|
+
const chunkListMsb = chunkList(msb);
|
|
16
22
|
/**
|
|
17
23
|
* Builds a {@link BaseN} codec for a fixed chunk width and alphabet.
|
|
18
24
|
*
|
|
@@ -24,31 +30,43 @@ const { popFront, concat } = msb;
|
|
|
24
30
|
* `i`/`l`→`1`, `o`→`0`.
|
|
25
31
|
*/
|
|
26
32
|
export const baseN = (bits, alphabet, normalize) => {
|
|
27
|
-
const popFrontN = popFront(bits);
|
|
28
33
|
const vecN = vec(bits);
|
|
29
34
|
const toIndex = normalize === undefined
|
|
30
35
|
? (c) => alphabet.indexOf(c)
|
|
31
36
|
: (c) => alphabet.indexOf(normalize(c));
|
|
37
|
+
const unpackSplitBits = unpackSplit(bits);
|
|
38
|
+
// Converts one `<= bits`-wide chunk (as yielded by `chunkList`, already
|
|
39
|
+
// masked to its own length) to its alphabet index. A trailing partial
|
|
40
|
+
// chunk shorter than `bits` is left-padded with zeros: `unpackSplit`'s
|
|
41
|
+
// shift amount goes negative, which per spec becomes a left shift.
|
|
42
|
+
const chunkToIndex = (chunk) => {
|
|
43
|
+
const u = unpack(chunk);
|
|
44
|
+
return Number(u.length < bits ? unpackSplitBits(u)[0] : u.uint);
|
|
45
|
+
};
|
|
46
|
+
// Folds directly over `chunkList`'s lazy list in one pass — faster than
|
|
47
|
+
// `map` into a second lazy list before joining, since there's no second
|
|
48
|
+
// list to allocate/traverse.
|
|
49
|
+
const chunkToString = (chunk) => (acc) => acc + alphabet[chunkToIndex(chunk)];
|
|
32
50
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
v = rest;
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
},
|
|
51
|
+
// `chunkListMsb(bits)` then `fold(chunkToString)('')` — neither half
|
|
52
|
+
// depends on `v`, so `compose` builds (and this closure captures)
|
|
53
|
+
// the composed function once per `baseN(...)` codec; no need to name
|
|
54
|
+
// the halves separately just to get that one-time build.
|
|
55
|
+
vecToString: compose(chunkListMsb(bits))(fold(chunkToString)('')),
|
|
42
56
|
stringToVec: s => {
|
|
43
|
-
|
|
57
|
+
// Build a reversed chunk list, bailing out at the first invalid
|
|
58
|
+
// character so malformed input is rejected in O(prefix) time and
|
|
59
|
+
// `normalize` is never run past it. `listToVec` then concatenates in
|
|
60
|
+
// O(n log n).
|
|
61
|
+
let chunks = null;
|
|
44
62
|
for (const c of s) {
|
|
45
63
|
const index = toIndex(c);
|
|
46
64
|
if (index < 0) {
|
|
47
65
|
return null;
|
|
48
66
|
}
|
|
49
|
-
|
|
67
|
+
chunks = { first: vecN(BigInt(index)), tail: chunks };
|
|
50
68
|
}
|
|
51
|
-
return
|
|
69
|
+
return reversedListToVec(chunks);
|
|
52
70
|
},
|
|
53
71
|
};
|
|
54
72
|
};
|
package/fs/base_n/proof.f.d.ts
CHANGED
package/fs/base_n/proof.f.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertEq } from "../asserts/module.f.js";
|
|
2
|
+
import { empty, maxLength, vec, length } from "../types/bit_vec/module.f.js";
|
|
2
3
|
import { baseN } from "./module.f.js";
|
|
3
4
|
const hex = baseN(4n, '0123456789abcdef');
|
|
4
5
|
const cb32 = baseN(5n, '0123456789abcdefghjkmnpqrstvwxyz', c => {
|
|
@@ -18,6 +19,9 @@ const cb32 = baseN(5n, '0123456789abcdefghjkmnpqrstvwxyz', c => {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
});
|
|
22
|
+
// Sample input for the `big` proof below: 262 144 `f` characters decode into a
|
|
23
|
+
// 1 Mibit (`maxLength`) vector.
|
|
24
|
+
const bigSampleHex = `f`.repeat(Number(maxLength >> 2n));
|
|
21
25
|
export const proof = {
|
|
22
26
|
encodeEmpty: () => {
|
|
23
27
|
const s = hex.vecToString(empty);
|
|
@@ -83,4 +87,11 @@ export const proof = {
|
|
|
83
87
|
throw 'unknown char should return null';
|
|
84
88
|
}
|
|
85
89
|
},
|
|
90
|
+
// Decodes a 1 Mibit hex string. With the O(n log n) `listToVec` builder this
|
|
91
|
+
// runs in well under a second (was ~13 s node / ~43 s bun under the old
|
|
92
|
+
// per-chunk `concat`).
|
|
93
|
+
big: () => {
|
|
94
|
+
const x = hex.stringToVec(bigSampleHex);
|
|
95
|
+
assertEq(length(x), maxLength);
|
|
96
|
+
}
|
|
86
97
|
};
|
package/fs/cas/mcp/module.f.js
CHANGED
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
* only metadata was asked for; streaming detection returns correct
|
|
48
48
|
* `{ length, mime_type, type[, url] }` regardless of size.
|
|
49
49
|
*
|
|
50
|
-
* **`content: true`**
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
53
|
-
* `text`, base64 for `base64`. A
|
|
54
|
-
*
|
|
50
|
+
* **`content: true`** first derives `{ length, mime_type, type }` with the same
|
|
51
|
+
* size-independent `fs/mime` `detectStream` machine, then materializes the bytes
|
|
52
|
+
* (via `collectRead`, bounded by `maxLength`) and encodes the inline payload by
|
|
53
|
+
* `type` — a `fs/text/utf8` `fromVec` string for `text`, base64 for `base64`. A
|
|
54
|
+
* blob larger than `maxLength` (128 KiB) cannot be buffered into one `Vec`, so it
|
|
55
|
+
* is rejected here with a descriptive *"too large"* error (carrying the byte size
|
|
56
|
+
* and `url`) rather than being misreported as absent; it should be fetched via
|
|
57
|
+
* `url` or inspected with metadata-only `cas_get`.
|
|
55
58
|
*
|
|
56
59
|
* ## Encoding split: hashes vs. content
|
|
57
60
|
*
|
|
@@ -69,6 +72,8 @@
|
|
|
69
72
|
* - `type: 'base64'` with malformed content (base64 `decode` → `null`) → `isError`
|
|
70
73
|
* - malformed `hash` (`cBase32ToVec` → `null`) → `isError`
|
|
71
74
|
* - `cas_get` on an absent hash (`c.read` → `undefined`) → `isError`
|
|
75
|
+
* - `cas_get` with `content: true` on a blob larger than `maxLength` → `isError`
|
|
76
|
+
* (distinct "too large" message, not "no such hash")
|
|
72
77
|
* - unknown tool `name` → `isError`
|
|
73
78
|
*
|
|
74
79
|
* @module
|
|
@@ -79,9 +84,9 @@ import { pure } from "../../effects/module.f.js";
|
|
|
79
84
|
import { create } from "../../effects/memory/module.f.js";
|
|
80
85
|
import { cBase32ToVec, vecToCBase32 } from "../../cbase32/module.f.js";
|
|
81
86
|
import { decode as base64Decode, encode as base64Encode } from "../../base64/module.f.js";
|
|
82
|
-
import {
|
|
83
|
-
import {
|
|
84
|
-
import { empty, length as bitVecLength, maxLength, msb } from "../../types/bit_vec/module.f.js";
|
|
87
|
+
import { tryUtf8 } from "../../text/module.f.js";
|
|
88
|
+
import { detectStream } from "../../mime/module.f.js";
|
|
89
|
+
import { empty, length as bitVecLength, maxLength, maxLengthBytes, msb } from "../../types/bit_vec/module.f.js";
|
|
85
90
|
import { ok, error } from "../../types/result/module.f.js";
|
|
86
91
|
import { rm } from "../../effects/node/module.f.js";
|
|
87
92
|
import { stdioTransport } from "../../mcp/stdio/module.f.js";
|
|
@@ -139,7 +144,7 @@ const casToolRegistry = (home) => {
|
|
|
139
144
|
const c = fileCas(sha256)(home);
|
|
140
145
|
const casUploadDir = `${home}/cas_upload`;
|
|
141
146
|
return [
|
|
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)
|
|
147
|
+
toolEntry('cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; omit or pass type:"text" for UTF-8 text (default). Inline content (text/base64) is capped at 128 KiB (131072 bytes) — larger content is rejected. For larger content use type:"url" to stream a file from $HOME/cas_upload/ (no size limit).', casAddArgs, ({ type, content }) => {
|
|
143
148
|
// type:'url' — stream the file into cas, then delete the source on success
|
|
144
149
|
if (type === 'url') {
|
|
145
150
|
if (!content.startsWith(`${casUploadDir}/`) || content.includes('..')) {
|
|
@@ -150,66 +155,56 @@ const casToolRegistry = (home) => {
|
|
|
150
155
|
: rm(content).step(() => pure(okResult(vecToCBase32(v)))));
|
|
151
156
|
}
|
|
152
157
|
// type:'text' or 'base64' — resolve content to Vec, store via c.write()
|
|
153
|
-
let x
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
break;
|
|
159
|
-
default:
|
|
160
|
-
x = pure(utf8(content));
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
return x.step(value => typeof value === 'string'
|
|
164
|
-
? pure(errorResult(value))
|
|
158
|
+
let x = type === 'base64'
|
|
159
|
+
? base64Decode(content)
|
|
160
|
+
: tryUtf8(content);
|
|
161
|
+
return x === null
|
|
162
|
+
? pure(errorResult('too large or malformed — use type:"url" for large content'))
|
|
165
163
|
// The resolved content fits in one chunk; feed it as a single-item stream.
|
|
166
|
-
: c.write(nonEmpty(ok(
|
|
164
|
+
: c.write(nonEmpty(ok(x), elEmpty())).step(([tag, hash]) => pure(tag === 'error'
|
|
167
165
|
? errorResult('write')
|
|
168
|
-
: okResult(vecToCBase32(
|
|
166
|
+
: okResult(vecToCBase32(hash))));
|
|
169
167
|
}),
|
|
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 => {
|
|
168
|
+
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, but content is capped at 128 KiB (131072 bytes) — a larger blob is rejected with an error. To download a blob, prefer the url field returned in the result instead of requesting inline content.', casGetArgs, r => {
|
|
171
169
|
const key = cBase32ToVec(r.hash);
|
|
172
170
|
if (key === null) {
|
|
173
171
|
return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
|
|
174
172
|
}
|
|
175
173
|
const url = c.url(key);
|
|
176
|
-
|
|
177
|
-
|
|
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}`));
|
|
184
|
-
}
|
|
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') {
|
|
174
|
+
return detectStream(c.read(key)).step(([tag, detected]) => {
|
|
175
|
+
if (tag === 'error') {
|
|
196
176
|
return pure(errorResult(`no such hash: ${r.hash}`));
|
|
197
177
|
}
|
|
198
|
-
const
|
|
199
|
-
const { length, mime_type, type } = detectVec(value);
|
|
178
|
+
const { length, mime_type, type } = detected;
|
|
200
179
|
const meta = { length: Number(length), mime_type, type, url };
|
|
201
|
-
if (
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
180
|
+
if (r.content !== true) {
|
|
181
|
+
// content:true path continues below; this is just the metadata step.
|
|
182
|
+
return pure(okResult(toJson(meta)));
|
|
183
|
+
}
|
|
184
|
+
// A single `Vec` caps at `maxLength` bits (`maxLengthBytes` bytes), so
|
|
185
|
+
// a larger blob cannot be buffered for inline transfer. Report the
|
|
186
|
+
// size and point at the size-independent alternatives instead of
|
|
187
|
+
// misreporting an existing blob as `no such hash`.
|
|
188
|
+
if (length > maxLengthBytes) {
|
|
189
|
+
return pure(errorResult(`blob too large to fetch inline (${length} bytes, limit ${maxLengthBytes} bytes); use the url field (${url}) or omit content for metadata`));
|
|
208
190
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
191
|
+
return collectRead(c.read(key)).step(([collectTag, value]) => {
|
|
192
|
+
if (collectTag === 'error') {
|
|
193
|
+
return pure(errorResult(`no such hash: ${r.hash}`));
|
|
194
|
+
}
|
|
195
|
+
if (type === 'text') {
|
|
196
|
+
// `type: 'text'` means the detector validated `value` as UTF-8,
|
|
197
|
+
// so `fromVec` is non-null here; guard defensively regardless.
|
|
198
|
+
const str = fromVec(value);
|
|
199
|
+
return pure(str === null
|
|
200
|
+
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
201
|
+
: okResult(toJson({ ...meta, content: str })));
|
|
202
|
+
}
|
|
203
|
+
const blob = base64Encode(value);
|
|
204
|
+
return pure(blob === null
|
|
205
|
+
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
206
|
+
: okResult(toJson({ ...meta, content: blob })));
|
|
207
|
+
});
|
|
213
208
|
});
|
|
214
209
|
}),
|
|
215
210
|
toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
|
package/fs/cas/mcp/proof.f.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export declare const proof: {
|
|
|
2
2
|
getMetaLargeMultiChunkBlobNoError: () => void;
|
|
3
3
|
getMetaLargeMultiChunkUtf8Symbols: () => void;
|
|
4
4
|
getMetaLargeMultiChunkTextThenBinary: () => void;
|
|
5
|
+
getContentLargeBlobTooLargeError: () => void;
|
|
6
|
+
getContentMissingHashIsError: () => void;
|
|
7
|
+
getContentBase64InflationOverflowWritesInternalError: () => void;
|
|
8
|
+
getContentBase64NearBoundarySucceeds: () => void;
|
|
9
|
+
getContentDoubleEscapedOverflowWritesInternalError: () => void;
|
|
5
10
|
toolsListAdvertisesThreeTools: () => void;
|
|
6
11
|
addReturnsHash: () => void;
|
|
7
12
|
textAddGetMetaRoundTrips: () => void;
|
|
@@ -14,6 +19,10 @@ export declare const proof: {
|
|
|
14
19
|
listEnumeratesStoredHashes: () => void;
|
|
15
20
|
addInvalidContentIsError: () => void;
|
|
16
21
|
addBadLengthContentIsError: () => void;
|
|
22
|
+
addBase64OverLimitIsError: () => void;
|
|
23
|
+
addTextOverLimitIsError: () => void;
|
|
24
|
+
addTextAtLimitSucceeds: () => void;
|
|
25
|
+
addBase64AtLimitIsError: () => void;
|
|
17
26
|
getUnterminatedHashIsError: () => void;
|
|
18
27
|
addMissingContentIsError: () => void;
|
|
19
28
|
getMissingHashArgumentIsError: () => void;
|