@fungi.computer/knapsack 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -4
- package/dist/files-patch.d.ts +16 -2
- package/dist/files-patch.js +8 -1
- package/dist/files-read.d.ts +108 -3
- package/dist/files-read.js +77 -7
- package/dist/files-search.d.ts +14 -0
- package/dist/files-search.js +52 -0
- package/dist/files.d.ts +40 -46
- package/dist/files.js +171 -85
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/ui/browser.d.ts +44 -0
- package/dist/ui/browser.js +171 -0
- package/dist/ui/effect.d.ts +106 -0
- package/dist/ui/effect.js +208 -0
- package/dist/ui/host.d.ts +9 -0
- package/dist/ui/host.js +158 -0
- package/dist/ui/sessions.d.ts +32 -0
- package/dist/ui/sessions.js +172 -0
- package/dist/ui/wire.d.ts +290 -0
- package/dist/ui/wire.js +136 -0
- package/dist/ui-browser.d.ts +6 -0
- package/dist/ui-browser.js +6 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +35 -0
- package/docs/api/README.md +1 -0
- package/docs/api/files-patch.md +39 -43
- package/docs/api/files-read.md +263 -27
- package/docs/api/index.md +207 -107
- package/docs/api/ui-browser.md +94 -0
- package/package.json +16 -5
package/README.md
CHANGED
|
@@ -36,6 +36,15 @@ catalog or a result. Servers connect concurrently under a timeout. One that
|
|
|
36
36
|
rejects, refuses its credential, times out or advertises an unusable list is
|
|
37
37
|
dropped and reported through `onUnavailable`, while the others stay available.
|
|
38
38
|
|
|
39
|
+
`Knapsack.ui({ id, hasSession })` connects the App open in an Agent Session's
|
|
40
|
+
browser. Its `source` lends each Session run the `ui` module: `ui.discover`
|
|
41
|
+
lists the App's live Whistle commands (its agent projection) and `ui.execute`
|
|
42
|
+
runs one. The run returns the module when it ends. Its `fetch` serves
|
|
43
|
+
`/browser/sessions/<sessionId>/browser/{events,claims,results}`: the browser
|
|
44
|
+
claims each request before it runs it, and the host accepts only the matching
|
|
45
|
+
result. The browser side is `connectBrowser({ baseUrl, whistle })` from
|
|
46
|
+
`@fungi.computer/knapsack/ui/browser`.
|
|
47
|
+
|
|
39
48
|
Knapsack does not own model loops, tool execution policy, Computer storage, or
|
|
40
49
|
provider-specific operations.
|
|
41
50
|
|
|
@@ -43,6 +52,13 @@ provider-specific operations.
|
|
|
43
52
|
contracts without loading tool connections or the agent runtime. Browser and
|
|
44
53
|
transport consumers use this subpath.
|
|
45
54
|
|
|
55
|
+
`@fungi.computer/knapsack/files-search` supplies `SearchVisibility` to Local and
|
|
56
|
+
hosted Files adapters. One invocation-local policy checks canonical
|
|
57
|
+
root-relative search paths (the root and every entry), excludes `.git` path
|
|
58
|
+
components and respects excluded ancestors. Adapters supply bounded ignore-file
|
|
59
|
+
text; maintained `ignore` owns gitignore grammar and nested negation. Exact-path
|
|
60
|
+
reads do not use search visibility.
|
|
61
|
+
|
|
46
62
|
## Set up
|
|
47
63
|
|
|
48
64
|
```sh
|
|
@@ -51,8 +67,9 @@ npm install @fungi.computer/knapsack
|
|
|
51
67
|
|
|
52
68
|
The package is an ESM public candidate, not a claim of npm publication. It
|
|
53
69
|
depends on Cairn, Forage, Mule, Mycelium, the MCP SDK client, Effect, Zod and
|
|
54
|
-
`diff`. Its explicit file list contains compiled JavaScript and
|
|
55
|
-
the API reference, this README and the license texts. It has no
|
|
70
|
+
`diff` and `ignore`. Its explicit file list contains compiled JavaScript and
|
|
71
|
+
declarations, the API reference, this README and the license texts. It has no
|
|
72
|
+
install scripts.
|
|
56
73
|
|
|
57
74
|
## Quick start: validate a read request
|
|
58
75
|
|
|
@@ -89,6 +106,12 @@ replace atomically or refuse a conflict. A native patch operation is available
|
|
|
89
106
|
only when that port supplies a patch engine. Paths are validated, not confined
|
|
90
107
|
by Knapsack: the captured host port enforces its own root and authority.
|
|
91
108
|
|
|
109
|
+
`Knapsack.ui` keeps one browser host per Session, opened by the browser's event
|
|
110
|
+
stream for a stored Session or by a run, and dropped when neither holds it.
|
|
111
|
+
Neither its `fetch` nor the browser's requests are authenticated by Knapsack:
|
|
112
|
+
mount `fetch` behind the host's own admission. `close()` ends every stream;
|
|
113
|
+
later requests answer 410 and later runs fail to borrow.
|
|
114
|
+
|
|
92
115
|
## API reference
|
|
93
116
|
|
|
94
117
|
The [generated reference](docs/api/README.md) covers every public entry:
|
|
@@ -100,6 +123,7 @@ The [generated reference](docs/api/README.md) covers every public entry:
|
|
|
100
123
|
| `/files-read` | Bounded text read schemas and shared path validation. |
|
|
101
124
|
| `/patch` | Read Codex patch operations for presentation; the native engine validates and applies them. |
|
|
102
125
|
| `/files-read-codec` | Encode read requests and adapt a framed host transport to the Files read port. |
|
|
126
|
+
| `/ui/browser` | Connect an App's Whistle runtime to one Agent Session's `Knapsack.ui` transport. |
|
|
103
127
|
|
|
104
128
|
## Examples and checks
|
|
105
129
|
|
|
@@ -107,8 +131,12 @@ The [generated reference](docs/api/README.md) covers every public entry:
|
|
|
107
131
|
provider failures and invocation context. [Files tests](test/files.test.ts)
|
|
108
132
|
exercise bounded reads and edits.
|
|
109
133
|
[Read codec tests](test/files-read-codec.test.ts) exercise the framed transport.
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
[Knapsack.ui tests](test/ui.test.ts) exercise run borrows and teardown; the
|
|
135
|
+
[host](test/ui-host.test.ts), [event stream](test/ui-eventsource.test.ts) and
|
|
136
|
+
[Session host](test/ui-sessions.test.ts) tests cover discovery, execution,
|
|
137
|
+
claims, disconnects and multi-tab delivery. The packed-consumer runtime and
|
|
138
|
+
typecheck fixtures import the root and Files entries from an isolated tarball
|
|
139
|
+
installation.
|
|
112
140
|
|
|
113
141
|
From a configured repository checkout:
|
|
114
142
|
|
|
@@ -131,6 +159,11 @@ search results contain at most 256 entries and expose truncation. Schemas reject
|
|
|
131
159
|
invalid requests and results. `FilesEditError` distinguishes conflicts, missing
|
|
132
160
|
or ambiguous matches, oversized files and partial-character edits.
|
|
133
161
|
|
|
162
|
+
One Session's browser serves one outstanding request: a second gets
|
|
163
|
+
`browser_busy`, a missing browser `browser_not_connected`. If the browser
|
|
164
|
+
disconnects after it claimed an execution, the result is
|
|
165
|
+
`browser_execution_indeterminate`, because the command may already have run.
|
|
166
|
+
|
|
134
167
|
## Contribute and license
|
|
135
168
|
|
|
136
169
|
Knapsack's package license is [MIT](LICENSE). The retained Codex patch grammar
|
package/dist/files-patch.d.ts
CHANGED
|
@@ -13,7 +13,14 @@ export declare const patchOutputSchema: z.ZodLazy<z.ZodUnion<readonly [z.ZodObje
|
|
|
13
13
|
exitCode: z.ZodNumber;
|
|
14
14
|
stdout: z.ZodString;
|
|
15
15
|
stderr: z.ZodString;
|
|
16
|
-
truncated: z.
|
|
16
|
+
truncated: z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodArray<z.ZodObject<{
|
|
17
|
+
stream: z.ZodEnum<{
|
|
18
|
+
stdout: "stdout";
|
|
19
|
+
stderr: "stderr";
|
|
20
|
+
}>;
|
|
21
|
+
keptBytes: z.ZodNumber;
|
|
22
|
+
totalBytes: z.ZodNumber;
|
|
23
|
+
}, z.core.$strict>>]>;
|
|
17
24
|
}, z.core.$strict>, z.ZodObject<{
|
|
18
25
|
status: z.ZodEnum<{
|
|
19
26
|
completed: "completed";
|
|
@@ -23,7 +30,14 @@ export declare const patchOutputSchema: z.ZodLazy<z.ZodUnion<readonly [z.ZodObje
|
|
|
23
30
|
exitCode: z.ZodNumber;
|
|
24
31
|
stdout: z.ZodString;
|
|
25
32
|
stderr: z.ZodString;
|
|
26
|
-
truncated: z.
|
|
33
|
+
truncated: z.ZodUnion<readonly [z.ZodLiteral<false>, z.ZodArray<z.ZodObject<{
|
|
34
|
+
stream: z.ZodEnum<{
|
|
35
|
+
stdout: "stdout";
|
|
36
|
+
stderr: "stderr";
|
|
37
|
+
}>;
|
|
38
|
+
keptBytes: z.ZodNumber;
|
|
39
|
+
totalBytes: z.ZodNumber;
|
|
40
|
+
}, z.core.$strict>>]>;
|
|
27
41
|
error: z.ZodObject<{
|
|
28
42
|
kind: z.ZodLiteral<"mismatch">;
|
|
29
43
|
}, z.core.$strict>;
|
package/dist/files-patch.js
CHANGED
|
@@ -6,12 +6,19 @@ export const patchInputSchema = z.lazy(() => z.strictObject({
|
|
|
6
6
|
description: `Codex patch text, at most ${String(maximumReadBytes)} UTF-8 bytes.`,
|
|
7
7
|
}),
|
|
8
8
|
}));
|
|
9
|
+
/** One patch output stream cut at its byte bound; absent streams were whole. */
|
|
10
|
+
const patchTruncationSchema = z.strictObject({
|
|
11
|
+
stream: z.enum(["stdout", "stderr"]),
|
|
12
|
+
keptBytes: z.number().int().nonnegative(),
|
|
13
|
+
totalBytes: z.number().int().positive(),
|
|
14
|
+
});
|
|
9
15
|
const patchResultSchema = z.strictObject({
|
|
10
16
|
status: z.enum(["completed", "failed", "cancelled"]),
|
|
11
17
|
exitCode: z.number().int(),
|
|
12
18
|
stdout: z.string(),
|
|
13
19
|
stderr: z.string(),
|
|
14
|
-
|
|
20
|
+
/** `false` when both streams are whole, else one entry per cut stream. */
|
|
21
|
+
truncated: z.union([z.literal(false), z.array(patchTruncationSchema).min(1)]),
|
|
15
22
|
});
|
|
16
23
|
/** Native patch completion, cancellation or failure, including an optional mismatch. */
|
|
17
24
|
export const patchOutputSchema = z.lazy(() => z.union([
|
package/dist/files-read.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
/** Maximum UTF-8 byte length of a Files text page. */
|
|
3
3
|
export declare const maximumReadBytes: number;
|
|
4
|
+
/** Maximum byte length of an image the read tool returns; a host refuses an image it cannot bring within it. */
|
|
5
|
+
export declare const maximumImageBytes: number;
|
|
6
|
+
/** Bytes needed at the start of a file to recognise a supported image. */
|
|
7
|
+
export declare const imageSniffBytes = 12;
|
|
8
|
+
/** Identify png, jpeg, gif or webp by their full magic bytes; `undefined` means not a supported image. */
|
|
9
|
+
export declare function sniffImageMimeType(head: Uint8Array): string | undefined;
|
|
10
|
+
/** Message for an image above a host's read cap, shared by every Files host. */
|
|
11
|
+
export declare function imageTooLargeMessage(path: string, size: number, limit: number): string;
|
|
4
12
|
/** Nonempty file path bounded to 4,096 characters, without null bytes. */
|
|
5
13
|
export declare const pathSchema: z.ZodLazy<z.ZodString>;
|
|
6
14
|
/** Validate text against a minimum character length and the Files UTF-8 byte limit. */
|
|
@@ -11,15 +19,112 @@ export declare const readInputSchema: z.ZodLazy<z.ZodObject<{
|
|
|
11
19
|
offset: z.ZodDefault<z.ZodNumber>;
|
|
12
20
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
13
21
|
}, z.core.$strict>>;
|
|
22
|
+
/** The image types a Files read can return. */
|
|
23
|
+
export declare const readImageMimeType: z.ZodEnum<{
|
|
24
|
+
"image/png": "image/png";
|
|
25
|
+
"image/jpeg": "image/jpeg";
|
|
26
|
+
"image/gif": "image/gif";
|
|
27
|
+
"image/webp": "image/webp";
|
|
28
|
+
}>;
|
|
29
|
+
/** Whole png, jpeg, gif or webp file, at most `maximumImageBytes`, as base64. */
|
|
30
|
+
export declare const readImageOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
31
|
+
path: z.ZodLazy<z.ZodString>;
|
|
32
|
+
image: z.ZodObject<{
|
|
33
|
+
mimeType: z.ZodEnum<{
|
|
34
|
+
"image/png": "image/png";
|
|
35
|
+
"image/jpeg": "image/jpeg";
|
|
36
|
+
"image/gif": "image/gif";
|
|
37
|
+
"image/webp": "image/webp";
|
|
38
|
+
}>;
|
|
39
|
+
data: z.ZodBase64;
|
|
40
|
+
bytes: z.ZodNumber;
|
|
41
|
+
}, z.core.$strict>;
|
|
42
|
+
file: z.ZodExactOptional<z.ZodLazy<z.ZodObject<{
|
|
43
|
+
computerId: z.ZodString;
|
|
44
|
+
path: z.ZodLazy<z.ZodString>;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
46
|
+
}, z.core.$strict>>;
|
|
14
47
|
/** Bounded text page, continuation offset and optional hosted file identity. */
|
|
15
|
-
export declare const
|
|
48
|
+
export declare const readTextOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
16
49
|
path: z.ZodLazy<z.ZodString>;
|
|
17
50
|
content: z.ZodString;
|
|
18
51
|
offset: z.ZodNumber;
|
|
19
52
|
nextOffset: z.ZodNullable<z.ZodNumber>;
|
|
20
53
|
truncated: z.ZodBoolean;
|
|
21
|
-
file: z.ZodExactOptional<z.ZodObject<{
|
|
54
|
+
file: z.ZodExactOptional<z.ZodLazy<z.ZodObject<{
|
|
22
55
|
computerId: z.ZodString;
|
|
23
56
|
path: z.ZodLazy<z.ZodString>;
|
|
24
|
-
}, z.core.$strict
|
|
57
|
+
}, z.core.$strict>>>;
|
|
58
|
+
}, z.core.$strict>>;
|
|
59
|
+
/**
|
|
60
|
+
* What guest code and MCP callers see: an image never travels as base64 in the
|
|
61
|
+
* value. It is pushed through the execution's image collector and the value is
|
|
62
|
+
* only this caption.
|
|
63
|
+
*/
|
|
64
|
+
export declare const readImageCaptionSchema: z.ZodLazy<z.ZodObject<{
|
|
65
|
+
path: z.ZodLazy<z.ZodString>;
|
|
66
|
+
image: z.ZodObject<{
|
|
67
|
+
ref: z.ZodString;
|
|
68
|
+
mimeType: z.ZodEnum<{
|
|
69
|
+
"image/png": "image/png";
|
|
70
|
+
"image/jpeg": "image/jpeg";
|
|
71
|
+
"image/gif": "image/gif";
|
|
72
|
+
"image/webp": "image/webp";
|
|
73
|
+
}>;
|
|
74
|
+
bytes: z.ZodNumber;
|
|
75
|
+
}, z.core.$strict>;
|
|
25
76
|
}, z.core.$strict>>;
|
|
77
|
+
/** Text page or image caption returned to guest code. */
|
|
78
|
+
export declare const readGuestOutputSchema: z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodObject<{
|
|
79
|
+
path: z.ZodLazy<z.ZodString>;
|
|
80
|
+
content: z.ZodString;
|
|
81
|
+
offset: z.ZodNumber;
|
|
82
|
+
nextOffset: z.ZodNullable<z.ZodNumber>;
|
|
83
|
+
truncated: z.ZodBoolean;
|
|
84
|
+
file: z.ZodExactOptional<z.ZodLazy<z.ZodObject<{
|
|
85
|
+
computerId: z.ZodString;
|
|
86
|
+
path: z.ZodLazy<z.ZodString>;
|
|
87
|
+
}, z.core.$strict>>>;
|
|
88
|
+
}, z.core.$strict>>, z.ZodLazy<z.ZodObject<{
|
|
89
|
+
path: z.ZodLazy<z.ZodString>;
|
|
90
|
+
image: z.ZodObject<{
|
|
91
|
+
ref: z.ZodString;
|
|
92
|
+
mimeType: z.ZodEnum<{
|
|
93
|
+
"image/png": "image/png";
|
|
94
|
+
"image/jpeg": "image/jpeg";
|
|
95
|
+
"image/gif": "image/gif";
|
|
96
|
+
"image/webp": "image/webp";
|
|
97
|
+
}>;
|
|
98
|
+
bytes: z.ZodNumber;
|
|
99
|
+
}, z.core.$strict>;
|
|
100
|
+
}, z.core.$strict>>]>>;
|
|
101
|
+
/** A bounded text page or a whole image; images are only read from offset 0. */
|
|
102
|
+
export declare const readOutputSchema: z.ZodLazy<z.ZodUnion<readonly [z.ZodLazy<z.ZodObject<{
|
|
103
|
+
path: z.ZodLazy<z.ZodString>;
|
|
104
|
+
content: z.ZodString;
|
|
105
|
+
offset: z.ZodNumber;
|
|
106
|
+
nextOffset: z.ZodNullable<z.ZodNumber>;
|
|
107
|
+
truncated: z.ZodBoolean;
|
|
108
|
+
file: z.ZodExactOptional<z.ZodLazy<z.ZodObject<{
|
|
109
|
+
computerId: z.ZodString;
|
|
110
|
+
path: z.ZodLazy<z.ZodString>;
|
|
111
|
+
}, z.core.$strict>>>;
|
|
112
|
+
}, z.core.$strict>>, z.ZodLazy<z.ZodObject<{
|
|
113
|
+
path: z.ZodLazy<z.ZodString>;
|
|
114
|
+
image: z.ZodObject<{
|
|
115
|
+
mimeType: z.ZodEnum<{
|
|
116
|
+
"image/png": "image/png";
|
|
117
|
+
"image/jpeg": "image/jpeg";
|
|
118
|
+
"image/gif": "image/gif";
|
|
119
|
+
"image/webp": "image/webp";
|
|
120
|
+
}>;
|
|
121
|
+
data: z.ZodBase64;
|
|
122
|
+
bytes: z.ZodNumber;
|
|
123
|
+
}, z.core.$strict>;
|
|
124
|
+
file: z.ZodExactOptional<z.ZodLazy<z.ZodObject<{
|
|
125
|
+
computerId: z.ZodString;
|
|
126
|
+
path: z.ZodLazy<z.ZodString>;
|
|
127
|
+
}, z.core.$strict>>>;
|
|
128
|
+
}, z.core.$strict>>]>>;
|
|
129
|
+
/** Build the image read result for a recognised image's complete bytes. */
|
|
130
|
+
export declare function imageReadResult(path: string, mimeType: string, bytes: Uint8Array): z.output<typeof readImageOutputSchema>;
|
package/dist/files-read.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
+
import * as Base64 from "effect/encoding/Base64";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
/** Maximum UTF-8 byte length of a Files text page. */
|
|
3
4
|
export const maximumReadBytes = 128 * 1_024;
|
|
5
|
+
/** Maximum byte length of an image the read tool returns; a host refuses an image it cannot bring within it. */
|
|
6
|
+
export const maximumImageBytes = 512 * 1_024;
|
|
4
7
|
const utf8 = new TextEncoder();
|
|
8
|
+
const signatures = [
|
|
9
|
+
{
|
|
10
|
+
mimeType: "image/png",
|
|
11
|
+
bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
|
12
|
+
},
|
|
13
|
+
{ mimeType: "image/jpeg", bytes: [0xff, 0xd8, 0xff] },
|
|
14
|
+
{ mimeType: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] },
|
|
15
|
+
{ mimeType: "image/gif", bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] },
|
|
16
|
+
];
|
|
17
|
+
const riff = [0x52, 0x49, 0x46, 0x46];
|
|
18
|
+
const webp = [0x57, 0x45, 0x42, 0x50];
|
|
19
|
+
/** Bytes needed at the start of a file to recognise a supported image. */
|
|
20
|
+
export const imageSniffBytes = 12;
|
|
21
|
+
const hasBytesAt = (head, offset, bytes) => bytes.every((byte, index) => head[offset + index] === byte);
|
|
22
|
+
/** Identify png, jpeg, gif or webp by their full magic bytes; `undefined` means not a supported image. */
|
|
23
|
+
export function sniffImageMimeType(head) {
|
|
24
|
+
for (const { mimeType, bytes } of signatures)
|
|
25
|
+
if (hasBytesAt(head, 0, bytes))
|
|
26
|
+
return mimeType;
|
|
27
|
+
return hasBytesAt(head, 0, riff) && hasBytesAt(head, 8, webp)
|
|
28
|
+
? "image/webp"
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Message for an image above a host's read cap, shared by every Files host. */
|
|
32
|
+
export function imageTooLargeMessage(path, size, limit) {
|
|
33
|
+
return `Image ${path} is ${String(size)} bytes; the read limit is ${String(limit)} bytes. Refusing to read it.`;
|
|
34
|
+
}
|
|
5
35
|
/** Nonempty file path bounded to 4,096 characters, without null bytes. */
|
|
6
36
|
export const pathSchema = z.lazy(() => z
|
|
7
37
|
.string()
|
|
@@ -26,18 +56,58 @@ export const readInputSchema = z.lazy(() => z.strictObject({
|
|
|
26
56
|
.max(maximumReadBytes)
|
|
27
57
|
.default(maximumReadBytes),
|
|
28
58
|
}));
|
|
59
|
+
const fileIdentitySchema = z.lazy(() => z.strictObject({
|
|
60
|
+
computerId: z.string().min(1).max(256),
|
|
61
|
+
path: pathSchema.refine((path) => path.startsWith("/"), "Computer file paths must be absolute"),
|
|
62
|
+
}));
|
|
63
|
+
/** The image types a Files read can return. */
|
|
64
|
+
export const readImageMimeType = z.enum([
|
|
65
|
+
"image/png",
|
|
66
|
+
"image/jpeg",
|
|
67
|
+
"image/gif",
|
|
68
|
+
"image/webp",
|
|
69
|
+
]);
|
|
70
|
+
/** Whole png, jpeg, gif or webp file, at most `maximumImageBytes`, as base64. */
|
|
71
|
+
export const readImageOutputSchema = z.lazy(() => z.strictObject({
|
|
72
|
+
path: pathSchema,
|
|
73
|
+
image: z.strictObject({
|
|
74
|
+
mimeType: readImageMimeType,
|
|
75
|
+
data: z.base64(),
|
|
76
|
+
bytes: z.number().int().min(1).max(maximumImageBytes),
|
|
77
|
+
}),
|
|
78
|
+
file: fileIdentitySchema.exactOptional(),
|
|
79
|
+
}));
|
|
29
80
|
/** Bounded text page, continuation offset and optional hosted file identity. */
|
|
30
|
-
export const
|
|
81
|
+
export const readTextOutputSchema = z.lazy(() => z.strictObject({
|
|
31
82
|
path: pathSchema,
|
|
32
83
|
content: boundedTextSchema(),
|
|
33
84
|
offset: z.number().int().min(0),
|
|
34
85
|
nextOffset: z.number().int().min(0).nullable(),
|
|
35
86
|
truncated: z.boolean(),
|
|
36
87
|
/** Captured hosted file identity; a local Files host may have no Computer identity. */
|
|
37
|
-
file:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
88
|
+
file: fileIdentitySchema.exactOptional(),
|
|
89
|
+
}));
|
|
90
|
+
/**
|
|
91
|
+
* What guest code and MCP callers see: an image never travels as base64 in the
|
|
92
|
+
* value. It is pushed through the execution's image collector and the value is
|
|
93
|
+
* only this caption.
|
|
94
|
+
*/
|
|
95
|
+
export const readImageCaptionSchema = z.lazy(() => z.strictObject({
|
|
96
|
+
path: pathSchema,
|
|
97
|
+
image: z.strictObject({
|
|
98
|
+
ref: z.string().min(1).max(32),
|
|
99
|
+
mimeType: readImageMimeType,
|
|
100
|
+
bytes: z.number().int().min(1).max(maximumImageBytes),
|
|
101
|
+
}),
|
|
43
102
|
}));
|
|
103
|
+
/** Text page or image caption returned to guest code. */
|
|
104
|
+
export const readGuestOutputSchema = z.lazy(() => z.union([readTextOutputSchema, readImageCaptionSchema]));
|
|
105
|
+
/** A bounded text page or a whole image; images are only read from offset 0. */
|
|
106
|
+
export const readOutputSchema = z.lazy(() => z.union([readTextOutputSchema, readImageOutputSchema]));
|
|
107
|
+
/** Build the image read result for a recognised image's complete bytes. */
|
|
108
|
+
export function imageReadResult(path, mimeType, bytes) {
|
|
109
|
+
return readImageOutputSchema.parse({
|
|
110
|
+
path,
|
|
111
|
+
image: { mimeType, data: Base64.encode(bytes), bytes: bytes.byteLength },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invocation-local search visibility shared by Local and hosted Files.
|
|
3
|
+
* Adapters supply bounded `.gitignore` text; `ignore` owns its grammar.
|
|
4
|
+
* All paths here are canonical, root-relative paths with `/` separators;
|
|
5
|
+
* the captured root itself is `""`. Exact reads do not use this policy.
|
|
6
|
+
*/
|
|
7
|
+
export declare class SearchVisibility {
|
|
8
|
+
private readonly readGitignore;
|
|
9
|
+
private readonly matchers;
|
|
10
|
+
constructor(readGitignore: (directory: string) => Promise<string | undefined>);
|
|
11
|
+
/** Check the search root and every entry with the same ancestor precedence. */
|
|
12
|
+
visible(path: string, isDirectory: boolean): Promise<boolean>;
|
|
13
|
+
private at;
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ignore, {} from "ignore";
|
|
2
|
+
/**
|
|
3
|
+
* Invocation-local search visibility shared by Local and hosted Files.
|
|
4
|
+
* Adapters supply bounded `.gitignore` text; `ignore` owns its grammar.
|
|
5
|
+
* All paths here are canonical, root-relative paths with `/` separators;
|
|
6
|
+
* the captured root itself is `""`. Exact reads do not use this policy.
|
|
7
|
+
*/
|
|
8
|
+
export class SearchVisibility {
|
|
9
|
+
readGitignore;
|
|
10
|
+
matchers = new Map();
|
|
11
|
+
constructor(readGitignore) {
|
|
12
|
+
this.readGitignore = readGitignore;
|
|
13
|
+
}
|
|
14
|
+
/** Check the search root and every entry with the same ancestor precedence. */
|
|
15
|
+
async visible(path, isDirectory) {
|
|
16
|
+
if (path === "")
|
|
17
|
+
return true;
|
|
18
|
+
const parts = path.split("/");
|
|
19
|
+
if (parts.includes(".git"))
|
|
20
|
+
return false;
|
|
21
|
+
const layers = [];
|
|
22
|
+
for (let index = 0; index < parts.length; index++) {
|
|
23
|
+
const base = parts.slice(0, index).join("/");
|
|
24
|
+
layers.push({ base, matcher: await this.at(base) });
|
|
25
|
+
const candidate = parts.slice(0, index + 1).join("/");
|
|
26
|
+
const directory = index < parts.length - 1 || isDirectory;
|
|
27
|
+
// Never reopen an excluded ancestor with a descendant's negation.
|
|
28
|
+
for (const layer of layers.toReversed()) {
|
|
29
|
+
const local = layer.base === ""
|
|
30
|
+
? candidate
|
|
31
|
+
: candidate.slice(layer.base.length + 1);
|
|
32
|
+
const verdict = layer.matcher.test(directory ? `${local}/` : local);
|
|
33
|
+
if (verdict.ignored)
|
|
34
|
+
return false;
|
|
35
|
+
if (verdict.unignored)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
async at(directory) {
|
|
42
|
+
const cached = this.matchers.get(directory);
|
|
43
|
+
if (cached !== undefined)
|
|
44
|
+
return cached;
|
|
45
|
+
const content = await this.readGitignore(directory);
|
|
46
|
+
const matcher = ignore();
|
|
47
|
+
if (content !== undefined)
|
|
48
|
+
matcher.add(content);
|
|
49
|
+
this.matchers.set(directory, matcher);
|
|
50
|
+
return matcher;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/files.d.ts
CHANGED
|
@@ -2,66 +2,53 @@ import { type Access, type InvocationContext, type LabelledTool, type ModuleRegi
|
|
|
2
2
|
import { patchInputSchema, patchOutputSchema } from "./files-patch.js";
|
|
3
3
|
export { patchInputSchema, patchOutputSchema } from "./files-patch.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { readInputSchema, readOutputSchema } from "./files-read.js";
|
|
6
|
-
export { readInputSchema, readOutputSchema };
|
|
5
|
+
import { readInputSchema, readGuestOutputSchema, readOutputSchema } from "./files-read.js";
|
|
6
|
+
export { readGuestOutputSchema, readInputSchema, readOutputSchema };
|
|
7
7
|
/** Typed refusal from a bounded or conditional Files edit. */
|
|
8
|
-
export type FilesEditErrorReason = "not-found" | "ambiguous" | "file-too-large" | "partial-character" | "unchanged" | "conflict";
|
|
8
|
+
export type FilesEditErrorReason = "not-found" | "ambiguous" | "file-too-large" | "partial-character" | "unchanged" | "not-text" | "conflict";
|
|
9
9
|
/** Expected Files edit refusal, kept distinct from unexpected host failures. */
|
|
10
10
|
export declare class FilesEditError extends Error {
|
|
11
11
|
readonly reason: FilesEditErrorReason;
|
|
12
12
|
readonly path: string;
|
|
13
13
|
constructor(reason: FilesEditErrorReason, path: string, detail: string);
|
|
14
14
|
}
|
|
15
|
-
/**
|
|
16
|
-
export declare const listInputSchema: z.ZodObject<{
|
|
17
|
-
path: z.ZodLazy<z.ZodString
|
|
15
|
+
/** List one directory level. */
|
|
16
|
+
export declare const listInputSchema: z.ZodLazy<z.ZodObject<{
|
|
17
|
+
path: z.ZodDefault<z.ZodLazy<z.ZodString>>;
|
|
18
18
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
19
|
-
}, z.core.$strict>;
|
|
20
|
-
/** Directory entries and an explicit truncation flag. */
|
|
21
|
-
export declare const listOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
22
|
-
path: z.ZodLazy<z.ZodString>;
|
|
23
|
-
entries: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
24
|
-
path: z.ZodLazy<z.ZodString>;
|
|
25
|
-
name: z.ZodString;
|
|
26
|
-
type: z.ZodEnum<{
|
|
27
|
-
file: "file";
|
|
28
|
-
directory: "directory";
|
|
29
|
-
symlink: "symlink";
|
|
30
|
-
}>;
|
|
31
|
-
size: z.ZodNumber;
|
|
32
|
-
modifiedAt: z.ZodNumber;
|
|
33
|
-
}, z.core.$strict>>>;
|
|
34
|
-
truncated: z.ZodBoolean;
|
|
35
19
|
}, z.core.$strict>>;
|
|
36
|
-
/**
|
|
20
|
+
/** Match paths below a directory by glob. */
|
|
37
21
|
export declare const globInputSchema: z.ZodLazy<z.ZodObject<{
|
|
38
|
-
path: z.ZodLazy<z.ZodString
|
|
22
|
+
path: z.ZodDefault<z.ZodLazy<z.ZodString>>;
|
|
39
23
|
pattern: z.ZodString;
|
|
40
24
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
41
25
|
}, z.core.$strict>>;
|
|
42
|
-
/**
|
|
43
|
-
export declare const globOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
44
|
-
path: z.ZodLazy<z.ZodString>;
|
|
45
|
-
matches: z.ZodArray<z.ZodLazy<z.ZodString>>;
|
|
46
|
-
truncated: z.ZodBoolean;
|
|
47
|
-
}, z.core.$strict>>;
|
|
48
|
-
/** Bounded content-search request with optional regular-expression matching. */
|
|
26
|
+
/** Search file content, optionally restricted to matching paths. */
|
|
49
27
|
export declare const grepInputSchema: z.ZodLazy<z.ZodObject<{
|
|
50
|
-
path: z.ZodLazy<z.ZodString
|
|
28
|
+
path: z.ZodDefault<z.ZodLazy<z.ZodString>>;
|
|
51
29
|
pattern: z.ZodString;
|
|
30
|
+
include: z.ZodOptional<z.ZodString>;
|
|
52
31
|
regex: z.ZodDefault<z.ZodBoolean>;
|
|
53
32
|
ignoreCase: z.ZodDefault<z.ZodBoolean>;
|
|
54
33
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
55
34
|
}, z.core.$strict>>;
|
|
56
|
-
/**
|
|
57
|
-
export declare const
|
|
35
|
+
/** Found paths or matching lines, and which cap (if any) cut the search short. */
|
|
36
|
+
export declare const searchOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
58
37
|
path: z.ZodLazy<z.ZodString>;
|
|
59
|
-
|
|
38
|
+
results: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
60
39
|
path: z.ZodLazy<z.ZodString>;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
40
|
+
type: z.ZodExactOptional<z.ZodEnum<{
|
|
41
|
+
file: "file";
|
|
42
|
+
directory: "directory";
|
|
43
|
+
symlink: "symlink";
|
|
44
|
+
}>>;
|
|
45
|
+
line: z.ZodExactOptional<z.ZodNumber>;
|
|
46
|
+
text: z.ZodExactOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strict>>>;
|
|
48
|
+
truncated: z.ZodNullable<z.ZodEnum<{
|
|
49
|
+
limit: "limit";
|
|
50
|
+
scan: "scan";
|
|
51
|
+
}>>;
|
|
65
52
|
}, z.core.$strict>>;
|
|
66
53
|
/** Text replacement request decoded before the captured Files port is called. */
|
|
67
54
|
export declare const editInputSchema: z.ZodLazy<z.ZodObject<{
|
|
@@ -79,6 +66,7 @@ export declare const filesEditFailureSchema: z.ZodLazy<z.ZodObject<{
|
|
|
79
66
|
"file-too-large": "file-too-large";
|
|
80
67
|
"partial-character": "partial-character";
|
|
81
68
|
unchanged: "unchanged";
|
|
69
|
+
"not-text": "not-text";
|
|
82
70
|
conflict: "conflict";
|
|
83
71
|
}>;
|
|
84
72
|
path: z.ZodLazy<z.ZodString>;
|
|
@@ -105,6 +93,7 @@ export declare const editOutputSchema: z.ZodUnion<readonly [z.ZodLazy<z.ZodObjec
|
|
|
105
93
|
"file-too-large": "file-too-large";
|
|
106
94
|
"partial-character": "partial-character";
|
|
107
95
|
unchanged: "unchanged";
|
|
96
|
+
"not-text": "not-text";
|
|
108
97
|
conflict: "conflict";
|
|
109
98
|
}>;
|
|
110
99
|
path: z.ZodLazy<z.ZodString>;
|
|
@@ -119,12 +108,12 @@ export type FilesPort = Readonly<{
|
|
|
119
108
|
patch?: FilesPatch;
|
|
120
109
|
/** Read a bounded text page. */
|
|
121
110
|
read: (input: z.output<typeof readInputSchema>, context: InvocationContext) => Promise<z.output<typeof readOutputSchema>>;
|
|
122
|
-
/** List
|
|
123
|
-
list: (input: z.output<typeof listInputSchema>, context: InvocationContext) => Promise<z.output<typeof
|
|
124
|
-
/** Match
|
|
125
|
-
glob: (input: z.output<typeof globInputSchema>, context: InvocationContext) => Promise<z.output<typeof
|
|
126
|
-
/** Search
|
|
127
|
-
grep: (input: z.output<typeof grepInputSchema>, context: InvocationContext) => Promise<z.output<typeof
|
|
111
|
+
/** List one directory level under the admitted root. */
|
|
112
|
+
list: (input: z.output<typeof listInputSchema>, context: InvocationContext) => Promise<z.output<typeof searchOutputSchema>>;
|
|
113
|
+
/** Match paths below the admitted root by glob. */
|
|
114
|
+
glob: (input: z.output<typeof globInputSchema>, context: InvocationContext) => Promise<z.output<typeof searchOutputSchema>>;
|
|
115
|
+
/** Search file content under the admitted root; the host owns the walk. */
|
|
116
|
+
grep: (input: z.output<typeof grepInputSchema>, context: InvocationContext) => Promise<z.output<typeof searchOutputSchema>>;
|
|
128
117
|
/** Replace atomically only if the stored content still matches the baseline. */
|
|
129
118
|
replace: (input: Readonly<{
|
|
130
119
|
path: string;
|
|
@@ -135,7 +124,7 @@ export type FilesPort = Readonly<{
|
|
|
135
124
|
bytes: number;
|
|
136
125
|
}>>;
|
|
137
126
|
}>;
|
|
138
|
-
/** Closed set of direct, model-visible Files tools;
|
|
127
|
+
/** Closed set of direct, model-visible Files tools; changing it is an ADR. */
|
|
139
128
|
export type FileToolName = "read" | "list" | "glob" | "grep" | "apply_patch";
|
|
140
129
|
declare const fileToolBrand: unique symbol;
|
|
141
130
|
/**
|
|
@@ -166,5 +155,10 @@ export type FilesConnectionOptions = Readonly<{
|
|
|
166
155
|
}>;
|
|
167
156
|
/** Apply the same edit behavior as the Files edit operation directly through a captured FilesPort. */
|
|
168
157
|
export declare function edit(port: FilesPort, input: z.output<typeof editInputSchema>, context: InvocationContext): Promise<z.output<typeof editOutputSchema>>;
|
|
158
|
+
/**
|
|
159
|
+
* Guest code and MCP callers never receive base64: the image goes through the
|
|
160
|
+
* execution's image collector (the `images.view` path) and the value is a caption.
|
|
161
|
+
*/
|
|
162
|
+
export declare function presentReadForGuest(result: z.output<typeof readOutputSchema>, context: InvocationContext): z.output<typeof readGuestOutputSchema>;
|
|
169
163
|
/** Build both patch presentations around one injected Files operation. */
|
|
170
164
|
export declare function files(input: FilesConnectionOptions): FilesSurface;
|