functionalscript 0.32.4 → 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.
- package/README.md +1 -1
- package/fs/asserts/module.f.d.ts +1 -1
- package/fs/asserts/module.f.js +4 -1
- package/fs/bnf/data/module.f.d.ts +0 -103
- package/fs/bnf/data/module.f.js +10 -246
- package/fs/bnf/data/proof.f.d.ts +0 -7
- package/fs/bnf/data/proof.f.js +3 -636
- package/fs/bnf/descent/module.f.d.ts +56 -0
- package/fs/bnf/descent/module.f.js +119 -0
- package/fs/bnf/descent/proof.f.d.ts +5 -0
- package/fs/bnf/descent/proof.f.js +296 -0
- package/fs/bnf/ll1/module.f.d.ts +72 -0
- package/fs/bnf/ll1/module.f.js +153 -0
- package/fs/bnf/ll1/proof.f.d.ts +9 -0
- package/fs/bnf/ll1/proof.f.js +358 -0
- package/fs/cas/cli/module.f.d.ts +6 -0
- package/fs/cas/cli/module.f.js +56 -0
- package/fs/cas/cli/proof.f.d.ts +14 -0
- package/fs/cas/cli/proof.f.js +149 -0
- package/fs/cas/mcp/module.f.d.ts +9 -17
- package/fs/cas/mcp/module.f.js +117 -83
- package/fs/cas/mcp/proof.f.d.ts +5 -1
- package/fs/cas/mcp/proof.f.js +79 -32
- package/fs/cas/module.f.d.ts +33 -27
- package/fs/cas/module.f.js +151 -126
- package/fs/cas/proof.f.d.ts +16 -13
- package/fs/cas/proof.f.js +255 -130
- package/fs/ci/config/module.f.d.ts +15 -15
- package/fs/ci/config/module.f.js +15 -15
- package/fs/ci/proof.f.js +8 -6
- package/fs/cli/proof.f.js +2 -12
- package/fs/djs/parser/module.f.d.ts +0 -5
- package/fs/djs/proof.f.js +5 -6
- package/fs/djs/serializer/module.f.d.ts +1 -3
- package/fs/djs/tokenizer-new/module.f.js +3 -2
- package/fs/djs/tokenizer-new/proof.f.d.ts +3 -0
- package/fs/djs/tokenizer-new/proof.f.js +458 -296
- package/fs/djs/transpiler/proof.f.js +10 -10
- package/fs/effects/list/module.f.d.ts +31 -0
- package/fs/effects/list/module.f.js +16 -0
- package/fs/effects/module.f.d.ts +11 -5
- package/fs/effects/module.f.js +16 -6
- package/fs/effects/node/module.f.d.ts +29 -3
- package/fs/effects/node/module.f.js +35 -1
- package/fs/effects/node/module.js +32 -18
- package/fs/effects/node/proof.f.js +36 -36
- package/fs/effects/node/virtual/module.f.d.ts +16 -2
- package/fs/effects/node/virtual/module.f.js +129 -20
- package/fs/effects/node/virtual/proof.f.d.ts +4 -0
- package/fs/effects/node/virtual/proof.f.js +47 -14
- package/fs/effects/proof.f.d.ts +4 -0
- package/fs/effects/proof.f.js +18 -1
- package/fs/emergent_testing/proof.f.js +3 -11
- package/fs/fjs/module.f.js +2 -8
- package/fs/fjs/proof.f.js +2 -11
- package/fs/html/module.f.js +6 -8
- package/fs/js/tokenizer/module.f.d.ts +5 -0
- package/fs/js/tokenizer/module.f.js +13 -0
- package/fs/json/serializer/module.f.js +0 -1
- package/fs/mcp/module.f.d.ts +1 -1
- package/fs/mime/module.f.d.ts +101 -5
- package/fs/mime/module.f.js +159 -6
- package/fs/mime/proof.f.d.ts +25 -0
- package/fs/mime/proof.f.js +193 -3
- package/fs/text/code_point/module.f.d.ts +39 -2
- package/fs/text/code_point/module.f.js +74 -2
- package/fs/text/code_point/proof.f.d.ts +8 -0
- package/fs/text/code_point/proof.f.js +72 -0
- package/fs/text/sgr/proof.f.js +2 -11
- package/fs/text/utf16/module.f.js +4 -26
- package/fs/text/utf8/module.f.d.ts +27 -0
- package/fs/text/utf8/module.f.js +8 -14
- package/fs/types/bigint/module.f.d.ts +8 -15
- package/fs/types/bigint/module.f.js +9 -22
- package/fs/types/bigint/proof.f.d.ts +0 -2
- package/fs/types/bigint/proof.f.js +6 -16
- package/fs/types/function/operator/module.f.d.ts +26 -0
- package/fs/types/list/module.f.d.ts +3 -0
- package/fs/types/result/module.d.ts +2 -4
- package/fs/types/result/module.js +9 -3
- package/package.json +3 -3
- package/issues/demo/data/data.f.js +0 -12
- package/issues/demo/data/shared.f.js +0 -3
- package/issues/demo/fs/app.js +0 -4
- package/issues/demo/fs/math.f.js +0 -4
- package/issues/demo/sample/proof.f.js +0 -13
- /package/{issues → todo}/031-json.f.d.ts +0 -0
- /package/{issues → todo}/031-json.f.js +0 -0
- /package/{issues → todo}/proof.f.d.ts +0 -0
- /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
|
+
};
|
package/fs/cas/mcp/module.f.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { type Effect
|
|
1
|
+
import { type Effect } from '../../effects/module.f.ts';
|
|
2
2
|
import { type MemOp } from '../../effects/memory/module.f.ts';
|
|
3
|
-
import { type
|
|
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
|
|
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<
|
|
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
|
|
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:
|
|
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.
|
|
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:
|
|
32
|
+
export declare const casMcpServer: (home: string) => Effect<Read | Write | MemOp | FileCasOperation | Rm, void>;
|
package/fs/cas/mcp/module.f.js
CHANGED
|
@@ -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'`).
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 3. **Fallback
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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 {
|
|
73
|
-
import { length as bitVecLength } from "../../types/bit_vec/module.f.js";
|
|
74
|
-
import {} from "../../
|
|
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 {
|
|
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 = {
|
|
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 = {
|
|
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 = (
|
|
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' —
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,63 +162,54 @@ const casToolRegistry = (c, home, toUrl) => {
|
|
|
112
162
|
}
|
|
113
163
|
return x.step(value => typeof value === 'string'
|
|
114
164
|
? pure(errorResult(value))
|
|
115
|
-
|
|
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'
|
|
167
|
+
? errorResult('write')
|
|
168
|
+
: okResult(vecToCBase32(hashResult[1])))));
|
|
116
169
|
}),
|
|
117
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 => {
|
|
118
171
|
const key = cBase32ToVec(r.hash);
|
|
119
172
|
if (key === null) {
|
|
120
173
|
return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
|
|
121
174
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const meta = {
|
|
132
|
-
length: byteLength,
|
|
133
|
-
mime_type: detectedMime,
|
|
134
|
-
type: 'base64',
|
|
135
|
-
...(url !== undefined && { url })
|
|
136
|
-
};
|
|
137
|
-
if (r.content === true) {
|
|
138
|
-
const blob = base64Encode(value);
|
|
139
|
-
return pure(blob === null
|
|
140
|
-
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
141
|
-
: 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}`));
|
|
142
184
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
return pure(r.content === true
|
|
156
|
-
? okResult(JSON.stringify({ ...meta, content: str }))
|
|
157
|
-
: 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}`));
|
|
158
197
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
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
|
|
168
206
|
? errorResult(`content is not byte-aligned: ${r.hash}`)
|
|
169
|
-
: okResult(
|
|
207
|
+
: okResult(toJson({ ...meta, content: str })));
|
|
170
208
|
}
|
|
171
|
-
|
|
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 })));
|
|
172
213
|
});
|
|
173
214
|
}),
|
|
174
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'))))),
|
|
@@ -179,14 +220,9 @@ const casToolRegistry = (c, home, toUrl) => {
|
|
|
179
220
|
const okResult = (text) => ({ content: [{ type: 'text', text }] });
|
|
180
221
|
// ── Handlers ────────────────────────────────────────────────────────────────────
|
|
181
222
|
/**
|
|
182
|
-
* MCP handlers for
|
|
183
|
-
* itself, so the same handlers run over `Fs` (production) or memory (tests).
|
|
184
|
-
*
|
|
185
|
-
* When `toUrl` is provided, `cas_get` includes the `url` field pointing to
|
|
186
|
-
* the blob on the local filesystem. When absent (e.g. memory-backed tests),
|
|
187
|
-
* `url` is omitted.
|
|
223
|
+
* MCP handlers for `FileCas`.
|
|
188
224
|
*/
|
|
189
|
-
export const casMcpHandlers = (
|
|
225
|
+
export const casMcpHandlers = (home) => fromRegistry(casToolRegistry(home));
|
|
190
226
|
// ── Session configuration ───────────────────────────────────────────────────────
|
|
191
227
|
/**
|
|
192
228
|
* Static MCP configuration for the CAS server: advertises the `tools`
|
|
@@ -199,10 +235,8 @@ export const casConfig = {
|
|
|
199
235
|
};
|
|
200
236
|
// ── Server ──────────────────────────────────────────────────────────────────────
|
|
201
237
|
/**
|
|
202
|
-
* 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
|
|
203
239
|
* the `mcpStep` for `c`, and drives the read → parse → dispatch → write loop
|
|
204
|
-
* until stdin EOF.
|
|
205
|
-
*
|
|
206
|
-
* When `toUrl` is provided, `cas_get` includes the blob's filesystem URL.
|
|
240
|
+
* until stdin EOF.
|
|
207
241
|
*/
|
|
208
|
-
export const casMcpServer = (
|
|
242
|
+
export const casMcpServer = (home) => create(uninitializedState).step(key => stdioTransport(mcpStep(casConfig)(casMcpHandlers(home))(key)));
|
package/fs/cas/mcp/proof.f.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|