pi-subagent-windows 0.0.0-stage → 0.1.1

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,206 @@
1
+ #!/usr/bin/env node
2
+
3
+ // supervisor.ts
4
+ import { appendFileSync, createWriteStream, writeFileSync as writeFileSync2 } from "node:fs";
5
+ import { spawn } from "node:child_process";
6
+ import { join as join2 } from "node:path";
7
+
8
+ // shared.ts
9
+ import {
10
+ accessSync,
11
+ constants,
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ renameSync,
17
+ rmSync,
18
+ writeFileSync
19
+ } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { dirname, join, resolve } from "node:path";
22
+ function getAgentDir() {
23
+ return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
24
+ }
25
+ function supervisorPidPath(runDir2) {
26
+ return join(runDir2, "supervisor.pid");
27
+ }
28
+ function resolvePiCliPath() {
29
+ const installRoot2 = join(getAgentDir(), "install");
30
+ const currentVersion = readFileSync(join(installRoot2, "current-version"), "utf8").trim();
31
+ if (!currentVersion || !/^[0-9A-Za-z._+-]+$/.test(currentVersion)) {
32
+ throw new Error("The managed Pi version file is invalid");
33
+ }
34
+ const packageDirectory = join(installRoot2, "releases", currentVersion, "node_modules", "@earendil-works", "pi-coding-agent");
35
+ const manifest = JSON.parse(readFileSync(join(packageDirectory, "package.json"), "utf8"));
36
+ const bin = manifest.bin;
37
+ const binPath = typeof bin === "string" ? bin : bin?.pi;
38
+ if (!binPath) throw new Error("The installed Pi package does not declare a pi bin");
39
+ return resolve(packageDirectory, binPath);
40
+ }
41
+ function metadataPath(runDir2) {
42
+ return join(runDir2, "metadata.json");
43
+ }
44
+ function isValidRunName(value) {
45
+ return typeof value === "string" && value.length > 0 && value.length <= 64 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
46
+ }
47
+ function readMetadata(runDir2) {
48
+ try {
49
+ const value = JSON.parse(readFileSync(metadataPath(runDir2), "utf8"));
50
+ if (typeof value !== "object" || value === null) return void 0;
51
+ const metadata2 = value;
52
+ if (metadata2.version !== 1 || typeof metadata2.handle !== "string" || metadata2.name !== void 0 && !isValidRunName(metadata2.name) || typeof metadata2.tmuxSession !== "string" || typeof metadata2.sessionFile !== "string" || typeof metadata2.runDir !== "string") {
53
+ return void 0;
54
+ }
55
+ return metadata2;
56
+ } catch {
57
+ return void 0;
58
+ }
59
+ }
60
+ function writeMetadata(metadata2) {
61
+ mkdirSync(dirname(metadataPath(metadata2.runDir)), { recursive: true, mode: 448 });
62
+ const target = metadataPath(metadata2.runDir);
63
+ const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
64
+ writeFileSync(temporary, `${JSON.stringify(metadata2, null, 2)}
65
+ `, { encoding: "utf8", mode: 384 });
66
+ renameSync(temporary, target);
67
+ }
68
+ function updateMetadata(runDir2, patch) {
69
+ const current = readMetadata(runDir2);
70
+ if (!current) return void 0;
71
+ const next = {
72
+ ...current,
73
+ ...patch,
74
+ version: 1,
75
+ handle: current.handle,
76
+ runDir: current.runDir,
77
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
78
+ };
79
+ writeMetadata(next);
80
+ return next;
81
+ }
82
+
83
+ // supervisor.ts
84
+ var runDir = process.argv[2];
85
+ if (!runDir) {
86
+ process.stderr.write("pi-subagent-windows supervisor: missing run directory\n");
87
+ process.exit(1);
88
+ }
89
+ var metadata = readMetadata(runDir);
90
+ if (!metadata) {
91
+ process.stderr.write(`pi-subagent-windows supervisor: no valid metadata in ${runDir}
92
+ `);
93
+ process.exit(1);
94
+ }
95
+ var logPath = join2(runDir, "supervisor.log");
96
+ var log = (message) => {
97
+ try {
98
+ appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
99
+ `, { encoding: "utf8" });
100
+ } catch {
101
+ }
102
+ };
103
+ writeFileSync2(supervisorPidPath(runDir), `${process.pid}
104
+ `, { encoding: "utf8", mode: 384 });
105
+ var installRoot = join2(getAgentDir(), "install");
106
+ var cliPath;
107
+ try {
108
+ cliPath = resolvePiCliPath();
109
+ } catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ log(`could not resolve the Pi CLI: ${message}`);
112
+ updateMetadata(runDir, { state: "error", error: message });
113
+ process.exit(1);
114
+ }
115
+ var args = [
116
+ cliPath,
117
+ "--mode",
118
+ "rpc",
119
+ "--session",
120
+ metadata.sessionFile,
121
+ "--provider",
122
+ metadata.provider,
123
+ "--model",
124
+ metadata.model,
125
+ "--thinking",
126
+ metadata.thinking,
127
+ ...metadata.launchArgs ?? []
128
+ ];
129
+ log(`supervisor pid=${process.pid} starting child: ${JSON.stringify(args)}`);
130
+ var child = spawn(process.execPath, args, {
131
+ cwd: metadata.cwd,
132
+ windowsHide: true,
133
+ stdio: ["pipe", "pipe", "pipe"],
134
+ env: {
135
+ ...process.env,
136
+ PI_SUBAGENT_RUN_DIR: runDir,
137
+ PI_MANAGED_INSTALL_ROOT: installRoot
138
+ }
139
+ });
140
+ if (!child.pid || !child.stdin || !child.stdout) {
141
+ log("failed to spawn the Pi RPC child");
142
+ updateMetadata(runDir, { state: "error", error: "Failed to spawn the Pi RPC child" });
143
+ process.exit(1);
144
+ }
145
+ log(`child pid=${child.pid}`);
146
+ child.stderr.pipe(createWriteStream(logPath, { flags: "a" }));
147
+ function respond(id, payload) {
148
+ try {
149
+ child.stdin.write(`${JSON.stringify({ type: "extension_ui_response", id, ...payload })}
150
+ `);
151
+ } catch {
152
+ }
153
+ }
154
+ var stdoutBuffer = "";
155
+ child.stdout.on("data", (data) => {
156
+ stdoutBuffer += data.toString("utf8");
157
+ let index;
158
+ while ((index = stdoutBuffer.indexOf("\n")) !== -1) {
159
+ const line = stdoutBuffer.slice(0, index).replace(/\r$/, "");
160
+ stdoutBuffer = stdoutBuffer.slice(index + 1);
161
+ if (!line.trim()) continue;
162
+ let record;
163
+ try {
164
+ record = JSON.parse(line);
165
+ } catch {
166
+ continue;
167
+ }
168
+ if (record.type !== "extension_ui_request" || typeof record.id !== "string") continue;
169
+ switch (record.method) {
170
+ case "confirm":
171
+ log("auto-declined an extension confirm dialog (no interactive UI in hidden subagents)");
172
+ respond(record.id, { confirmed: false });
173
+ break;
174
+ case "select":
175
+ case "input":
176
+ case "editor":
177
+ log(`auto-cancelled an extension ${record.method} dialog (no interactive UI in hidden subagents)`);
178
+ respond(record.id, { cancelled: true });
179
+ break;
180
+ default:
181
+ break;
182
+ }
183
+ }
184
+ });
185
+ child.on("error", (error) => {
186
+ log(`child process error: ${error.message}`);
187
+ updateMetadata(runDir, { state: "error", error: error.message });
188
+ process.exit(1);
189
+ });
190
+ child.on("exit", (code, signal) => {
191
+ log(`child exited code=${code} signal=${signal}`);
192
+ const current = readMetadata(runDir);
193
+ if (current && current.state !== "exited" && current.state !== "error") {
194
+ updateMetadata(
195
+ runDir,
196
+ code === 0 ? { state: "exited" } : { state: "error", error: `Subagent process exited unexpectedly (code=${code}, signal=${signal})` }
197
+ );
198
+ }
199
+ process.exit(0);
200
+ });
201
+ process.on("SIGTERM", () => {
202
+ try {
203
+ child.stdin.end();
204
+ } catch {
205
+ }
206
+ });
package/index.ts ADDED
@@ -0,0 +1,345 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, chmodSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { DynamicBorder, type ExtensionAPI, type ExtensionContext, keyHint } from "@earendil-works/pi-coding-agent";
6
+ import { Container, type SelectItem, SelectList, Text, type TUI } from "@earendil-works/pi-tui";
7
+ import {
8
+ effectiveRunState,
9
+ findRuntimeFile,
10
+ getAgentDir,
11
+ inboxDir,
12
+ isRunAlive,
13
+ killRun,
14
+ launchRun,
15
+ listRuns,
16
+ readMetadata,
17
+ removeRunDir,
18
+ runDisplayName,
19
+ type InboxMessage,
20
+ type RunMetadata,
21
+ updateMetadata,
22
+ waitForRunShutdown,
23
+ } from "./shared.ts";
24
+
25
+ const packageDir = dirname(fileURLToPath(import.meta.url));
26
+
27
+ /**
28
+ * On Windows, keep `subagent.cmd` (cmd/PowerShell) and an extensionless `subagent`
29
+ * sh script (Git Bash, which does not resolve bare names through PATHEXT) in the
30
+ * agent bin directory so the CLI is on PATH after install. Never clobbers a file
31
+ * this extension did not write.
32
+ */
33
+ function ensureCliShim(): void {
34
+ if (process.platform !== "win32") return;
35
+ try {
36
+ const binDir = join(getAgentDir(), "bin");
37
+ const cliPath = findRuntimeFile("subagent");
38
+ const cliPathUnix = cliPath.replace(/\\/g, "/");
39
+
40
+ const cmdMarker = "rem pi-subagent-windows shim";
41
+ const cmdContent = `@ECHO off\r\n${cmdMarker}\r\nnode "${cliPath}" %*\r\n`;
42
+
43
+ const shMarker = "# pi-subagent-windows shim";
44
+ const shContent = `#!/bin/sh\n${shMarker}\nexec node "${cliPathUnix}" "$@"\n`;
45
+
46
+ mkdirSync(binDir, { recursive: true });
47
+ const writeShim = (path: string, marker: string, content: string, mode?: number): void => {
48
+ let shouldWrite = true;
49
+ if (existsSync(path)) {
50
+ const current = readFileSync(path, "utf8");
51
+ if (!current.includes(marker)) return; // foreign file: leave it alone
52
+ if (current === content) shouldWrite = false;
53
+ }
54
+ if (shouldWrite) writeFileSync(path, content, { encoding: "utf8" });
55
+ if (mode !== undefined) chmodSync(path, mode);
56
+ };
57
+ writeShim(join(binDir, "subagent.cmd"), cmdMarker, cmdContent);
58
+ writeShim(join(binDir, "subagent"), shMarker, shContent, 0o755);
59
+ } catch {
60
+ // The shim is a convenience; extension loading must never fail over it.
61
+ }
62
+ }
63
+
64
+ function isInboxMessage(value: unknown): value is InboxMessage {
65
+ if (typeof value !== "object" || value === null) return false;
66
+ const message = value as Record<string, unknown>;
67
+ return typeof message.message === "string" && (message.delivery === "auto" || message.delivery === "followUp");
68
+ }
69
+
70
+ function displayState(metadata: RunMetadata): string {
71
+ return effectiveRunState(metadata).padEnd(8);
72
+ }
73
+
74
+ export default function subagentExtension(pi: ExtensionAPI) {
75
+ const runDir = process.env.PI_SUBAGENT_RUN_DIR;
76
+ if (!runDir) {
77
+ pi.on("resources_discover", () => ({ skillPaths: [join(packageDir, "skills")] }));
78
+ ensureCliShim();
79
+ }
80
+
81
+ pi.registerCommand("subagent", {
82
+ description: "Select a subagent spawned by this session",
83
+ handler: async (_args, ctx) => {
84
+ const runs = listRuns(ctx.sessionManager.getSessionId()).filter((run) => effectiveRunState(run) !== "exited");
85
+ if (runs.length === 0) {
86
+ ctx.ui.notify("No active subagents spawned by this session", "info");
87
+ return;
88
+ }
89
+
90
+ const items: SelectItem[] = runs.map((run) => ({
91
+ value: run.handle,
92
+ label: `${runDisplayName(run)} ${displayState(run)} ${run.provider}/${run.model} ${run.thinking}`,
93
+ }));
94
+ let tui: TUI | undefined;
95
+ const selected = await ctx.ui.custom<string | undefined>((customTui, theme, _keybindings, done) => {
96
+ tui = customTui;
97
+ const list = new SelectList(items, Math.min(items.length, 10), {
98
+ selectedPrefix: (text) => theme.fg("accent", text),
99
+ selectedText: (text) => theme.fg("accent", text),
100
+ description: (text) => theme.fg("muted", text),
101
+ scrollInfo: (text) => theme.fg("dim", text),
102
+ noMatch: (text) => theme.fg("warning", text),
103
+ });
104
+ list.onSelect = (item) => done(item.value);
105
+ list.onCancel = () => done(undefined);
106
+
107
+ const container = new Container();
108
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
109
+ container.addChild(
110
+ new Text(theme.fg("accent", theme.bold(process.platform === "win32" ? "Subagent status" : "Attach to subagent")), 1, 0),
111
+ );
112
+ container.addChild(list);
113
+ container.addChild(
114
+ new Text(
115
+ theme.fg(
116
+ "dim",
117
+ `${keyHint("tui.select.confirm", process.platform === "win32" ? "status" : "attach")} ${keyHint("tui.select.cancel", "cancel")}`,
118
+ ),
119
+ 1,
120
+ 0,
121
+ ),
122
+ );
123
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
124
+
125
+ return {
126
+ render: (width) => container.render(width),
127
+ invalidate: () => container.invalidate(),
128
+ handleInput: (data) => {
129
+ list.handleInput(data);
130
+ customTui.requestRender();
131
+ },
132
+ };
133
+ });
134
+ if (!selected || !tui) return;
135
+ const run = runs.find((candidate) => candidate.handle === selected);
136
+ if (!run) return;
137
+
138
+ if (process.platform === "win32") {
139
+ ctx.ui.notify(
140
+ `${runDisplayName(run)}: ${effectiveRunState(run)} — session: ${run.sessionFile}`,
141
+ "info",
142
+ );
143
+ return;
144
+ }
145
+
146
+ if (process.env.TMUX) {
147
+ const exitCode = await new Promise<number | null>((resolveExit) => {
148
+ const child = spawn("tmux", ["switch-client", "-t", run.tmuxSession], { stdio: "inherit" });
149
+ child.on("error", () => resolveExit(null));
150
+ child.on("close", resolveExit);
151
+ });
152
+ if (exitCode !== 0) ctx.ui.notify(`Could not switch to ${run.handle}`, "error");
153
+ return;
154
+ }
155
+
156
+ tui.stop();
157
+ try {
158
+ const exitCode = await new Promise<number | null>((resolveExit) => {
159
+ const child = spawn("tmux", ["attach-session", "-t", run.tmuxSession], { stdio: "inherit" });
160
+ child.on("error", () => resolveExit(null));
161
+ child.on("close", resolveExit);
162
+ });
163
+ if (exitCode !== 0) process.stderr.write(`Could not attach to ${run.handle}\n`);
164
+ } finally {
165
+ tui.start();
166
+ tui.requestRender(true);
167
+ }
168
+ },
169
+ });
170
+
171
+ if (!runDir) {
172
+ let widgetTimer: ReturnType<typeof setInterval> | undefined;
173
+ let widgetContext: ExtensionContext | undefined;
174
+
175
+ const refreshWidget = (): void => {
176
+ if (!widgetContext) return;
177
+ const activeRuns = listRuns(widgetContext.sessionManager.getSessionId())
178
+ .map((run) => ({ run, state: effectiveRunState(run) }))
179
+ .filter(({ state }) => state !== "exited");
180
+ if (activeRuns.length === 0) {
181
+ widgetContext.ui.setWidget("subagents", undefined);
182
+ return;
183
+ }
184
+
185
+ const visible = activeRuns.slice(0, 5).map(({ run, state }) => {
186
+ const color =
187
+ state === "busy" ? "warning" : state === "idle" ? "success" : state === "error" ? "error" : "muted";
188
+ return widgetContext!.ui.theme.fg(color, `${run.name ?? run.handle}:${state}`);
189
+ });
190
+ if (activeRuns.length > visible.length) {
191
+ visible.push(widgetContext.ui.theme.fg("muted", `+${activeRuns.length - visible.length}`));
192
+ }
193
+ widgetContext.ui.setWidget(
194
+ "subagents",
195
+ [widgetContext.ui.theme.fg("dim", "subagents: ") + visible.join(widgetContext.ui.theme.fg("dim", " | "))],
196
+ { placement: "belowEditor" },
197
+ );
198
+ };
199
+
200
+ pi.on("session_start", (_event, ctx) => {
201
+ // Relaunch children that were suspended when this session was last quit or switched away from.
202
+ for (const run of listRuns(ctx.sessionManager.getSessionId())) {
203
+ if (!run.suspended || isRunAlive(run)) continue;
204
+ if (!existsSync(run.sessionFile)) {
205
+ removeRunDir(run.runDir);
206
+ continue;
207
+ }
208
+ const starting = updateMetadata(run.runDir, { state: "starting", error: undefined }) ?? run;
209
+ try {
210
+ launchRun(starting);
211
+ } catch (error) {
212
+ const message = error instanceof Error ? error.message : String(error);
213
+ updateMetadata(run.runDir, { state: "error", error: message });
214
+ if (ctx.hasUI) ctx.ui.notify(`Could not resume subagent ${runDisplayName(run)}: ${message}`, "error");
215
+ }
216
+ }
217
+
218
+ if (!ctx.hasUI) return;
219
+ widgetContext = ctx;
220
+ refreshWidget();
221
+ widgetTimer = setInterval(refreshWidget, 1000);
222
+ widgetTimer.unref();
223
+ });
224
+
225
+ pi.on("session_shutdown", async (event, ctx) => {
226
+ if (widgetTimer) clearInterval(widgetTimer);
227
+ widgetTimer = undefined;
228
+ widgetContext = undefined;
229
+ ctx.ui.setWidget("subagents", undefined);
230
+ if (event.reason === "reload") return;
231
+ // Suspend running children: stop the process but keep transcript and metadata so resuming this
232
+ // session relaunches them. Children that already exited on their own are discarded.
233
+ for (const run of listRuns(ctx.sessionManager.getSessionId())) {
234
+ if (!isRunAlive(run)) {
235
+ if (!run.suspended) removeRunDir(run.runDir);
236
+ continue;
237
+ }
238
+ updateMetadata(run.runDir, { suspended: true });
239
+ killRun(run);
240
+ await waitForRunShutdown(run.runDir);
241
+ }
242
+ });
243
+ return;
244
+ }
245
+
246
+ let currentContext: ExtensionContext | undefined;
247
+ let timer: ReturnType<typeof setInterval> | undefined;
248
+ let processing = false;
249
+ let sessionName: string | undefined;
250
+
251
+ const syncSessionName = (): void => {
252
+ const metadata = readMetadata(runDir);
253
+ if (!metadata) return;
254
+ const next = `subagent ${metadata.name ?? metadata.handle}`;
255
+ if (next === sessionName) return;
256
+ pi.setSessionName(next);
257
+ sessionName = next;
258
+ };
259
+
260
+ const processInbox = async (): Promise<void> => {
261
+ if (processing || !currentContext) return;
262
+ syncSessionName();
263
+ const queueDir = inboxDir(runDir);
264
+ if (!existsSync(queueDir)) return;
265
+ processing = true;
266
+ try {
267
+ for (const name of readdirSync(queueDir)
268
+ .filter((entry) => entry.endsWith(".json"))
269
+ .sort()) {
270
+ const path = join(queueDir, name);
271
+ let payload: InboxMessage;
272
+ try {
273
+ const value: unknown = JSON.parse(readFileSync(path, "utf8"));
274
+ if (!isInboxMessage(value)) throw new Error("Invalid inbox message");
275
+ payload = value;
276
+ } catch (error) {
277
+ unlinkSync(path);
278
+ updateMetadata(runDir, {
279
+ state: "error",
280
+ error: error instanceof Error ? error.message : String(error),
281
+ });
282
+ continue;
283
+ }
284
+
285
+ updateMetadata(runDir, { state: "busy", error: undefined });
286
+ try {
287
+ if (currentContext.isIdle()) {
288
+ pi.sendUserMessage(payload.message);
289
+ } else {
290
+ pi.sendUserMessage(payload.message, {
291
+ deliverAs: payload.delivery === "followUp" ? "followUp" : "steer",
292
+ });
293
+ }
294
+ unlinkSync(path);
295
+ } catch (error) {
296
+ updateMetadata(runDir, {
297
+ state: currentContext.isIdle() ? "idle" : "busy",
298
+ error: error instanceof Error ? error.message : String(error),
299
+ });
300
+ return;
301
+ }
302
+ }
303
+ } finally {
304
+ processing = false;
305
+ }
306
+ };
307
+
308
+ pi.on("session_start", (_event, ctx) => {
309
+ currentContext = ctx;
310
+ const metadata = readMetadata(runDir);
311
+ if (!metadata) return;
312
+ updateMetadata(runDir, {
313
+ childSessionId: ctx.sessionManager.getSessionId(),
314
+ sessionFile: ctx.sessionManager.getSessionFile() ?? metadata.sessionFile,
315
+ state: ctx.isIdle() ? "idle" : "busy",
316
+ suspended: undefined,
317
+ error: undefined,
318
+ });
319
+ syncSessionName();
320
+ if (!timer) {
321
+ timer = setInterval(() => void processInbox(), 250);
322
+ timer.unref();
323
+ }
324
+ void processInbox();
325
+ });
326
+
327
+ pi.on("agent_start", (_event, ctx) => {
328
+ currentContext = ctx;
329
+ updateMetadata(runDir, { state: "busy", hasStarted: true, error: undefined });
330
+ });
331
+
332
+ pi.on("agent_settled", (_event, ctx) => {
333
+ currentContext = ctx;
334
+ if (ctx.isIdle()) updateMetadata(runDir, { state: "idle" });
335
+ });
336
+
337
+ pi.on("session_shutdown", (event) => {
338
+ currentContext = undefined;
339
+ if (timer) {
340
+ clearInterval(timer);
341
+ timer = undefined;
342
+ }
343
+ if (event.reason === "quit") updateMetadata(runDir, { state: "exited" });
344
+ });
345
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "pi-subagent-windows",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Windows port of pi-subagent: hidden, persistent Pi subagents without tmux",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "subagent",
9
+ "windows"
10
+ ],
11
+ "type": "module",
12
+ "license": "MIT",
13
+ "author": "Muntasim Ul Haque",
14
+ "homepage": "https://github.com/muntasimulhaque/pi-subagent-windows#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/muntasimulhaque/pi-subagent-windows.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/muntasimulhaque/pi-subagent-windows/issues"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "index.ts",
25
+ "shared.ts",
26
+ "subagent.ts",
27
+ "supervisor.ts",
28
+ "skills"
29
+ ],
30
+ "bin": {
31
+ "subagent": "./dist/subagent.js"
32
+ },
33
+ "scripts": {
34
+ "build": "node scripts/build.mjs",
35
+ "prepack": "npm run build"
36
+ },
37
+ "pi": {
38
+ "extensions": [
39
+ "./index.ts"
40
+ ]
41
+ },
42
+ "engines": {
43
+ "node": ">=22.19.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-coding-agent": "*",
47
+ "@earendil-works/pi-tui": "*"
48
+ },
49
+ "devDependencies": {
50
+ "esbuild": "^0.28.2"
51
+ }
52
+ }