@zswarm/pi 0.1.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/LICENSE +21 -0
- package/README.md +69 -0
- package/extensions/zswarm-mcp.ts +330 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zSwarm contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @zswarm/pi
|
|
2
|
+
|
|
3
|
+
Bridge stdio MCP servers into [pi](https://pi.dev) as native tools. Defaults to zSwarm; point it at any stdio MCP server with `ZSWARM_MCP_SERVERS`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@zswarm/pi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From a checkout of this repo:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pi install -l ./packages/pi
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`-l` installs into the project (`.pi/settings.json`) rather than your home
|
|
18
|
+
directory, and is shareable — pi installs it for anyone who opens the project.
|
|
19
|
+
|
|
20
|
+
Restart pi after installing. After that `/reload` is enough, since the extension
|
|
21
|
+
registers on every bind. On success:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
[Extensions]
|
|
25
|
+
zswarm-mcp.ts
|
|
26
|
+
MCP zswarm: 1 tool(s) registered
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configure
|
|
30
|
+
|
|
31
|
+
With nothing set, it looks for zSwarm's own server in the only two places it can
|
|
32
|
+
be, in order:
|
|
33
|
+
|
|
34
|
+
1. `<cwd>/bin/launch-mcp.mjs` — a checkout of the zswarm repo, pi started at its root
|
|
35
|
+
2. `@zswarm/mcp` — resolved by name, wherever your package manager put it
|
|
36
|
+
|
|
37
|
+
`@zswarm/mcp` is a dependency of this package, so a plain
|
|
38
|
+
`pi install npm:@zswarm/pi` pulls the server in and step 2 finds it. Nothing to
|
|
39
|
+
configure.
|
|
40
|
+
|
|
41
|
+
Found in neither, it registers nothing and says so. It does not guess a relative
|
|
42
|
+
path: a wrong guess becomes a spawn error you have to decode, whereas finding
|
|
43
|
+
nothing names the variable that fixes it.
|
|
44
|
+
|
|
45
|
+
Set `ZSWARM_MCP_SERVERS` to bridge a **different** server — JSON, same shape as
|
|
46
|
+
other MCP hosts:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ZSWARM_MCP_SERVERS='{
|
|
50
|
+
"zswarm": {"command": "/abs/path/to/node", "args": ["/abs/path/to/launch-mcp.mjs"]},
|
|
51
|
+
"other": {"command": "/abs/path/to/other-server"}
|
|
52
|
+
}'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use absolute paths throughout. Hosts that spawn servers outside your shell
|
|
56
|
+
inherit no `PATH`, so a bare `node` fails with
|
|
57
|
+
`exec: "node": executable file not found in $PATH`. The built-in defaults use
|
|
58
|
+
`process.execPath` — the interpreter already running pi — for exactly this
|
|
59
|
+
reason.
|
|
60
|
+
|
|
61
|
+
Tools register as `<server>_<tool>`, or `<tool>` when the names match. A failed server is skipped. Set `ZSWARM_SESSION` if more than one Zellij session is live.
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
On load: spawn each server → `initialize` → `tools/list` → `pi.registerTool()`. A call is `tools/call`. JSON-RPC, no SDK; schemas pass through as-is. Tools may not be ready in the same instant pi starts.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge MCP tools into pi.
|
|
3
|
+
*
|
|
4
|
+
* pi ships no MCP client on purpose — its position is that a CLI with a README
|
|
5
|
+
* is the better interface. That holds right up until you want one surface that
|
|
6
|
+
* every harness in a crew shares, which is what an MCP server already is. This
|
|
7
|
+
* extension spawns a stdio MCP server, asks it for its tools, and registers
|
|
8
|
+
* each one with pi, so the same tool call works here and in the MCP hosts.
|
|
9
|
+
*
|
|
10
|
+
* It speaks the protocol directly rather than depending on an SDK: an extension
|
|
11
|
+
* is loaded from the project tree and cannot rely on the repo's node_modules
|
|
12
|
+
* being resolvable from wherever pi was started.
|
|
13
|
+
*
|
|
14
|
+
* Configure with ZSWARM_MCP_SERVERS (JSON, same shape as any MCP host config):
|
|
15
|
+
*
|
|
16
|
+
* {"zswarm":{"command":"/abs/path/to/node","args":["/abs/path/to/server.js"]}}
|
|
17
|
+
*
|
|
18
|
+
* With nothing set it looks for zSwarm's own server in the two places it can
|
|
19
|
+
* actually be, and registers nothing if it is in neither — see defaultServers.
|
|
20
|
+
*/
|
|
21
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
23
|
+
import { existsSync } from "node:fs";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
|
|
27
|
+
type ServerSpec = {
|
|
28
|
+
command: string;
|
|
29
|
+
args?: string[];
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type McpTool = {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputSchema?: Record<string, unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where zSwarm's own server might be, in order of confidence.
|
|
41
|
+
*
|
|
42
|
+
* `process.execPath` rather than `"node"`: a host that spawns servers outside
|
|
43
|
+
* your shell inherits no PATH, and the interpreter already running this code is
|
|
44
|
+
* both absolute and known-good.
|
|
45
|
+
*
|
|
46
|
+
* There is deliberately no fallback to a bare relative path. Guessing produces
|
|
47
|
+
* a spawn failure the user has to decode; finding nothing produces a message
|
|
48
|
+
* that names the environment variable that fixes it.
|
|
49
|
+
*/
|
|
50
|
+
function resolveDependencyServer(): string | null {
|
|
51
|
+
try {
|
|
52
|
+
return createRequire(import.meta.url).resolve("@zswarm/mcp");
|
|
53
|
+
} catch {
|
|
54
|
+
return null; // Not installed; the checkout path or an override covers it.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function defaultServers(): Record<string, ServerSpec> {
|
|
59
|
+
const candidates = [
|
|
60
|
+
// A checkout of the zswarm repo, with pi started at its root.
|
|
61
|
+
resolve(process.cwd(), "bin", "launch-mcp.mjs"),
|
|
62
|
+
// Installed as a dependency: @zswarm/mcp exports its server entry, so
|
|
63
|
+
// node resolution finds it wherever the package manager put it.
|
|
64
|
+
resolveDependencyServer(),
|
|
65
|
+
].filter((entry): entry is string => entry !== null);
|
|
66
|
+
for (const entry of candidates) {
|
|
67
|
+
if (existsSync(entry)) {
|
|
68
|
+
return { zswarm: { command: process.execPath, args: [entry] } };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How long to wait for a server to answer initialize/tools-list before giving up. */
|
|
75
|
+
const HANDSHAKE_MS = 20_000;
|
|
76
|
+
/** Tool calls can legitimately block — `wait` holds until a pane goes idle. */
|
|
77
|
+
const CALL_MS = 15 * 60_000;
|
|
78
|
+
|
|
79
|
+
function readServers(): Record<string, ServerSpec> {
|
|
80
|
+
const raw = process.env.ZSWARM_MCP_SERVERS?.trim();
|
|
81
|
+
if (!raw) return defaultServers();
|
|
82
|
+
try {
|
|
83
|
+
const parsed: unknown = JSON.parse(raw);
|
|
84
|
+
if (parsed && typeof parsed === "object") {
|
|
85
|
+
return parsed as Record<string, ServerSpec>;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// A malformed override should not cost you the default server.
|
|
89
|
+
}
|
|
90
|
+
return defaultServers();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* One stdio MCP server, kept alive for the session.
|
|
95
|
+
*
|
|
96
|
+
* Framing is newline-delimited JSON-RPC. Responses are matched by id because
|
|
97
|
+
* a server may interleave notifications, and stdout can split a message across
|
|
98
|
+
* chunk boundaries — so bytes are buffered until a newline lands.
|
|
99
|
+
*/
|
|
100
|
+
class McpClient {
|
|
101
|
+
private child: ChildProcessWithoutNullStreams | null = null;
|
|
102
|
+
private buffer = "";
|
|
103
|
+
private nextId = 1;
|
|
104
|
+
private readonly pending = new Map<
|
|
105
|
+
number,
|
|
106
|
+
{ resolve: (value: unknown) => void; reject: (error: Error) => void }
|
|
107
|
+
>();
|
|
108
|
+
|
|
109
|
+
constructor(
|
|
110
|
+
private readonly name: string,
|
|
111
|
+
private readonly spec: ServerSpec,
|
|
112
|
+
private readonly cwd: string,
|
|
113
|
+
) {}
|
|
114
|
+
|
|
115
|
+
async start(): Promise<McpTool[]> {
|
|
116
|
+
const child = spawn(this.spec.command, this.spec.args ?? [], {
|
|
117
|
+
cwd: this.cwd,
|
|
118
|
+
env: { ...process.env, ...(this.spec.env ?? {}) },
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
120
|
+
windowsHide: true,
|
|
121
|
+
});
|
|
122
|
+
this.child = child;
|
|
123
|
+
|
|
124
|
+
child.stdout.setEncoding("utf8");
|
|
125
|
+
child.stdout.on("data", (chunk: string) => this.consume(chunk));
|
|
126
|
+
child.on("error", (err) => this.failAll(err));
|
|
127
|
+
child.on("close", () => this.failAll(new Error(`${this.name} exited`)));
|
|
128
|
+
|
|
129
|
+
await this.request(
|
|
130
|
+
"initialize",
|
|
131
|
+
{
|
|
132
|
+
protocolVersion: "2024-11-05",
|
|
133
|
+
capabilities: {},
|
|
134
|
+
clientInfo: { name: "pi", version: "1" },
|
|
135
|
+
},
|
|
136
|
+
HANDSHAKE_MS,
|
|
137
|
+
);
|
|
138
|
+
this.notify("notifications/initialized", {});
|
|
139
|
+
|
|
140
|
+
const listed = (await this.request("tools/list", {}, HANDSHAKE_MS)) as {
|
|
141
|
+
tools?: McpTool[];
|
|
142
|
+
};
|
|
143
|
+
return listed.tools ?? [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private consume(chunk: string): void {
|
|
147
|
+
this.buffer += chunk;
|
|
148
|
+
let newline = this.buffer.indexOf("\n");
|
|
149
|
+
while (newline !== -1) {
|
|
150
|
+
const line = this.buffer.slice(0, newline).trim();
|
|
151
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
152
|
+
if (line) this.dispatch(line);
|
|
153
|
+
newline = this.buffer.indexOf("\n");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private dispatch(line: string): void {
|
|
158
|
+
let message: { id?: number; result?: unknown; error?: { message?: string } };
|
|
159
|
+
try {
|
|
160
|
+
message = JSON.parse(line);
|
|
161
|
+
} catch {
|
|
162
|
+
return; // Servers may log to stdout; ignore anything that is not a message.
|
|
163
|
+
}
|
|
164
|
+
if (typeof message.id !== "number") return;
|
|
165
|
+
const waiter = this.pending.get(message.id);
|
|
166
|
+
if (!waiter) return;
|
|
167
|
+
this.pending.delete(message.id);
|
|
168
|
+
if (message.error) {
|
|
169
|
+
waiter.reject(new Error(message.error.message ?? "MCP error"));
|
|
170
|
+
} else {
|
|
171
|
+
waiter.resolve(message.result);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private failAll(error: Error): void {
|
|
176
|
+
for (const waiter of this.pending.values()) waiter.reject(error);
|
|
177
|
+
this.pending.clear();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private send(payload: Record<string, unknown>): void {
|
|
181
|
+
this.child?.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private notify(method: string, params: unknown): void {
|
|
185
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private request(
|
|
189
|
+
method: string,
|
|
190
|
+
params: unknown,
|
|
191
|
+
timeoutMs: number,
|
|
192
|
+
): Promise<unknown> {
|
|
193
|
+
const id = this.nextId++;
|
|
194
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
195
|
+
const timer = setTimeout(() => {
|
|
196
|
+
this.pending.delete(id);
|
|
197
|
+
reject(new Error(`${this.name} ${method} timed out`));
|
|
198
|
+
}, timeoutMs);
|
|
199
|
+
this.pending.set(id, {
|
|
200
|
+
resolve: (value) => {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
resolve(value);
|
|
203
|
+
},
|
|
204
|
+
reject: (error) => {
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
reject(error);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async call(tool: string, args: Record<string, unknown>): Promise<string> {
|
|
214
|
+
const result = (await this.request(
|
|
215
|
+
"tools/call",
|
|
216
|
+
{ name: tool, arguments: args },
|
|
217
|
+
CALL_MS,
|
|
218
|
+
)) as { content?: Array<{ type?: string; text?: string }> };
|
|
219
|
+
const text = (result.content ?? [])
|
|
220
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
221
|
+
.map((part) => part.text)
|
|
222
|
+
.join("\n");
|
|
223
|
+
return text || JSON.stringify(result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
stop(): void {
|
|
227
|
+
this.child?.kill();
|
|
228
|
+
this.child = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export default function (pi: ExtensionAPI) {
|
|
233
|
+
const clients: McpClient[] = [];
|
|
234
|
+
/** What each server reported at connect time, for the session_start notice. */
|
|
235
|
+
const registered: Array<{ name: string; count: number; error?: string }> = [];
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Connect and register at module load rather than from `session_start`.
|
|
239
|
+
*
|
|
240
|
+
* `/reload` rebinds extensions, but only a session switch, fork, or new
|
|
241
|
+
* session is documented to re-emit `session_start` — so a handler is the
|
|
242
|
+
* wrong place to put the one thing that must happen on every bind. A fresh
|
|
243
|
+
* module instance is created per bind, so this closure runs exactly once
|
|
244
|
+
* each time and needs no idempotence guard.
|
|
245
|
+
*
|
|
246
|
+
* Registration is async because a server's tools are unknown until it has
|
|
247
|
+
* been asked, and the default export cannot await. Tools therefore appear a
|
|
248
|
+
* moment after load; `session_start` reports what landed.
|
|
249
|
+
*/
|
|
250
|
+
const connecting = (async () => {
|
|
251
|
+
for (const [name, spec] of Object.entries(readServers())) {
|
|
252
|
+
// No ExtensionContext at load time, so the project root comes from the
|
|
253
|
+
// process — pi is started in the directory it operates on.
|
|
254
|
+
const client = new McpClient(name, spec, process.cwd());
|
|
255
|
+
let tools: McpTool[];
|
|
256
|
+
try {
|
|
257
|
+
tools = await client.start();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
// A missing server is a missing capability, never a broken session.
|
|
260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
261
|
+
registered.push({ name, count: 0, error: message });
|
|
262
|
+
client.stop();
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
clients.push(client);
|
|
266
|
+
registered.push({ name, count: tools.length });
|
|
267
|
+
|
|
268
|
+
for (const tool of tools) {
|
|
269
|
+
// `server_tool`, unless the server named its tool after itself.
|
|
270
|
+
const toolName = tool.name === name ? name : `${name}_${tool.name}`;
|
|
271
|
+
pi.registerTool({
|
|
272
|
+
name: toolName,
|
|
273
|
+
label: toolName,
|
|
274
|
+
description: tool.description ?? `${tool.name} via the ${name} MCP server`,
|
|
275
|
+
promptSnippet: (tool.description ?? toolName).split(".")[0],
|
|
276
|
+
// An MCP inputSchema is already JSON Schema, which is what the
|
|
277
|
+
// TypeBox helpers produce — so it is handed over unchanged.
|
|
278
|
+
parameters: (tool.inputSchema ?? {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {},
|
|
281
|
+
}) as never,
|
|
282
|
+
async execute(_toolCallId, params, signal) {
|
|
283
|
+
if (signal?.aborted) {
|
|
284
|
+
return { content: [{ type: "text", text: "Cancelled" }], details: {} };
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const text = await client.call(
|
|
288
|
+
tool.name,
|
|
289
|
+
(params ?? {}) as Record<string, unknown>,
|
|
290
|
+
);
|
|
291
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: `MCP ${name} error: ${message}` }],
|
|
296
|
+
details: { error: message },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
|
|
305
|
+
// Surface the outcome once there is a UI to surface it to. Awaiting the same
|
|
306
|
+
// promise means this reports rather than repeats the work.
|
|
307
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
308
|
+
await connecting;
|
|
309
|
+
if (registered.length === 0) {
|
|
310
|
+
ctx.ui.notify(
|
|
311
|
+
"No MCP server found. Set ZSWARM_MCP_SERVERS to point at one.",
|
|
312
|
+
"error",
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
for (const entry of registered) {
|
|
317
|
+
if (entry.error) {
|
|
318
|
+
ctx.ui.notify(`MCP ${entry.name} unavailable: ${entry.error}`, "error");
|
|
319
|
+
} else {
|
|
320
|
+
ctx.ui.notify(`MCP ${entry.name}: ${entry.count} tool(s) registered`, "info");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Idempotent by construction: the list is emptied as it is drained.
|
|
326
|
+
pi.on("session_shutdown", async () => {
|
|
327
|
+
for (const client of clients) client.stop();
|
|
328
|
+
clients.length = 0;
|
|
329
|
+
});
|
|
330
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zswarm/pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Bridge stdio MCP servers into pi as native tools",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"mcp",
|
|
9
|
+
"zellij",
|
|
10
|
+
"zswarm"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"pi": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./extensions"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"extensions",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": {
|
|
27
|
+
"optional": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@zswarm/mcp": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {}
|
|
40
|
+
}
|