@fungi.computer/knapsack 0.0.0-stage → 0.0.2

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.
@@ -0,0 +1,70 @@
1
+ import { type CapabilitySource, type InvocationContext, type MyceliumJsonValue } from "@fungi.computer/mycelium";
2
+ import { files, type FilesConnectionOptions } from "./files.js";
3
+ import { mcpServers } from "./mcp.js";
4
+ /** Generic advertised-call interface supplied by one Computer provider. */
5
+ export type KnapsackComputer = Readonly<{
6
+ /** Knapsack stops waiting when `signal` aborts, whether or not the provider honours it. */
7
+ getCapabilities: (signal: AbortSignal) => Promise<MyceliumJsonValue>;
8
+ /** Dispatch one decoded advertised identifier with its JSON payload. */
9
+ call: (capabilityId: string, input: MyceliumJsonValue, context: InvocationContext) => Promise<MyceliumJsonValue>;
10
+ }>;
11
+ /** Selection and naming required to connect one Computer to Mycelium. */
12
+ export type ComputerConnectionOptions = Readonly<{
13
+ id: string;
14
+ name: string;
15
+ /** Logical parent namespace; the Computer name is its final segment. */
16
+ namespace?: readonly string[];
17
+ /** Files scoped by the host to this Computer, under its own authority id. */
18
+ files?: FilesConnectionOptions;
19
+ /**
20
+ * Short parenthetical shown alongside the Computer's real name in forage
21
+ * discovery, e.g. "home" for the agent's own machine. Omit for Computers
22
+ * with no such role; never used to rename or hide the Computer itself.
23
+ */
24
+ role?: string;
25
+ computer: KnapsackComputer;
26
+ }>;
27
+ /** Why one Computer contributed no module to an acquisition. */
28
+ export type ComputerUnavailableReason = "rejected" | "timeout" | "invalid-advertisement";
29
+ /** One Computer whose advertisement could not be acquired. */
30
+ export declare class ComputerUnavailableError extends Error {
31
+ readonly name = "ComputerUnavailableError";
32
+ readonly computerId: string;
33
+ readonly reason: ComputerUnavailableReason;
34
+ constructor(computerId: string, reason: ComputerUnavailableReason, cause?: unknown);
35
+ }
36
+ declare function computer(input: ComputerConnectionOptions): CapabilitySource;
37
+ /** A captured host connection list; authority remains with its supplying host. */
38
+ export type ComputersConnectionOptions = Readonly<{
39
+ id: string;
40
+ /** Capture the current host-owned connection list and its revision. */
41
+ read: () => Readonly<{
42
+ version: string;
43
+ connections: readonly ComputerConnectionOptions[];
44
+ }>;
45
+ /** Register invalidation and return its release callback. */
46
+ subscribe: (invalidate: () => void) => () => void;
47
+ /** Receives each Computer dropped from an acquisition and why. */
48
+ onUnavailable: (error: ComputerUnavailableError) => void;
49
+ /** Bound on each Computer's advertisement read; defaults to 10 seconds. */
50
+ describeTimeoutMillis?: number;
51
+ }>;
52
+ /**
53
+ * Acquire every captured Computer concurrently. A Computer that rejects, times
54
+ * out, or advertises an unusable schema is dropped and reported; the remaining
55
+ * Computers and Files stay available. Aborting the acquisition rejects it.
56
+ */
57
+ declare function computers(input: ComputersConnectionOptions): CapabilitySource;
58
+ /** Agent tool connections available for composition through Mycelium. */
59
+ export declare const Knapsack: Readonly<{
60
+ /** Project one Computer's advertised operations into a capability source. */
61
+ computer: typeof computer;
62
+ /** Capture a changing Computer list, reporting unavailable providers independently. */
63
+ computers: typeof computers;
64
+ /** Build Files operations from one captured host port. */
65
+ files: typeof files;
66
+ /** Connect a changing list of remote MCP servers, reporting unavailable ones independently. */
67
+ mcpServers: typeof mcpServers;
68
+ }>;
69
+ 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, listOutputSchema, globInputSchema, globOutputSchema, grepInputSchema, grepOutputSchema, editInputSchema, editOutputSchema, patchInputSchema, patchOutputSchema, type FileTool, type FileToolName, } from "./files.js";
package/dist/index.js ADDED
@@ -0,0 +1,178 @@
1
+ import { Mycelium, } from "@fungi.computer/mycelium";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Result from "effect/Result";
4
+ import { z } from "zod";
5
+ import { files } from "./files.js";
6
+ import { mcpServers } from "./mcp.js";
7
+ const maximumCapabilities = 256;
8
+ const describeConcurrency = 8;
9
+ const defaultDescribeTimeoutMillis = 10_000;
10
+ const utf8 = new TextEncoder();
11
+ const jsonValueSchema = z.json();
12
+ const jsonSchemaDocument = z.custom((value) => jsonValueSchema.safeParse(value).success);
13
+ const capabilityNameSchema = z
14
+ .string()
15
+ .min(1)
16
+ .max(128)
17
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/);
18
+ const capabilitySchema = z.strictObject({
19
+ id: capabilityNameSchema,
20
+ name: capabilityNameSchema,
21
+ kind: z.string().min(1).max(128),
22
+ description: z.string().min(1).max(2_048),
23
+ parameters: jsonSchemaDocument,
24
+ output: jsonSchemaDocument.optional(),
25
+ });
26
+ const capabilitiesSchema = z.array(capabilitySchema).max(maximumCapabilities);
27
+ function operationSchema(document) {
28
+ return z.fromJSONSchema(document).pipe(jsonValueSchema);
29
+ }
30
+ function operationOutputSchema(document) {
31
+ return z.intersection(jsonValueSchema, z.fromJSONSchema(document));
32
+ }
33
+ async function snapshotVersion(serialized) {
34
+ const bytes = utf8.encode(serialized);
35
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
36
+ return `computer-${Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
37
+ }
38
+ /** One Computer whose advertisement could not be acquired. */
39
+ export class ComputerUnavailableError extends Error {
40
+ name = "ComputerUnavailableError";
41
+ computerId;
42
+ reason;
43
+ constructor(computerId, reason, cause) {
44
+ super(`Computer ${computerId} is unavailable: ${reason}`, { cause });
45
+ this.computerId = computerId;
46
+ this.reason = reason;
47
+ }
48
+ }
49
+ function projectComputer(input, advertisement) {
50
+ const capabilities = capabilitiesSchema.parse(advertisement);
51
+ const operations = {};
52
+ for (const advertised of capabilities) {
53
+ const output = advertised.output === undefined
54
+ ? jsonValueSchema
55
+ : operationOutputSchema(advertised.output);
56
+ operations[advertised.id] = Mycelium.operation({
57
+ // Computer advertisements may execute arbitrary host actions.
58
+ replay: "unsafe",
59
+ description: advertised.description,
60
+ input: operationSchema(advertised.parameters),
61
+ output,
62
+ execute: (value, context) => input.computer.call(advertised.id, value, context),
63
+ });
64
+ }
65
+ if (input.files !== undefined) {
66
+ if (operations.files !== undefined)
67
+ throw new Error("Computer advertisement reserves the files namespace");
68
+ operations.files = files(input.files).module;
69
+ }
70
+ const description = input.role === undefined
71
+ ? `Computer "${input.name}": capabilities advertised by this Computer.`
72
+ : `Computer "${input.name}" (${input.role}): capabilities advertised by this Computer.`;
73
+ const module = Mycelium.module({
74
+ id: input.id,
75
+ name: input.name,
76
+ namespace: input.namespace ?? [],
77
+ description,
78
+ operations,
79
+ });
80
+ return { module, serialized: JSON.stringify(capabilities) };
81
+ }
82
+ /** Read and project one advertisement; interruption abandons a hung provider. */
83
+ function describeComputer(input, timeoutMillis) {
84
+ return Effect.tryPromise({
85
+ try: (signal) => input.computer.getCapabilities(signal),
86
+ catch: (cause) => new ComputerUnavailableError(input.id, "rejected", cause),
87
+ }).pipe(Effect.timeoutOrElse({
88
+ duration: timeoutMillis,
89
+ orElse: () => Effect.fail(new ComputerUnavailableError(input.id, "timeout")),
90
+ }), Effect.flatMap((advertisement) => Effect.try({
91
+ try: () => projectComputer(input, advertisement),
92
+ catch: (cause) => new ComputerUnavailableError(input.id, "invalid-advertisement", cause),
93
+ })), Effect.flatMap(({ module, serialized }) => Effect.promise(() => snapshotVersion(serialized)).pipe(Effect.map((version) => ({ module, version })))));
94
+ }
95
+ async function runAcquisition(effect, signal) {
96
+ signal.throwIfAborted();
97
+ try {
98
+ return await Effect.runPromise(effect, { signal });
99
+ }
100
+ catch (error) {
101
+ signal.throwIfAborted();
102
+ throw error;
103
+ }
104
+ }
105
+ function computer(input) {
106
+ return {
107
+ id: input.id,
108
+ acquire: async ({ signal }) => {
109
+ const described = await runAcquisition(Effect.result(describeComputer(input, defaultDescribeTimeoutMillis)), signal);
110
+ if (Result.isFailure(described))
111
+ throw described.failure;
112
+ return {
113
+ snapshot: {
114
+ version: described.success.version,
115
+ modules: [described.success.module],
116
+ },
117
+ release: () => Promise.resolve(),
118
+ };
119
+ },
120
+ };
121
+ }
122
+ /**
123
+ * Acquire every captured Computer concurrently. A Computer that rejects, times
124
+ * out, or advertises an unusable schema is dropped and reported; the remaining
125
+ * Computers and Files stay available. Aborting the acquisition rejects it.
126
+ */
127
+ function computers(input) {
128
+ const timeoutMillis = input.describeTimeoutMillis ?? defaultDescribeTimeoutMillis;
129
+ return {
130
+ id: input.id,
131
+ acquire: async (context) => {
132
+ context.signal.throwIfAborted();
133
+ const unsubscribe = input.subscribe(context.invalidate);
134
+ try {
135
+ const captured = input.read();
136
+ const described = await runAcquisition(Effect.forEach(captured.connections, (connection) => Effect.result(describeComputer(connection, timeoutMillis)), { concurrency: describeConcurrency }), context.signal);
137
+ const modules = [];
138
+ const versions = [captured.version];
139
+ for (const result of described) {
140
+ if (Result.isFailure(result)) {
141
+ input.onUnavailable(result.failure);
142
+ versions.push(`unavailable:${result.failure.computerId}:${result.failure.reason}`);
143
+ continue;
144
+ }
145
+ modules.push(result.success.module);
146
+ versions.push(result.success.version);
147
+ }
148
+ return {
149
+ snapshot: {
150
+ version: await snapshotVersion(JSON.stringify(versions)),
151
+ modules,
152
+ },
153
+ release: () => {
154
+ unsubscribe();
155
+ return Promise.resolve();
156
+ },
157
+ };
158
+ }
159
+ catch (error) {
160
+ unsubscribe();
161
+ throw error;
162
+ }
163
+ },
164
+ };
165
+ }
166
+ /** Agent tool connections available for composition through Mycelium. */
167
+ export const Knapsack = Object.freeze({
168
+ /** Project one Computer's advertised operations into a capability source. */
169
+ computer,
170
+ /** Capture a changing Computer list, reporting unavailable providers independently. */
171
+ computers,
172
+ /** Build Files operations from one captured host port. */
173
+ files,
174
+ /** Connect a changing list of remote MCP servers, reporting unavailable ones independently. */
175
+ mcpServers,
176
+ });
177
+ export { McpServerUnavailableError, } from "./mcp.js";
178
+ export { FilesEditError, filesEditFailureSchema, edit, readInputSchema, readOutputSchema, listInputSchema, listOutputSchema, globInputSchema, globOutputSchema, grepInputSchema, grepOutputSchema, editInputSchema, editOutputSchema, patchInputSchema, patchOutputSchema, } from "./files.js";
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { FetchLike } from "@modelcontextprotocol/client";
2
+ import { type CapabilitySource } from "@fungi.computer/mycelium";
3
+ /** One remote MCP server a host has admitted for an Agent. */
4
+ export type McpServerConnection = Readonly<{
5
+ /** Stable authority id for the server's module; never shown as a secret. */
6
+ id: string;
7
+ /** Model-facing identifier: the guest calls `env.<namespace>.<name>.<tool>()`. */
8
+ name: string;
9
+ /** Streamable HTTP endpoint. */
10
+ url: URL;
11
+ /**
12
+ * Host-held request headers such as `Authorization`. They are sent only to
13
+ * `url` by the SDK transport and never enter the snapshot, the catalog or a
14
+ * tool result.
15
+ */
16
+ headers?: Readonly<Record<string, string>>;
17
+ }>;
18
+ /** A captured host list of remote MCP servers; authority stays with the host. */
19
+ export type McpServersConnectionOptions = Readonly<{
20
+ id: string;
21
+ /** Capture the current server list and a secret-free revision of it. */
22
+ read: () => Promise<Readonly<{
23
+ version: string;
24
+ servers: readonly McpServerConnection[];
25
+ }>>;
26
+ /** Register invalidation and return its release callback. */
27
+ subscribe: (invalidate: () => void) => () => void;
28
+ /** Receives each server dropped from an acquisition and why. */
29
+ onUnavailable: (error: McpServerUnavailableError) => void;
30
+ /** Host egress for every MCP request; the host chooses where requests may go. */
31
+ fetch: FetchLike;
32
+ /** Parent namespace for every server module; defaults to `["mcp"]`. */
33
+ namespace?: readonly string[];
34
+ /** Bound on connecting and listing one server; defaults to 10 seconds. */
35
+ connectTimeoutMillis?: number;
36
+ }>;
37
+ /** Why one MCP server contributed no module to an acquisition. */
38
+ export type McpServerUnavailableReason = "rejected" | "unauthorized" | "timeout" | "invalid-advertisement";
39
+ /** One MCP server whose tools could not be acquired. */
40
+ export declare class McpServerUnavailableError extends Error {
41
+ readonly name = "McpServerUnavailableError";
42
+ readonly serverId: string;
43
+ readonly reason: McpServerUnavailableReason;
44
+ constructor(serverId: string, reason: McpServerUnavailableReason, cause?: unknown);
45
+ }
46
+ /**
47
+ * Connect every captured MCP server concurrently with the maintained SDK
48
+ * client over Streamable HTTP and project its tools into one Mycelium module
49
+ * per server. A server that rejects, refuses its credential, times out or
50
+ * advertises nothing usable is dropped and reported; the others stay
51
+ * available. Releasing the acquisition closes every client.
52
+ */
53
+ declare function mcpServers(input: McpServersConnectionOptions): CapabilitySource;
54
+ export { mcpServers };
package/dist/mcp.js ADDED
@@ -0,0 +1,237 @@
1
+ import { Mycelium, } from "@fungi.computer/mycelium";
2
+ import { jsonSchemaSubset } from "@fungi.computer/forage/json-schema-subset";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Result from "effect/Result";
5
+ import { z } from "zod";
6
+ const maximumServers = 16;
7
+ const maximumTools = 256;
8
+ const maximumToolPages = 16;
9
+ const connectConcurrency = 4;
10
+ const defaultConnectTimeoutMillis = 10_000;
11
+ const maximumDescription = 2_048;
12
+ const utf8 = new TextEncoder();
13
+ const jsonValueSchema = z.json();
14
+ const argumentsSchema = z.record(z.string(), jsonValueSchema);
15
+ const identifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/u;
16
+ /** One MCP server whose tools could not be acquired. */
17
+ export class McpServerUnavailableError extends Error {
18
+ name = "McpServerUnavailableError";
19
+ serverId;
20
+ reason;
21
+ constructor(serverId, reason, cause) {
22
+ super(`MCP server ${serverId} is unavailable: ${reason}`, { cause });
23
+ this.serverId = serverId;
24
+ this.reason = reason;
25
+ }
26
+ }
27
+ async function digest(serialized) {
28
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", utf8.encode(serialized)));
29
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
30
+ }
31
+ /** Map an MCP tool name onto the guest's identifier grammar. */
32
+ function operationKey(toolName) {
33
+ const mapped = toolName.replace(/[^A-Za-z0-9_]/gu, "_");
34
+ return identifierPattern.test(mapped) ? mapped : `t_${mapped}`;
35
+ }
36
+ const looseDescription = "Arguments are a JSON object; this server's schema is outside the supported subset, so the server validates them.";
37
+ /** The tool's advertised schema when it fits Forage's subset, else a JSON object. */
38
+ function toolInput(tool) {
39
+ const subset = jsonSchemaSubset().safeParse(tool.inputSchema);
40
+ if (!subset.success)
41
+ return { schema: argumentsSchema, loose: true };
42
+ try {
43
+ return {
44
+ schema: z.fromJSONSchema(subset.data).pipe(argumentsSchema),
45
+ loose: false,
46
+ };
47
+ }
48
+ catch {
49
+ return { schema: argumentsSchema, loose: true };
50
+ }
51
+ }
52
+ function toolDescription(tool, loose) {
53
+ const text = [tool.title, tool.description]
54
+ .filter((part) => part !== undefined && part.length > 0)
55
+ .join(": ");
56
+ const base = text.length > 0 ? text : tool.name;
57
+ const described = loose ? `${base}\n\n${looseDescription}` : base;
58
+ return described.slice(0, maximumDescription);
59
+ }
60
+ function project(server, namespace, client, tools) {
61
+ const operations = {};
62
+ for (const tool of tools) {
63
+ const key = operationKey(tool.name);
64
+ // A later tool that maps onto a taken key is dropped, never renamed twice.
65
+ if (key in operations)
66
+ continue;
67
+ const input = toolInput(tool);
68
+ operations[key] = Mycelium.operation({
69
+ // Only the server's own read-only hint permits replay.
70
+ replay: tool.annotations?.readOnlyHint === true ? "safe" : "unsafe",
71
+ description: toolDescription(tool, input.loose),
72
+ input: input.schema,
73
+ output: jsonValueSchema,
74
+ execute: async (args, context) => {
75
+ const result = await client.callTool({ name: tool.name, arguments: args }, { signal: context.signal });
76
+ // MCP's own result shape: structured data when the server sends it,
77
+ // otherwise the content blocks with the error flag.
78
+ if (result.isError !== true && result.structuredContent !== undefined)
79
+ return jsonValueSchema.parse(result.structuredContent);
80
+ return jsonValueSchema.parse({
81
+ isError: result.isError === true,
82
+ content: result.content,
83
+ });
84
+ },
85
+ });
86
+ }
87
+ return Mycelium.module({
88
+ id: server.id,
89
+ name: server.name,
90
+ namespace,
91
+ description: `MCP server "${server.name}" at ${server.url.origin}: tools it advertises.`,
92
+ operations,
93
+ });
94
+ }
95
+ async function listAllTools(client, signal) {
96
+ const tools = [];
97
+ let cursor;
98
+ for (let page = 0; page < maximumToolPages; page += 1) {
99
+ const listed = await client.listTools(cursor === undefined ? undefined : { cursor }, { signal });
100
+ tools.push(...listed.tools);
101
+ if (tools.length > maximumTools)
102
+ throw new Error(`MCP server advertises more than ${String(maximumTools)} tools`);
103
+ cursor = listed.nextCursor;
104
+ if (cursor === undefined)
105
+ return tools;
106
+ }
107
+ throw new Error(`MCP server tool list exceeds ${String(maximumToolPages)} pages`);
108
+ }
109
+ /**
110
+ * The SDK builds its whole schema set when its module evaluates, so it loads
111
+ * on the first connection instead of during a host Worker's startup.
112
+ */
113
+ function loadClientModule() {
114
+ return import("@modelcontextprotocol/client");
115
+ }
116
+ function failureReason(mcp, cause) {
117
+ if (mcp.UnauthorizedError.isInstance(cause))
118
+ return "unauthorized";
119
+ if (mcp.SdkHttpError.isInstance(cause) &&
120
+ (cause.status === 401 || cause.status === 403))
121
+ return "unauthorized";
122
+ return "rejected";
123
+ }
124
+ async function connect(server, namespace, fetch, signal) {
125
+ const mcp = await loadClientModule();
126
+ const client = new mcp.Client({ name: "fungi-knapsack", version: "1" });
127
+ const transport = new mcp.StreamableHTTPClientTransport(new URL(server.url), {
128
+ // Credentials go to this endpoint only; a redirect could carry them away.
129
+ requestInit: { headers: { ...server.headers }, redirect: "error" },
130
+ fetch,
131
+ });
132
+ try {
133
+ await client.connect(transport, { signal });
134
+ }
135
+ catch (cause) {
136
+ await client.close();
137
+ throw new McpServerUnavailableError(server.id, failureReason(mcp, cause), cause);
138
+ }
139
+ try {
140
+ const tools = await listAllTools(client, signal).catch((cause) => {
141
+ throw new McpServerUnavailableError(server.id, failureReason(mcp, cause) === "unauthorized"
142
+ ? "unauthorized"
143
+ : "invalid-advertisement", cause);
144
+ });
145
+ let module;
146
+ try {
147
+ module = project(server, namespace, client, tools);
148
+ }
149
+ catch (cause) {
150
+ throw new McpServerUnavailableError(server.id, "invalid-advertisement", cause);
151
+ }
152
+ const version = await digest(JSON.stringify([
153
+ server.id,
154
+ server.name,
155
+ server.url.href,
156
+ tools.map((tool) => [
157
+ tool.name,
158
+ tool.description ?? null,
159
+ tool.inputSchema,
160
+ tool.annotations?.readOnlyHint ?? null,
161
+ ]),
162
+ ]));
163
+ return { module, version, close: () => client.close() };
164
+ }
165
+ catch (error) {
166
+ await client.close();
167
+ throw error;
168
+ }
169
+ }
170
+ function connectWithin(server, namespace, fetch, timeoutMillis) {
171
+ return Effect.tryPromise({
172
+ try: (signal) => connect(server, namespace, fetch, signal),
173
+ catch: (cause) => cause instanceof McpServerUnavailableError
174
+ ? cause
175
+ : new McpServerUnavailableError(server.id, "rejected", cause),
176
+ }).pipe(Effect.timeoutOrElse({
177
+ duration: timeoutMillis,
178
+ orElse: () => Effect.fail(new McpServerUnavailableError(server.id, "timeout")),
179
+ }));
180
+ }
181
+ /**
182
+ * Connect every captured MCP server concurrently with the maintained SDK
183
+ * client over Streamable HTTP and project its tools into one Mycelium module
184
+ * per server. A server that rejects, refuses its credential, times out or
185
+ * advertises nothing usable is dropped and reported; the others stay
186
+ * available. Releasing the acquisition closes every client.
187
+ */
188
+ function mcpServers(input) {
189
+ const timeoutMillis = input.connectTimeoutMillis ?? defaultConnectTimeoutMillis;
190
+ const namespace = input.namespace ?? ["mcp"];
191
+ return {
192
+ id: input.id,
193
+ acquire: async (context) => {
194
+ context.signal.throwIfAborted();
195
+ const unsubscribe = input.subscribe(context.invalidate);
196
+ const opened = [];
197
+ const closeAll = async () => {
198
+ await Promise.allSettled(opened.map((server) => server.close()));
199
+ };
200
+ try {
201
+ const captured = await input.read();
202
+ if (captured.servers.length > maximumServers)
203
+ throw new Error(`At most ${String(maximumServers)} MCP servers per source`);
204
+ const results = await Effect.runPromise(Effect.forEach(captured.servers, (server) => Effect.result(connectWithin(server, namespace, input.fetch, timeoutMillis).pipe(
205
+ // Record each open client at once, so an aborted
206
+ // acquisition still closes the ones that connected.
207
+ Effect.tap((connected) => Effect.sync(() => opened.push(connected))))), { concurrency: connectConcurrency }), { signal: context.signal });
208
+ const versions = [captured.version];
209
+ for (const result of results) {
210
+ if (Result.isFailure(result)) {
211
+ input.onUnavailable(result.failure);
212
+ versions.push(`unavailable:${result.failure.serverId}:${result.failure.reason}`);
213
+ continue;
214
+ }
215
+ versions.push(result.success.version);
216
+ }
217
+ context.signal.throwIfAborted();
218
+ return {
219
+ snapshot: {
220
+ version: `mcp-${await digest(JSON.stringify(versions))}`,
221
+ modules: opened.map((server) => server.module),
222
+ },
223
+ release: async () => {
224
+ unsubscribe();
225
+ await closeAll();
226
+ },
227
+ };
228
+ }
229
+ catch (error) {
230
+ unsubscribe();
231
+ await closeAll();
232
+ throw error;
233
+ }
234
+ },
235
+ };
236
+ }
237
+ export { mcpServers };
@@ -0,0 +1 @@
1
+ export declare const patchGrammar = "start: begin_patch hunk+ end_patch\nbegin_patch: \"*** Begin Patch\" LF\nend_patch: \"*** End Patch\" LF?\n\nhunk: add_hunk | delete_hunk | update_hunk\nadd_hunk: \"*** Add File: \" filename LF add_line+\ndelete_hunk: \"*** Delete File: \" filename LF\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\n\nfilename: /(.+)/\nadd_line: \"+\" /(.*)/ LF -> line\n\nchange_move: \"*** Move to: \" filename LF\nchange: (change_context | change_line)+ eof_line?\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\neof_line: \"*** End of File\" LF\n\n%import common.LF\n";
@@ -0,0 +1,3 @@
1
+ // OpenAI Codex rust-v0.154.0 core/assets/tools/apply_patch.lark (Apache-2.0).
2
+ // Kept verbatim; the native engine, not this presentation grammar, validates patches.
3
+ export const patchGrammar = 'start: begin_patch hunk+ end_patch\nbegin_patch: "*** Begin Patch" LF\nend_patch: "*** End Patch" LF?\n\nhunk: add_hunk | delete_hunk | update_hunk\nadd_hunk: "*** Add File: " filename LF add_line+\ndelete_hunk: "*** Delete File: " filename LF\nupdate_hunk: "*** Update File: " filename LF change_move? change?\n\nfilename: /(.+)/\nadd_line: "+" /(.*)/ LF -> line\n\nchange_move: "*** Move to: " filename LF\nchange: (change_context | change_line)+ eof_line?\nchange_context: ("@@" | "@@ " /(.+)/) LF\nchange_line: ("+" | "-" | " ") /(.*)/ LF\neof_line: "*** End of File" LF\n\n%import common.LF\n';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Presentation reading of the Codex `apply_patch` grammar (see
3
+ * `patch-grammar.ts`). Pure and side-effect free: display consumers import it
4
+ * through `@fungi.computer/knapsack/patch`. The native patch engine, not this
5
+ * reader, validates and applies patches, so the reader is tolerant: text that
6
+ * does not match the grammar is skipped rather than rejected.
7
+ */
8
+ /** One body line of an update or add hunk. */
9
+ export type CodexPatchLine = Readonly<{
10
+ kind: "add" | "context" | "remove";
11
+ text: string;
12
+ }>;
13
+ /** One `@@` change block; `context` is the text after `@@`, when present. */
14
+ export type CodexPatchHunk = Readonly<{
15
+ context?: string | undefined;
16
+ lines: readonly CodexPatchLine[];
17
+ }>;
18
+ /** One file operation in a patch, in patch order. */
19
+ export type CodexPatchFile = Readonly<{
20
+ operation: "add" | "delete" | "update";
21
+ path: string;
22
+ /** Destination of an update with `*** Move to:`. */
23
+ moveTo?: string | undefined;
24
+ hunks: readonly CodexPatchHunk[];
25
+ }>;
26
+ /** Read the file operations of one Codex patch, in order. */
27
+ export declare function parseCodexPatch(patch: string): readonly CodexPatchFile[];
package/dist/patch.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Presentation reading of the Codex `apply_patch` grammar (see
3
+ * `patch-grammar.ts`). Pure and side-effect free: display consumers import it
4
+ * through `@fungi.computer/knapsack/patch`. The native patch engine, not this
5
+ * reader, validates and applies patches, so the reader is tolerant: text that
6
+ * does not match the grammar is skipped rather than rejected.
7
+ */
8
+ const fileHeaders = [
9
+ ["*** Add File: ", "add"],
10
+ ["*** Delete File: ", "delete"],
11
+ ["*** Update File: ", "update"],
12
+ ];
13
+ const moveHeader = /^\*\*\* Move to: (.+)$/u;
14
+ function bodyLine(line) {
15
+ if (line.startsWith("+"))
16
+ return { kind: "add", text: line.slice(1) };
17
+ if (line.startsWith("-"))
18
+ return { kind: "remove", text: line.slice(1) };
19
+ if (line.startsWith(" "))
20
+ return { kind: "context", text: line.slice(1) };
21
+ return undefined;
22
+ }
23
+ /** Read the file operations of one Codex patch, in order. */
24
+ export function parseCodexPatch(patch) {
25
+ const files = [];
26
+ let file;
27
+ for (const line of patch.split("\n")) {
28
+ const header = fileHeaders.find(([prefix]) => line.startsWith(prefix));
29
+ if (header !== undefined) {
30
+ const [prefix, operation] = header;
31
+ file = { operation, path: line.slice(prefix.length).trim(), hunks: [] };
32
+ files.push(file);
33
+ continue;
34
+ }
35
+ if (file === undefined)
36
+ continue;
37
+ const move = moveHeader.exec(line);
38
+ if (move !== null) {
39
+ file.moveTo = (move[1] ?? "").trim();
40
+ continue;
41
+ }
42
+ if (line.startsWith("@@")) {
43
+ const context = line.slice(2).trim();
44
+ file.hunks.push(context === "" ? { lines: [] } : { context, lines: [] });
45
+ continue;
46
+ }
47
+ const body = bodyLine(line);
48
+ if (body === undefined)
49
+ continue;
50
+ const hunk = file.hunks.at(-1) ?? { lines: [] };
51
+ if (file.hunks.length === 0)
52
+ file.hunks.push(hunk);
53
+ hunk.lines.push(body);
54
+ }
55
+ return files;
56
+ }
@@ -0,0 +1,17 @@
1
+ <!-- Generated by Botanical API docs (TypeDoc + typedoc-plugin-markdown). Do not edit. -->
2
+
3
+ **@fungi.computer/knapsack**
4
+
5
+ ---
6
+
7
+ # @fungi.computer/knapsack
8
+
9
+ ## Modules
10
+
11
+ | Module | Description |
12
+ | --------------------------------------- | ----------- |
13
+ | [files-patch](files-patch.md) | - |
14
+ | [files-read-codec](files-read-codec.md) | - |
15
+ | [files-read](files-read.md) | - |
16
+ | [index](index.md) | - |
17
+ | [patch](patch.md) | - |