@fungi.computer/knapsack 0.0.2 → 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 +20 -6
- package/dist/files-patch.js +11 -4
- package/dist/files-read.d.ts +115 -10
- package/dist/files-read.js +82 -12
- package/dist/files-search.d.ts +14 -0
- package/dist/files-search.js +52 -0
- package/dist/files.d.ts +59 -65
- package/dist/files.js +183 -97
- package/dist/index.d.ts +5 -1
- package/dist/index.js +10 -7
- 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/dist/files.d.ts
CHANGED
|
@@ -2,75 +2,62 @@ 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.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
|
-
/**
|
|
21
|
-
export declare const
|
|
22
|
-
path: z.ZodString
|
|
23
|
-
entries: z.ZodArray<z.ZodObject<{
|
|
24
|
-
path: 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
|
-
}, z.core.$strict>;
|
|
36
|
-
/** Bounded path-pattern search request. */
|
|
37
|
-
export declare const globInputSchema: z.ZodObject<{
|
|
38
|
-
path: z.ZodString;
|
|
19
|
+
}, z.core.$strict>>;
|
|
20
|
+
/** Match paths below a directory by glob. */
|
|
21
|
+
export declare const globInputSchema: z.ZodLazy<z.ZodObject<{
|
|
22
|
+
path: z.ZodDefault<z.ZodLazy<z.ZodString>>;
|
|
39
23
|
pattern: z.ZodString;
|
|
40
24
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
41
|
-
}, z.core.$strict
|
|
42
|
-
/**
|
|
43
|
-
export declare const
|
|
44
|
-
path: z.ZodString
|
|
45
|
-
matches: z.ZodArray<z.ZodString>;
|
|
46
|
-
truncated: z.ZodBoolean;
|
|
47
|
-
}, z.core.$strict>;
|
|
48
|
-
/** Bounded content-search request with optional regular-expression matching. */
|
|
49
|
-
export declare const grepInputSchema: z.ZodObject<{
|
|
50
|
-
path: z.ZodString;
|
|
25
|
+
}, z.core.$strict>>;
|
|
26
|
+
/** Search file content, optionally restricted to matching paths. */
|
|
27
|
+
export declare const grepInputSchema: z.ZodLazy<z.ZodObject<{
|
|
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
|
-
}, z.core.$strict
|
|
56
|
-
/**
|
|
57
|
-
export declare const
|
|
58
|
-
path: z.ZodString
|
|
59
|
-
|
|
60
|
-
path: z.ZodString
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
34
|
+
}, z.core.$strict>>;
|
|
35
|
+
/** Found paths or matching lines, and which cap (if any) cut the search short. */
|
|
36
|
+
export declare const searchOutputSchema: z.ZodLazy<z.ZodObject<{
|
|
37
|
+
path: z.ZodLazy<z.ZodString>;
|
|
38
|
+
results: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
39
|
+
path: z.ZodLazy<z.ZodString>;
|
|
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
|
+
}>>;
|
|
52
|
+
}, z.core.$strict>>;
|
|
66
53
|
/** Text replacement request decoded before the captured Files port is called. */
|
|
67
|
-
export declare const editInputSchema: z.ZodObject<{
|
|
68
|
-
path: z.ZodString
|
|
54
|
+
export declare const editInputSchema: z.ZodLazy<z.ZodObject<{
|
|
55
|
+
path: z.ZodLazy<z.ZodString>;
|
|
69
56
|
oldText: z.ZodString;
|
|
70
57
|
newText: z.ZodString;
|
|
71
|
-
}, z.core.$strict
|
|
58
|
+
}, z.core.$strict>>;
|
|
72
59
|
/** Structured edit refusal, including its reason and affected path. */
|
|
73
|
-
export declare const filesEditFailureSchema: z.ZodObject<{
|
|
60
|
+
export declare const filesEditFailureSchema: z.ZodLazy<z.ZodObject<{
|
|
74
61
|
status: z.ZodLiteral<"failed">;
|
|
75
62
|
error: z.ZodObject<{
|
|
76
63
|
kind: z.ZodEnum<{
|
|
@@ -79,15 +66,16 @@ export declare const filesEditFailureSchema: 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
|
-
path: z.ZodString
|
|
72
|
+
path: z.ZodLazy<z.ZodString>;
|
|
85
73
|
message: z.ZodString;
|
|
86
74
|
}, z.core.$strict>;
|
|
87
|
-
}, z.core.$strict
|
|
75
|
+
}, z.core.$strict>>;
|
|
88
76
|
/** Successful edit metadata or a typed edit refusal. */
|
|
89
|
-
export declare const editOutputSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
90
|
-
path: z.ZodString
|
|
77
|
+
export declare const editOutputSchema: z.ZodUnion<readonly [z.ZodLazy<z.ZodObject<{
|
|
78
|
+
path: z.ZodLazy<z.ZodString>;
|
|
91
79
|
bytes: z.ZodNumber;
|
|
92
80
|
diff: z.ZodString;
|
|
93
81
|
firstChangedLine: z.ZodNullable<z.ZodNumber>;
|
|
@@ -96,7 +84,7 @@ export declare const editOutputSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
96
84
|
unifiedPatchAddedLines: z.ZodNumber;
|
|
97
85
|
unifiedPatchRemovedLines: z.ZodNumber;
|
|
98
86
|
unifiedPatchTruncated: z.ZodBoolean;
|
|
99
|
-
}, z.core.$strict
|
|
87
|
+
}, z.core.$strict>>, z.ZodLazy<z.ZodObject<{
|
|
100
88
|
status: z.ZodLiteral<"failed">;
|
|
101
89
|
error: z.ZodObject<{
|
|
102
90
|
kind: z.ZodEnum<{
|
|
@@ -105,12 +93,13 @@ export declare const editOutputSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
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
|
-
path: z.ZodString
|
|
99
|
+
path: z.ZodLazy<z.ZodString>;
|
|
111
100
|
message: z.ZodString;
|
|
112
101
|
}, z.core.$strict>;
|
|
113
|
-
}, z.core.$strict
|
|
102
|
+
}, z.core.$strict>>]>;
|
|
114
103
|
/** Host-owned native patch operation receiving the invocation's deadline and signal. */
|
|
115
104
|
export type FilesPatch = (input: z.output<typeof patchInputSchema>, context: InvocationContext) => Promise<z.output<typeof patchOutputSchema>>;
|
|
116
105
|
/** Captured host file authority; the host owns path confinement and atomic replacement. */
|
|
@@ -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;
|
package/dist/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nanoid } from "@fungi.computer/cairn";
|
|
2
|
-
import { Mycelium, } from "@fungi.computer/mycelium";
|
|
2
|
+
import { MAX_EXECUTION_IMAGES, Mycelium, } from "@fungi.computer/mycelium";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import * as Stream from "effect/Stream";
|
|
5
5
|
import { patchInputSchema, patchOutputSchema } from "./files-patch.js";
|
|
@@ -7,11 +7,14 @@ export { patchInputSchema, patchOutputSchema } from "./files-patch.js";
|
|
|
7
7
|
import { patchGrammar } from "./patch-grammar.js";
|
|
8
8
|
import { diffLines, formatPatch, structuredPatch } from "diff";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { maximumReadBytes, pathSchema, boundedTextSchema, readInputSchema, readOutputSchema, } from "./files-read.js";
|
|
11
|
-
export { readInputSchema, readOutputSchema };
|
|
10
|
+
import { maximumImageBytes, maximumReadBytes, pathSchema, boundedTextSchema, readInputSchema, readGuestOutputSchema, readOutputSchema, } from "./files-read.js";
|
|
11
|
+
export { readGuestOutputSchema, readInputSchema, readOutputSchema };
|
|
12
12
|
const maximumPatternCharacters = 512;
|
|
13
13
|
const maximumResultEntries = 256;
|
|
14
|
+
const defaultSearchResults = 100;
|
|
15
|
+
const maximumFoundLineCharacters = 500;
|
|
14
16
|
const maximumReadBytesText = String(maximumReadBytes);
|
|
17
|
+
const readDescription = `Read one bounded UTF-8 file page, or a whole png, jpeg, gif or webp image returned as an image you can look at (sent at most ${String(maximumImageBytes / 1_024)} KiB; the host may shrink it and refuses an image it cannot read or fit).`;
|
|
15
18
|
const maximumUnifiedPatchBytes = 65_536;
|
|
16
19
|
const utf8 = new TextEncoder();
|
|
17
20
|
/** Expected Files edit refusal, kept distinct from unexpected host failures. */
|
|
@@ -25,78 +28,53 @@ export class FilesEditError extends Error {
|
|
|
25
28
|
this.name = "FilesEditError";
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/** Bounded path-pattern search request. */
|
|
52
|
-
export const globInputSchema = z.strictObject({
|
|
53
|
-
path: pathSchema,
|
|
54
|
-
pattern: z.string().min(1).max(maximumPatternCharacters),
|
|
55
|
-
limit: z
|
|
56
|
-
.number()
|
|
57
|
-
.int()
|
|
58
|
-
.min(1)
|
|
59
|
-
.max(maximumResultEntries)
|
|
60
|
-
.default(maximumResultEntries),
|
|
61
|
-
});
|
|
62
|
-
/** Matching paths and an explicit truncation flag. */
|
|
63
|
-
export const globOutputSchema = z.strictObject({
|
|
64
|
-
path: pathSchema,
|
|
65
|
-
matches: z.array(pathSchema).max(maximumResultEntries),
|
|
66
|
-
truncated: z.boolean(),
|
|
67
|
-
});
|
|
68
|
-
/** Bounded content-search request with optional regular-expression matching. */
|
|
69
|
-
export const grepInputSchema = z.strictObject({
|
|
70
|
-
path: pathSchema,
|
|
71
|
-
pattern: z.string().min(1).max(maximumPatternCharacters),
|
|
31
|
+
const searchLimitSchema = z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.min(1)
|
|
35
|
+
.max(maximumResultEntries)
|
|
36
|
+
.default(defaultSearchResults);
|
|
37
|
+
const searchPatternSchema = z.string().min(1).max(maximumPatternCharacters);
|
|
38
|
+
/** List one directory level. */
|
|
39
|
+
export const listInputSchema = z.lazy(() => z.strictObject({
|
|
40
|
+
path: pathSchema.default("."),
|
|
41
|
+
limit: searchLimitSchema,
|
|
42
|
+
}));
|
|
43
|
+
/** Match paths below a directory by glob. */
|
|
44
|
+
export const globInputSchema = z.lazy(() => z.strictObject({
|
|
45
|
+
path: pathSchema.default("."),
|
|
46
|
+
pattern: searchPatternSchema,
|
|
47
|
+
limit: searchLimitSchema,
|
|
48
|
+
}));
|
|
49
|
+
/** Search file content, optionally restricted to matching paths. */
|
|
50
|
+
export const grepInputSchema = z.lazy(() => z.strictObject({
|
|
51
|
+
path: pathSchema.default("."),
|
|
52
|
+
pattern: searchPatternSchema,
|
|
53
|
+
include: searchPatternSchema.optional(),
|
|
72
54
|
regex: z.boolean().default(false),
|
|
73
55
|
ignoreCase: z.boolean().default(false),
|
|
74
|
-
limit:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.max(maximumResultEntries)
|
|
79
|
-
.default(maximumResultEntries),
|
|
80
|
-
});
|
|
81
|
-
/** Content matches and an explicit truncation flag. */
|
|
82
|
-
export const grepOutputSchema = z.strictObject({
|
|
56
|
+
limit: searchLimitSchema,
|
|
57
|
+
}));
|
|
58
|
+
/** One found path, with the matching line when the search was by content. */
|
|
59
|
+
const foundSchema = z.lazy(() => z.strictObject({
|
|
83
60
|
path: pathSchema,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
61
|
+
type: z.enum(["file", "directory", "symlink"]).exactOptional(),
|
|
62
|
+
line: z.number().int().positive().exactOptional(),
|
|
63
|
+
text: z.string().exactOptional(),
|
|
64
|
+
}));
|
|
65
|
+
/** Found paths or matching lines, and which cap (if any) cut the search short. */
|
|
66
|
+
export const searchOutputSchema = z.lazy(() => z.strictObject({
|
|
67
|
+
path: pathSchema,
|
|
68
|
+
results: z.array(foundSchema).max(maximumResultEntries),
|
|
69
|
+
truncated: z.enum(["limit", "scan"]).nullable(),
|
|
70
|
+
}));
|
|
93
71
|
/** Text replacement request decoded before the captured Files port is called. */
|
|
94
|
-
export const editInputSchema = z.strictObject({
|
|
72
|
+
export const editInputSchema = z.lazy(() => z.strictObject({
|
|
95
73
|
path: pathSchema,
|
|
96
74
|
oldText: boundedTextSchema(1),
|
|
97
75
|
newText: boundedTextSchema(),
|
|
98
|
-
});
|
|
99
|
-
const editSuccessSchema = z.strictObject({
|
|
76
|
+
}));
|
|
77
|
+
const editSuccessSchema = z.lazy(() => z.strictObject({
|
|
100
78
|
path: pathSchema,
|
|
101
79
|
bytes: z.number().int().nonnegative(),
|
|
102
80
|
diff: z.string(),
|
|
@@ -106,9 +84,9 @@ const editSuccessSchema = z.strictObject({
|
|
|
106
84
|
unifiedPatchAddedLines: z.number().int().nonnegative(),
|
|
107
85
|
unifiedPatchRemovedLines: z.number().int().nonnegative(),
|
|
108
86
|
unifiedPatchTruncated: z.boolean(),
|
|
109
|
-
});
|
|
87
|
+
}));
|
|
110
88
|
/** Structured edit refusal, including its reason and affected path. */
|
|
111
|
-
export const filesEditFailureSchema = z.strictObject({
|
|
89
|
+
export const filesEditFailureSchema = z.lazy(() => z.strictObject({
|
|
112
90
|
status: z.literal("failed"),
|
|
113
91
|
error: z.strictObject({
|
|
114
92
|
kind: z.enum([
|
|
@@ -116,13 +94,14 @@ export const filesEditFailureSchema = z.strictObject({
|
|
|
116
94
|
"ambiguous",
|
|
117
95
|
"file-too-large",
|
|
118
96
|
"partial-character",
|
|
97
|
+
"not-text",
|
|
119
98
|
"unchanged",
|
|
120
99
|
"conflict",
|
|
121
100
|
]),
|
|
122
101
|
path: pathSchema,
|
|
123
102
|
message: z.string(),
|
|
124
103
|
}),
|
|
125
|
-
});
|
|
104
|
+
}));
|
|
126
105
|
/** Successful edit metadata or a typed edit refusal. */
|
|
127
106
|
export const editOutputSchema = z.union([
|
|
128
107
|
editSuccessSchema,
|
|
@@ -281,6 +260,9 @@ async function applyEdit(port, input, context) {
|
|
|
281
260
|
offset: 0,
|
|
282
261
|
limit: maximumReadBytes,
|
|
283
262
|
}, context));
|
|
263
|
+
if ("image" in current) {
|
|
264
|
+
throw new FilesEditError("not-text", input.path, "file is an image, not editable text");
|
|
265
|
+
}
|
|
284
266
|
if (current.truncated) {
|
|
285
267
|
throw new FilesEditError("file-too-large", input.path, `File exceeds ${maximumReadBytesText} bytes; use files.patch for a small change`);
|
|
286
268
|
}
|
|
@@ -357,6 +339,12 @@ function runFileOperation(invoke) {
|
|
|
357
339
|
function runPatch(invoke, input) {
|
|
358
340
|
return runFileOperation((context) => invoke(input, context));
|
|
359
341
|
}
|
|
342
|
+
/** Keep model parameters while omitting the decoder's schema-document marker. */
|
|
343
|
+
function toolParameters(schema) {
|
|
344
|
+
const parameters = z.toJSONSchema(schema, { io: "input" });
|
|
345
|
+
delete parameters.$schema;
|
|
346
|
+
return parameters;
|
|
347
|
+
}
|
|
360
348
|
/** Direct presentation of the same validated Files operation. */
|
|
361
349
|
function patchTool(invokePatch) {
|
|
362
350
|
const tool = {
|
|
@@ -368,7 +356,7 @@ function patchTool(invokePatch) {
|
|
|
368
356
|
name: "apply_patch",
|
|
369
357
|
description: "Apply a Codex patch to the selected Files root. Example:\n*** Begin Patch\n*** Update File: path/to/file\n@@\n context line\n-old line\n+new line\n*** End Patch\nUse @@ alone or followed by an existing context line; unified-diff @@ line numbers are not supported. Add/Delete File and optional Move to are also supported. Paths stay inside the root; symlinks and parent traversal are rejected. A failure returns recovery guidance and may follow earlier file writes; inspect the result before retrying. Prefer this to shell or Python replacement.",
|
|
370
358
|
// Model-facing parameters derived from the one Zod decoder `prepare` uses.
|
|
371
|
-
parameters:
|
|
359
|
+
parameters: toolParameters(patchInputSchema),
|
|
372
360
|
constrainedSampling: {
|
|
373
361
|
type: "grammar",
|
|
374
362
|
variants: { openai_lark: patchGrammar },
|
|
@@ -397,8 +385,109 @@ function patchTool(invokePatch) {
|
|
|
397
385
|
};
|
|
398
386
|
return tool;
|
|
399
387
|
}
|
|
388
|
+
// One explicit scope difference, shared by both projections of each search.
|
|
389
|
+
const searchScopeDescription = "Search skips .git and gitignored paths.";
|
|
390
|
+
const searchDescriptions = {
|
|
391
|
+
list: `List one directory level. ${searchScopeDescription}`,
|
|
392
|
+
glob: `Find paths by glob. ${searchScopeDescription}`,
|
|
393
|
+
grep: `Search file content. ${searchScopeDescription}`,
|
|
394
|
+
};
|
|
395
|
+
/** Long matching lines would flood the context; the cap is applied once, here. */
|
|
396
|
+
function capFoundLines(output) {
|
|
397
|
+
return {
|
|
398
|
+
...output,
|
|
399
|
+
results: output.results.map((found) => found.text === undefined ||
|
|
400
|
+
found.text.length <= maximumFoundLineCharacters
|
|
401
|
+
? found
|
|
402
|
+
: {
|
|
403
|
+
...found,
|
|
404
|
+
text: `${found.text.slice(0, maximumFoundLineCharacters)}…`,
|
|
405
|
+
}),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/** Plain lines for the model: `path`, or `path:line: text`, then why it stopped early. */
|
|
409
|
+
function renderFound(output) {
|
|
410
|
+
const lines = output.results.map((found) => found.line === undefined
|
|
411
|
+
? `${found.path}${found.type === "directory" ? "/" : ""}`
|
|
412
|
+
: `${found.path}:${String(found.line)}: ${found.text ?? ""}`);
|
|
413
|
+
if (lines.length === 0 && output.truncated === null)
|
|
414
|
+
lines.push("(nothing found)");
|
|
415
|
+
if (output.truncated === "limit")
|
|
416
|
+
lines.push("[truncated: result limit reached; raise limit or narrow path]");
|
|
417
|
+
if (output.truncated === "scan")
|
|
418
|
+
lines.push("[truncated: scan limit reached before the search finished; narrow path]");
|
|
419
|
+
return lines.join("\n");
|
|
420
|
+
}
|
|
421
|
+
function presentSearch(result) {
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: "text", text: renderFound(result) }],
|
|
424
|
+
isError: false,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Guest code and MCP callers never receive base64: the image goes through the
|
|
429
|
+
* execution's image collector (the `images.view` path) and the value is a caption.
|
|
430
|
+
*/
|
|
431
|
+
export function presentReadForGuest(result, context) {
|
|
432
|
+
if (!("image" in result))
|
|
433
|
+
return result;
|
|
434
|
+
if (context.images === undefined)
|
|
435
|
+
throw new Error("Images can only be read inside execute or through the read tool.");
|
|
436
|
+
const { mimeType, data, bytes } = result.image;
|
|
437
|
+
const ref = context.images.push({
|
|
438
|
+
data,
|
|
439
|
+
mimeType,
|
|
440
|
+
path: result.path,
|
|
441
|
+
bytes,
|
|
442
|
+
});
|
|
443
|
+
return { path: result.path, image: { ref, mimeType, bytes } };
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Images reach the model as an image part; text pages stay JSON text. The
|
|
447
|
+
* Files surface counts the images it shows and refuses past the run limit.
|
|
448
|
+
*/
|
|
449
|
+
function presentRead(admit) {
|
|
450
|
+
return (result) => {
|
|
451
|
+
if (!("image" in result))
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
454
|
+
isError: false,
|
|
455
|
+
};
|
|
456
|
+
if (!admit())
|
|
457
|
+
return {
|
|
458
|
+
content: [
|
|
459
|
+
{
|
|
460
|
+
type: "text",
|
|
461
|
+
text: `Image limit reached for this run (max ${String(MAX_EXECUTION_IMAGES)} images). Work with the images already shown.`,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
isError: true,
|
|
465
|
+
};
|
|
466
|
+
return {
|
|
467
|
+
content: [
|
|
468
|
+
{
|
|
469
|
+
type: "text",
|
|
470
|
+
text: JSON.stringify({
|
|
471
|
+
path: result.path,
|
|
472
|
+
mimeType: result.image.mimeType,
|
|
473
|
+
bytes: result.image.bytes,
|
|
474
|
+
}),
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
type: "image",
|
|
478
|
+
data: result.image.data,
|
|
479
|
+
mimeType: result.image.mimeType,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
isError: false,
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
}
|
|
400
486
|
/** Present the very same Files operation and decoder used behind execute. */
|
|
401
|
-
function readOnlyTool(name, description, inputSchema, outputSchema, invoke) {
|
|
487
|
+
function readOnlyTool(name, description, inputSchema, outputSchema, invoke, present = (result) => ({
|
|
488
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
489
|
+
isError: false,
|
|
490
|
+
})) {
|
|
402
491
|
const readOnly = {
|
|
403
492
|
[fileToolBrand]: true,
|
|
404
493
|
module: filesModuleId,
|
|
@@ -407,7 +496,8 @@ function readOnlyTool(name, description, inputSchema, outputSchema, invoke) {
|
|
|
407
496
|
descriptor: {
|
|
408
497
|
name,
|
|
409
498
|
description,
|
|
410
|
-
|
|
499
|
+
// Input side: defaulted fields are optional for the model.
|
|
500
|
+
parameters: toolParameters(inputSchema),
|
|
411
501
|
},
|
|
412
502
|
executionMode: "sequential",
|
|
413
503
|
prepare: (call) => Effect.try({
|
|
@@ -419,13 +509,7 @@ function readOnlyTool(name, description, inputSchema, outputSchema, invoke) {
|
|
|
419
509
|
execute: () => Stream.fromEffect(runFileOperation(async (context) => outputSchema.parse(await invoke(value, context))).pipe(Effect.map((result) => ({
|
|
420
510
|
type: "result",
|
|
421
511
|
result: {
|
|
422
|
-
|
|
423
|
-
{
|
|
424
|
-
type: "text",
|
|
425
|
-
text: JSON.stringify(result),
|
|
426
|
-
},
|
|
427
|
-
],
|
|
428
|
-
isError: false,
|
|
512
|
+
...present(result),
|
|
429
513
|
},
|
|
430
514
|
})))),
|
|
431
515
|
}))),
|
|
@@ -441,38 +525,40 @@ export function files(input) {
|
|
|
441
525
|
const invokePatch = patch === undefined
|
|
442
526
|
? undefined
|
|
443
527
|
: async (value, context) => patchOutputSchema.parse(await patch(patchInputSchema.parse(value), context));
|
|
528
|
+
/** Images the direct `read` tool has shown in this Files surface's run. */
|
|
529
|
+
let directImages = 0;
|
|
444
530
|
const operations = {
|
|
445
531
|
read: Mycelium.operation({
|
|
446
532
|
replay: "safe",
|
|
447
533
|
access: "read",
|
|
448
534
|
input: readInputSchema,
|
|
449
|
-
output:
|
|
450
|
-
description: describe(
|
|
451
|
-
execute: async (operationInput, context) => input.port.read(operationInput, context),
|
|
535
|
+
output: readGuestOutputSchema,
|
|
536
|
+
description: describe(readDescription),
|
|
537
|
+
execute: async (operationInput, context) => presentReadForGuest(await input.port.read(operationInput, context), context),
|
|
452
538
|
}),
|
|
453
539
|
list: Mycelium.operation({
|
|
454
540
|
replay: "safe",
|
|
455
541
|
access: "read",
|
|
456
542
|
input: listInputSchema,
|
|
457
|
-
output:
|
|
458
|
-
description: describe(
|
|
459
|
-
execute: async (operationInput, context) => input.port.list(operationInput, context),
|
|
543
|
+
output: searchOutputSchema,
|
|
544
|
+
description: describe(searchDescriptions.list),
|
|
545
|
+
execute: async (operationInput, context) => capFoundLines(await input.port.list(operationInput, context)),
|
|
460
546
|
}),
|
|
461
547
|
glob: Mycelium.operation({
|
|
462
548
|
replay: "safe",
|
|
463
549
|
access: "read",
|
|
464
550
|
input: globInputSchema,
|
|
465
|
-
output:
|
|
466
|
-
description: describe(
|
|
467
|
-
execute: async (operationInput, context) => input.port.glob(operationInput, context),
|
|
551
|
+
output: searchOutputSchema,
|
|
552
|
+
description: describe(searchDescriptions.glob),
|
|
553
|
+
execute: async (operationInput, context) => capFoundLines(await input.port.glob(operationInput, context)),
|
|
468
554
|
}),
|
|
469
555
|
grep: Mycelium.operation({
|
|
470
556
|
replay: "safe",
|
|
471
557
|
access: "read",
|
|
472
558
|
input: grepInputSchema,
|
|
473
|
-
output:
|
|
474
|
-
description: describe(
|
|
475
|
-
execute: async (operationInput, context) => input.port.grep(operationInput, context),
|
|
559
|
+
output: searchOutputSchema,
|
|
560
|
+
description: describe(searchDescriptions.grep),
|
|
561
|
+
execute: async (operationInput, context) => capFoundLines(await input.port.grep(operationInput, context)),
|
|
476
562
|
}),
|
|
477
563
|
edit: Mycelium.operation({
|
|
478
564
|
replay: "unsafe",
|
|
@@ -496,10 +582,10 @@ export function files(input) {
|
|
|
496
582
|
return Object.freeze({
|
|
497
583
|
tools: Object.freeze([
|
|
498
584
|
...(invokePatch === undefined ? [] : [patchTool(invokePatch)]),
|
|
499
|
-
readOnlyTool("read", describe(
|
|
500
|
-
readOnlyTool("list", describe(
|
|
501
|
-
readOnlyTool("glob", describe(
|
|
502
|
-
readOnlyTool("grep", describe(
|
|
585
|
+
readOnlyTool("read", describe(readDescription), readInputSchema, readOutputSchema, input.port.read, presentRead(() => (directImages += 1) <= MAX_EXECUTION_IMAGES)),
|
|
586
|
+
readOnlyTool("list", describe(searchDescriptions.list), listInputSchema, searchOutputSchema, async (value, context) => capFoundLines(await input.port.list(value, context)), presentSearch),
|
|
587
|
+
readOnlyTool("glob", describe(searchDescriptions.glob), globInputSchema, searchOutputSchema, async (value, context) => capFoundLines(await input.port.glob(value, context)), presentSearch),
|
|
588
|
+
readOnlyTool("grep", describe(searchDescriptions.grep), grepInputSchema, searchOutputSchema, async (value, context) => capFoundLines(await input.port.grep(value, context)), presentSearch),
|
|
503
589
|
]),
|
|
504
590
|
module: Mycelium.module({
|
|
505
591
|
id: filesModuleId,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type CapabilitySource, type InvocationContext, type MyceliumJsonValue } from "@fungi.computer/mycelium";
|
|
2
2
|
import { files, type FilesConnectionOptions } from "./files.js";
|
|
3
3
|
import { mcpServers } from "./mcp.js";
|
|
4
|
+
import { ui } from "./ui.js";
|
|
4
5
|
/** Generic advertised-call interface supplied by one Computer provider. */
|
|
5
6
|
export type KnapsackComputer = Readonly<{
|
|
6
7
|
/** Knapsack stops waiting when `signal` aborts, whether or not the provider honours it. */
|
|
@@ -65,6 +66,9 @@ export declare const Knapsack: Readonly<{
|
|
|
65
66
|
files: typeof files;
|
|
66
67
|
/** Connect a changing list of remote MCP servers, reporting unavailable ones independently. */
|
|
67
68
|
mcpServers: typeof mcpServers;
|
|
69
|
+
/** Connect the open App's live Whistle commands to each Agent Session. */
|
|
70
|
+
ui: typeof ui;
|
|
68
71
|
}>;
|
|
72
|
+
export type { UiConnection, UiConnectionOptions } from "./ui.js";
|
|
69
73
|
export { McpServerUnavailableError, type McpServerConnection, type McpServerUnavailableReason, type McpServersConnectionOptions, } from "./mcp.js";
|
|
70
|
-
export { FilesEditError, filesEditFailureSchema, type FilesEditErrorReason, type FilesConnectionOptions, type FilesPort, type FilesPatch, type FilesSurface, edit, readInputSchema, readOutputSchema, listInputSchema,
|
|
74
|
+
export { FilesEditError, filesEditFailureSchema, type FilesEditErrorReason, type FilesConnectionOptions, type FilesPort, type FilesPatch, type FilesSurface, edit, readInputSchema, readOutputSchema, listInputSchema, globInputSchema, grepInputSchema, searchOutputSchema, readGuestOutputSchema, presentReadForGuest, editInputSchema, editOutputSchema, patchInputSchema, patchOutputSchema, type FileTool, type FileToolName, } from "./files.js";
|