pi-subagent-windows 0.1.0 → 0.1.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.
- package/README.md +6 -0
- package/dist/subagent.js +598 -0
- package/dist/supervisor.js +206 -0
- package/index.ts +2 -1
- package/package.json +20 -5
- package/shared.ts +14 -2
- package/subagent.ts +3 -1
- package/bin/subagent.js +0 -16
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ On non-Windows platforms the upstream tmux behavior is kept, so the fork stays m
|
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
26
|
+
```sh
|
|
27
|
+
pi install npm:pi-subagent-windows
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install straight from GitHub:
|
|
31
|
+
|
|
26
32
|
```sh
|
|
27
33
|
pi install git:github.com/muntasimulhaque/pi-subagent-windows
|
|
28
34
|
```
|
package/dist/subagent.js
ADDED
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// subagent.ts
|
|
4
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
6
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
8
|
+
|
|
9
|
+
// shared.ts
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import {
|
|
13
|
+
accessSync,
|
|
14
|
+
constants,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
writeFileSync
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
function findRuntimeFile(name) {
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const candidates = [join(here, "dist", `${name}.js`), join(here, `${name}.ts`), join(here, `${name}.js`)];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (existsSync(candidate)) return candidate;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`Could not locate the ${name} runtime file`);
|
|
33
|
+
}
|
|
34
|
+
function getAgentDir() {
|
|
35
|
+
return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
36
|
+
}
|
|
37
|
+
function getRunsDir() {
|
|
38
|
+
return join(getAgentDir(), "subagents");
|
|
39
|
+
}
|
|
40
|
+
function isWindows() {
|
|
41
|
+
return process.platform === "win32";
|
|
42
|
+
}
|
|
43
|
+
function supervisorPidPath(runDir) {
|
|
44
|
+
return join(runDir, "supervisor.pid");
|
|
45
|
+
}
|
|
46
|
+
function readSupervisorPid(runDir) {
|
|
47
|
+
try {
|
|
48
|
+
const value = Number.parseInt(readFileSync(supervisorPidPath(runDir), "utf8").trim(), 10);
|
|
49
|
+
return Number.isInteger(value) && value > 0 ? value : void 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function processIsAlive(pid) {
|
|
55
|
+
try {
|
|
56
|
+
process.kill(pid, 0);
|
|
57
|
+
return true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return error.code === "EPERM";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isRunAlive(metadata) {
|
|
63
|
+
if (isWindows()) {
|
|
64
|
+
const pid = readSupervisorPid(metadata.runDir);
|
|
65
|
+
return pid !== void 0 && processIsAlive(pid);
|
|
66
|
+
}
|
|
67
|
+
return tmuxSessionExists(metadata.tmuxSession);
|
|
68
|
+
}
|
|
69
|
+
function killRun(metadata) {
|
|
70
|
+
if (isWindows()) {
|
|
71
|
+
const pid = readSupervisorPid(metadata.runDir);
|
|
72
|
+
if (pid !== void 0 && processIsAlive(pid)) {
|
|
73
|
+
spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
spawnSync("tmux", ["kill-session", "-t", metadata.tmuxSession], { stdio: "ignore" });
|
|
78
|
+
}
|
|
79
|
+
function writeInboxMessage(runDir, payload) {
|
|
80
|
+
const queueDir = inboxDir(runDir);
|
|
81
|
+
mkdirSync(queueDir, { recursive: true, mode: 448 });
|
|
82
|
+
const id = `${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
83
|
+
const target = join(queueDir, `${id}.json`);
|
|
84
|
+
const temporary = `${target}.tmp`;
|
|
85
|
+
writeFileSync(temporary, `${JSON.stringify(payload)}
|
|
86
|
+
`, { encoding: "utf8", mode: 384 });
|
|
87
|
+
renameSync(temporary, target);
|
|
88
|
+
}
|
|
89
|
+
function metadataPath(runDir) {
|
|
90
|
+
return join(runDir, "metadata.json");
|
|
91
|
+
}
|
|
92
|
+
function inboxDir(runDir) {
|
|
93
|
+
return join(runDir, "inbox");
|
|
94
|
+
}
|
|
95
|
+
function isValidRunName(value) {
|
|
96
|
+
return typeof value === "string" && value.length > 0 && value.length <= 64 && value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
|
|
97
|
+
}
|
|
98
|
+
function runDisplayName(metadata) {
|
|
99
|
+
return metadata.name ? `${metadata.name} (${metadata.handle})` : metadata.handle;
|
|
100
|
+
}
|
|
101
|
+
function readMetadata(runDir) {
|
|
102
|
+
try {
|
|
103
|
+
const value = JSON.parse(readFileSync(metadataPath(runDir), "utf8"));
|
|
104
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
105
|
+
const metadata = value;
|
|
106
|
+
if (metadata.version !== 1 || typeof metadata.handle !== "string" || metadata.name !== void 0 && !isValidRunName(metadata.name) || typeof metadata.tmuxSession !== "string" || typeof metadata.sessionFile !== "string" || typeof metadata.runDir !== "string") {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
return metadata;
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function writeMetadata(metadata) {
|
|
115
|
+
mkdirSync(dirname(metadataPath(metadata.runDir)), { recursive: true, mode: 448 });
|
|
116
|
+
const target = metadataPath(metadata.runDir);
|
|
117
|
+
const temporary = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
118
|
+
writeFileSync(temporary, `${JSON.stringify(metadata, null, 2)}
|
|
119
|
+
`, { encoding: "utf8", mode: 384 });
|
|
120
|
+
renameSync(temporary, target);
|
|
121
|
+
}
|
|
122
|
+
function updateMetadata(runDir, patch) {
|
|
123
|
+
const current = readMetadata(runDir);
|
|
124
|
+
if (!current) return void 0;
|
|
125
|
+
const next = {
|
|
126
|
+
...current,
|
|
127
|
+
...patch,
|
|
128
|
+
version: 1,
|
|
129
|
+
handle: current.handle,
|
|
130
|
+
runDir: current.runDir,
|
|
131
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
132
|
+
};
|
|
133
|
+
writeMetadata(next);
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
async function waitForRunShutdown(runDir, timeoutMs = 2e3) {
|
|
137
|
+
const deadline = Date.now() + timeoutMs;
|
|
138
|
+
while (Date.now() < deadline) {
|
|
139
|
+
const metadata = readMetadata(runDir);
|
|
140
|
+
if (!metadata || metadata.state === "exited" || !isRunAlive(metadata)) return;
|
|
141
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 50));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function removeRunDir(runDir) {
|
|
145
|
+
rmSync(runDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
|
|
146
|
+
}
|
|
147
|
+
function tmuxSessionExists(session) {
|
|
148
|
+
return spawnSync("tmux", ["has-session", "-t", session], { stdio: "ignore" }).status === 0;
|
|
149
|
+
}
|
|
150
|
+
function launchRun(metadata, initialArgs = []) {
|
|
151
|
+
if (isWindows()) {
|
|
152
|
+
const supervisorPath = findRuntimeFile("supervisor");
|
|
153
|
+
const child = spawn(process.execPath, [supervisorPath, metadata.runDir], {
|
|
154
|
+
cwd: metadata.cwd,
|
|
155
|
+
detached: true,
|
|
156
|
+
stdio: "ignore",
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
env: { ...process.env }
|
|
159
|
+
});
|
|
160
|
+
child.unref();
|
|
161
|
+
if (!child.pid) throw new Error("Failed to start the subagent supervisor");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let launcher = "pi";
|
|
165
|
+
const testLauncher = join(metadata.cwd, "pi-test.sh");
|
|
166
|
+
try {
|
|
167
|
+
accessSync(testLauncher, constants.X_OK);
|
|
168
|
+
launcher = testLauncher;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
const result = spawnSync(
|
|
172
|
+
"tmux",
|
|
173
|
+
[
|
|
174
|
+
"new-session",
|
|
175
|
+
"-d",
|
|
176
|
+
"-s",
|
|
177
|
+
metadata.tmuxSession,
|
|
178
|
+
"-x",
|
|
179
|
+
"120",
|
|
180
|
+
"-y",
|
|
181
|
+
"40",
|
|
182
|
+
"-c",
|
|
183
|
+
metadata.cwd,
|
|
184
|
+
"--",
|
|
185
|
+
"env",
|
|
186
|
+
`PI_SUBAGENT_RUN_DIR=${metadata.runDir}`,
|
|
187
|
+
launcher,
|
|
188
|
+
"--session",
|
|
189
|
+
metadata.sessionFile,
|
|
190
|
+
"--provider",
|
|
191
|
+
metadata.provider,
|
|
192
|
+
"--model",
|
|
193
|
+
metadata.model,
|
|
194
|
+
"--thinking",
|
|
195
|
+
metadata.thinking,
|
|
196
|
+
...metadata.launchArgs ?? [],
|
|
197
|
+
...initialArgs
|
|
198
|
+
],
|
|
199
|
+
{ encoding: "utf8" }
|
|
200
|
+
);
|
|
201
|
+
if (result.status !== 0) throw new Error(result.stderr.trim() || "Failed to create tmux session");
|
|
202
|
+
}
|
|
203
|
+
function effectiveRunState(metadata) {
|
|
204
|
+
if ((metadata.state === "starting" || metadata.state === "busy" || metadata.state === "idle") && !isRunAlive(metadata)) {
|
|
205
|
+
return "exited";
|
|
206
|
+
}
|
|
207
|
+
return metadata.state;
|
|
208
|
+
}
|
|
209
|
+
function listRuns(parentSessionId) {
|
|
210
|
+
const root = getRunsDir();
|
|
211
|
+
if (!existsSync(root)) return [];
|
|
212
|
+
const runs = [];
|
|
213
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
214
|
+
if (!entry.isDirectory()) continue;
|
|
215
|
+
const metadata = readMetadata(join(root, entry.name));
|
|
216
|
+
if (!metadata) continue;
|
|
217
|
+
if (parentSessionId && metadata.parentSessionId !== parentSessionId) continue;
|
|
218
|
+
runs.push(metadata);
|
|
219
|
+
}
|
|
220
|
+
return runs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
221
|
+
}
|
|
222
|
+
function isSessionEntry(value) {
|
|
223
|
+
if (typeof value !== "object" || value === null) return false;
|
|
224
|
+
const entry = value;
|
|
225
|
+
return typeof entry.type === "string" && typeof entry.id === "string" && (entry.parentId === null || typeof entry.parentId === "string");
|
|
226
|
+
}
|
|
227
|
+
function activeBranch(entries) {
|
|
228
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
229
|
+
const branch = [];
|
|
230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
231
|
+
let current = entries.at(-1);
|
|
232
|
+
while (current && !seen.has(current.id)) {
|
|
233
|
+
branch.push(current);
|
|
234
|
+
seen.add(current.id);
|
|
235
|
+
current = current.parentId === null ? void 0 : byId.get(current.parentId);
|
|
236
|
+
}
|
|
237
|
+
return branch.reverse();
|
|
238
|
+
}
|
|
239
|
+
function isAssistantEntry(entry) {
|
|
240
|
+
if (entry.type !== "message" || typeof entry.message !== "object" || entry.message === null) return false;
|
|
241
|
+
return entry.message.role === "assistant";
|
|
242
|
+
}
|
|
243
|
+
function readLatestAssistant(sessionFile) {
|
|
244
|
+
let content;
|
|
245
|
+
try {
|
|
246
|
+
content = readFileSync(sessionFile, "utf8");
|
|
247
|
+
} catch {
|
|
248
|
+
return void 0;
|
|
249
|
+
}
|
|
250
|
+
const entries = [];
|
|
251
|
+
for (const line of content.split("\n")) {
|
|
252
|
+
if (!line.trim()) continue;
|
|
253
|
+
try {
|
|
254
|
+
const value = JSON.parse(line);
|
|
255
|
+
if (isSessionEntry(value)) entries.push(value);
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return activeBranch(entries).findLast(isAssistantEntry)?.message;
|
|
260
|
+
}
|
|
261
|
+
function assistantText(message) {
|
|
262
|
+
if (!Array.isArray(message.content)) return message.errorMessage ?? "(no response text)";
|
|
263
|
+
const parts = [];
|
|
264
|
+
for (const item of message.content) {
|
|
265
|
+
if (typeof item !== "object" || item === null) continue;
|
|
266
|
+
const block = item;
|
|
267
|
+
if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
268
|
+
}
|
|
269
|
+
return parts.join("\n").trim() || message.errorMessage || "(no response text)";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// subagent.ts
|
|
273
|
+
var VALID_THINKING_LEVELS = /* @__PURE__ */ new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
274
|
+
var moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
275
|
+
var packageRoot = existsSync2(join2(moduleDir, "index.ts")) ? moduleDir : dirname2(moduleDir);
|
|
276
|
+
var extensionPath = join2(packageRoot, "index.ts");
|
|
277
|
+
function fail(message) {
|
|
278
|
+
throw new Error(message);
|
|
279
|
+
}
|
|
280
|
+
function usage() {
|
|
281
|
+
fail(`Usage:
|
|
282
|
+
subagent spawn [--name <name>] [--provider <provider>] [--model <model>] [--thinking <level>]
|
|
283
|
+
[--cwd <dir>] [--tools <names>] [--no-extensions] [--no-skills]
|
|
284
|
+
[--no-prompt-templates] [--no-context-files] (--prompt <text> | --file <path>)...
|
|
285
|
+
subagent status <handle>
|
|
286
|
+
subagent rename <handle> <name>
|
|
287
|
+
subagent send <handle> [--follow-up] <message>
|
|
288
|
+
subagent wait <handle> [--timeout <seconds>]
|
|
289
|
+
subagent stop <handle>
|
|
290
|
+
subagent list`);
|
|
291
|
+
}
|
|
292
|
+
function valueAfter(args, index, option) {
|
|
293
|
+
const value = args[index + 1];
|
|
294
|
+
if (!value || value.startsWith("--")) fail(`${option} requires a value`);
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
function normalizeRunName(value) {
|
|
298
|
+
const name = value.trim();
|
|
299
|
+
if (!isValidRunName(name)) fail("Subagent name must be a single line of 1 to 64 characters");
|
|
300
|
+
return name;
|
|
301
|
+
}
|
|
302
|
+
function buildInitialMessage(prompts, files) {
|
|
303
|
+
const parts = [];
|
|
304
|
+
for (const file of files) {
|
|
305
|
+
try {
|
|
306
|
+
const buffer = readFileSync2(file);
|
|
307
|
+
if (buffer.includes(0) || buffer.byteLength > 1e6) {
|
|
308
|
+
parts.push(`A file is available at ${file}. Inspect it with the read tool before proceeding.`);
|
|
309
|
+
} else {
|
|
310
|
+
parts.push(`<file name="${file}">
|
|
311
|
+
${buffer.toString("utf8")}
|
|
312
|
+
</file>`);
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
parts.push(`A file was requested at ${file}, but it could not be read.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (prompts.length > 0) parts.push(`Task:
|
|
319
|
+
${prompts.join("\n\n")}`);
|
|
320
|
+
return parts.join("\n\n");
|
|
321
|
+
}
|
|
322
|
+
function runDirForHandle(handle) {
|
|
323
|
+
if (!/^[a-z0-9]+$/.test(handle)) fail(`Invalid subagent handle: ${handle}`);
|
|
324
|
+
return join2(getRunsDir(), handle);
|
|
325
|
+
}
|
|
326
|
+
function getRun(handle) {
|
|
327
|
+
const metadata = readMetadata(runDirForHandle(handle));
|
|
328
|
+
if (!metadata) fail(`Unknown subagent: ${handle}`);
|
|
329
|
+
return metadata;
|
|
330
|
+
}
|
|
331
|
+
function generateHandle() {
|
|
332
|
+
mkdirSync2(getRunsDir(), { recursive: true, mode: 448 });
|
|
333
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
334
|
+
const handle = randomBytes2(3).toString("hex");
|
|
335
|
+
if (!existsSync2(runDirForHandle(handle))) return handle;
|
|
336
|
+
}
|
|
337
|
+
fail("Could not allocate a unique subagent handle");
|
|
338
|
+
}
|
|
339
|
+
function spawnSubagent(args) {
|
|
340
|
+
if (process.env.PI_SUBAGENT_RUN_DIR) fail("Nested subagents are disabled");
|
|
341
|
+
let name;
|
|
342
|
+
let provider = process.env.PI_PROVIDER;
|
|
343
|
+
let model = process.env.PI_MODEL;
|
|
344
|
+
let thinking = process.env.PI_REASONING_LEVEL || "medium";
|
|
345
|
+
let cwd = process.cwd();
|
|
346
|
+
let tools;
|
|
347
|
+
let noExtensions = false;
|
|
348
|
+
let noSkills = false;
|
|
349
|
+
let noPromptTemplates = false;
|
|
350
|
+
let noContextFiles = false;
|
|
351
|
+
const prompts = [];
|
|
352
|
+
const files = [];
|
|
353
|
+
for (let i = 0; i < args.length; i++) {
|
|
354
|
+
const arg = args[i];
|
|
355
|
+
switch (arg) {
|
|
356
|
+
case "--name":
|
|
357
|
+
name = normalizeRunName(valueAfter(args, i, arg));
|
|
358
|
+
i++;
|
|
359
|
+
break;
|
|
360
|
+
case "--provider":
|
|
361
|
+
provider = valueAfter(args, i, arg);
|
|
362
|
+
i++;
|
|
363
|
+
break;
|
|
364
|
+
case "--model":
|
|
365
|
+
model = valueAfter(args, i, arg);
|
|
366
|
+
i++;
|
|
367
|
+
break;
|
|
368
|
+
case "--thinking":
|
|
369
|
+
thinking = valueAfter(args, i, arg);
|
|
370
|
+
i++;
|
|
371
|
+
break;
|
|
372
|
+
case "--cwd":
|
|
373
|
+
cwd = resolve2(valueAfter(args, i, arg));
|
|
374
|
+
i++;
|
|
375
|
+
break;
|
|
376
|
+
case "--tools":
|
|
377
|
+
tools = valueAfter(args, i, arg);
|
|
378
|
+
i++;
|
|
379
|
+
break;
|
|
380
|
+
case "--no-extensions":
|
|
381
|
+
noExtensions = true;
|
|
382
|
+
break;
|
|
383
|
+
case "--no-skills":
|
|
384
|
+
noSkills = true;
|
|
385
|
+
break;
|
|
386
|
+
case "--no-prompt-templates":
|
|
387
|
+
noPromptTemplates = true;
|
|
388
|
+
break;
|
|
389
|
+
case "--no-context-files":
|
|
390
|
+
noContextFiles = true;
|
|
391
|
+
break;
|
|
392
|
+
case "--prompt":
|
|
393
|
+
prompts.push(valueAfter(args, i, arg));
|
|
394
|
+
i++;
|
|
395
|
+
break;
|
|
396
|
+
case "--file": {
|
|
397
|
+
const file = resolve2(valueAfter(args, i, arg));
|
|
398
|
+
if (!existsSync2(file)) fail(`File not found: ${file}`);
|
|
399
|
+
files.push(file);
|
|
400
|
+
i++;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
fail(`Unknown spawn argument: ${arg}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!provider) fail("No provider specified and PI_PROVIDER is not set");
|
|
408
|
+
if (!model) fail("No model specified and PI_MODEL is not set");
|
|
409
|
+
if (!VALID_THINKING_LEVELS.has(thinking)) fail(`Invalid thinking level: ${thinking}`);
|
|
410
|
+
if (tools !== void 0 && !tools.split(",").some((name2) => name2.trim()))
|
|
411
|
+
fail("--tools requires at least one tool name");
|
|
412
|
+
if (prompts.length === 0 && files.length === 0) fail("spawn requires at least one --prompt or --file");
|
|
413
|
+
if (!existsSync2(cwd)) fail(`Working directory not found: ${cwd}`);
|
|
414
|
+
const handle = generateHandle();
|
|
415
|
+
const runDir = runDirForHandle(handle);
|
|
416
|
+
const sessionFile = join2(runDir, "session.jsonl");
|
|
417
|
+
const tmuxSession = `pi-subagent-${handle}`;
|
|
418
|
+
mkdirSync2(inboxDir(runDir), { recursive: true, mode: 448 });
|
|
419
|
+
writeFileSync2(sessionFile, "", { mode: 384 });
|
|
420
|
+
const launchArgs = [];
|
|
421
|
+
if (tools) launchArgs.push("--tools", tools);
|
|
422
|
+
if (noExtensions) launchArgs.push("--no-extensions", "--extension", extensionPath);
|
|
423
|
+
if (noSkills) launchArgs.push("--no-skills");
|
|
424
|
+
if (noPromptTemplates) launchArgs.push("--no-prompt-templates");
|
|
425
|
+
if (noContextFiles) launchArgs.push("--no-context-files");
|
|
426
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
427
|
+
const metadata = {
|
|
428
|
+
version: 1,
|
|
429
|
+
handle,
|
|
430
|
+
name,
|
|
431
|
+
parentSessionId: process.env.PI_SESSION_ID || void 0,
|
|
432
|
+
parentSessionFile: process.env.PI_SESSION_FILE || void 0,
|
|
433
|
+
tmuxSession,
|
|
434
|
+
runDir,
|
|
435
|
+
sessionFile,
|
|
436
|
+
cwd,
|
|
437
|
+
provider,
|
|
438
|
+
model,
|
|
439
|
+
thinking,
|
|
440
|
+
launchArgs,
|
|
441
|
+
state: "starting",
|
|
442
|
+
hasStarted: false,
|
|
443
|
+
createdAt: now,
|
|
444
|
+
updatedAt: now
|
|
445
|
+
};
|
|
446
|
+
writeMetadata(metadata);
|
|
447
|
+
try {
|
|
448
|
+
if (process.platform === "win32") {
|
|
449
|
+
writeInboxMessage(runDir, { message: buildInitialMessage(prompts, files), delivery: "auto" });
|
|
450
|
+
launchRun(metadata);
|
|
451
|
+
} else {
|
|
452
|
+
const initialArgs = files.map((file) => `@${file}`);
|
|
453
|
+
if (prompts.length > 0) initialArgs.push(`Task:
|
|
454
|
+
${prompts.join("\n\n")}`);
|
|
455
|
+
launchRun(metadata, initialArgs);
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
removeRunDir(runDir);
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
const attachLine = process.platform === "win32" ? `Session: ${sessionFile}
|
|
462
|
+
` : `Attach: tmux attach -t ${tmuxSession}
|
|
463
|
+
`;
|
|
464
|
+
process.stdout.write(`Spawned ${runDisplayName(metadata)}
|
|
465
|
+
State: busy
|
|
466
|
+
${attachLine}`);
|
|
467
|
+
}
|
|
468
|
+
function statusSubagent(args) {
|
|
469
|
+
if (args.length !== 1) usage();
|
|
470
|
+
const metadata = getRun(args[0]);
|
|
471
|
+
const attachLine = process.platform === "win32" ? `Session: ${metadata.sessionFile}
|
|
472
|
+
` : `Attach: tmux attach -t ${metadata.tmuxSession}
|
|
473
|
+
`;
|
|
474
|
+
process.stdout.write(
|
|
475
|
+
`${runDisplayName(metadata)}: ${effectiveRunState(metadata)} (${metadata.provider}/${metadata.model}, ${metadata.thinking})
|
|
476
|
+
${attachLine}`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
function renameSubagent(args) {
|
|
480
|
+
if (args.length !== 2) usage();
|
|
481
|
+
const metadata = getRun(args[0]);
|
|
482
|
+
const name = normalizeRunName(args[1]);
|
|
483
|
+
const updated = updateMetadata(metadata.runDir, { name });
|
|
484
|
+
if (!updated) fail(`Could not rename subagent: ${metadata.handle}`);
|
|
485
|
+
process.stdout.write(`Renamed ${runDisplayName(updated)}
|
|
486
|
+
`);
|
|
487
|
+
}
|
|
488
|
+
function sendSubagent(args) {
|
|
489
|
+
const handle = args.shift();
|
|
490
|
+
if (!handle) usage();
|
|
491
|
+
let followUp = false;
|
|
492
|
+
const messageParts = [];
|
|
493
|
+
for (const arg of args) {
|
|
494
|
+
if (arg === "--follow-up") followUp = true;
|
|
495
|
+
else if (arg.startsWith("--")) fail(`Unknown send argument: ${arg}`);
|
|
496
|
+
else messageParts.push(arg);
|
|
497
|
+
}
|
|
498
|
+
const message = messageParts.join(" ").trim();
|
|
499
|
+
if (!message) fail("send requires a message");
|
|
500
|
+
const metadata = getRun(handle);
|
|
501
|
+
if (!isRunAlive(metadata)) fail(`${handle} is not running`);
|
|
502
|
+
const payload = { message, delivery: followUp ? "followUp" : "auto" };
|
|
503
|
+
writeInboxMessage(metadata.runDir, payload);
|
|
504
|
+
const state = effectiveRunState(metadata);
|
|
505
|
+
const verb = followUp && state === "busy" ? "Queued follow-up for" : state === "idle" || state === "starting" ? "Prompted" : "Steered";
|
|
506
|
+
process.stdout.write(`${verb} ${runDisplayName(metadata)}
|
|
507
|
+
`);
|
|
508
|
+
}
|
|
509
|
+
function parseTimeout(args) {
|
|
510
|
+
if (args.length === 0) return 1800;
|
|
511
|
+
if (args.length !== 2 || args[0] !== "--timeout") usage();
|
|
512
|
+
const timeout = Number(args[1]);
|
|
513
|
+
if (!Number.isInteger(timeout) || timeout <= 0) fail("--timeout must be a positive integer");
|
|
514
|
+
return timeout;
|
|
515
|
+
}
|
|
516
|
+
async function waitSubagent(args) {
|
|
517
|
+
const handle = args.shift();
|
|
518
|
+
if (!handle) usage();
|
|
519
|
+
const timeoutSeconds = parseTimeout(args);
|
|
520
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
521
|
+
while (Date.now() < deadline) {
|
|
522
|
+
const metadata = getRun(handle);
|
|
523
|
+
const pending = existsSync2(inboxDir(metadata.runDir)) ? readdirSync2(inboxDir(metadata.runDir)).some((file) => file.endsWith(".json")) : false;
|
|
524
|
+
const state = effectiveRunState(metadata);
|
|
525
|
+
if (state === "error") fail(metadata.error || `${handle} failed`);
|
|
526
|
+
if (state === "exited") fail(`${handle} exited before finishing`);
|
|
527
|
+
if (metadata.hasStarted && state === "idle" && !pending) {
|
|
528
|
+
const message = readLatestAssistant(metadata.sessionFile);
|
|
529
|
+
if (!message) fail(`${handle} finished without an assistant response`);
|
|
530
|
+
process.stdout.write(`${handle} finished
|
|
531
|
+
|
|
532
|
+
${assistantText(message)}
|
|
533
|
+
`);
|
|
534
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") process.exitCode = 1;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 500));
|
|
538
|
+
}
|
|
539
|
+
fail(`Timed out after ${timeoutSeconds}s waiting for ${handle}`);
|
|
540
|
+
}
|
|
541
|
+
async function stopSubagent(args) {
|
|
542
|
+
if (args.length !== 1) usage();
|
|
543
|
+
const metadata = getRun(args[0]);
|
|
544
|
+
const wasRunning = isRunAlive(metadata);
|
|
545
|
+
killRun(metadata);
|
|
546
|
+
if (wasRunning) await waitForRunShutdown(metadata.runDir);
|
|
547
|
+
removeRunDir(metadata.runDir);
|
|
548
|
+
process.stdout.write(`Stopped ${runDisplayName(metadata)}
|
|
549
|
+
`);
|
|
550
|
+
}
|
|
551
|
+
function listSubagents(args) {
|
|
552
|
+
if (args.length !== 0) usage();
|
|
553
|
+
const runs = listRuns(process.env.PI_SESSION_ID || void 0);
|
|
554
|
+
if (runs.length === 0) {
|
|
555
|
+
process.stdout.write("No subagents\n");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
for (const metadata of runs) {
|
|
559
|
+
process.stdout.write(
|
|
560
|
+
`${runDisplayName(metadata)} ${effectiveRunState(metadata).padEnd(8)} ${metadata.provider}/${metadata.model} ${metadata.thinking}
|
|
561
|
+
`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
async function main() {
|
|
566
|
+
const args = process.argv.slice(2);
|
|
567
|
+
const command = args.shift();
|
|
568
|
+
switch (command) {
|
|
569
|
+
case "spawn":
|
|
570
|
+
spawnSubagent(args);
|
|
571
|
+
break;
|
|
572
|
+
case "status":
|
|
573
|
+
statusSubagent(args);
|
|
574
|
+
break;
|
|
575
|
+
case "rename":
|
|
576
|
+
renameSubagent(args);
|
|
577
|
+
break;
|
|
578
|
+
case "send":
|
|
579
|
+
sendSubagent(args);
|
|
580
|
+
break;
|
|
581
|
+
case "wait":
|
|
582
|
+
await waitSubagent(args);
|
|
583
|
+
break;
|
|
584
|
+
case "stop":
|
|
585
|
+
await stopSubagent(args);
|
|
586
|
+
break;
|
|
587
|
+
case "list":
|
|
588
|
+
listSubagents(args);
|
|
589
|
+
break;
|
|
590
|
+
default:
|
|
591
|
+
usage();
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
main().catch((error) => {
|
|
595
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
596
|
+
`);
|
|
597
|
+
process.exitCode = 1;
|
|
598
|
+
});
|
|
@@ -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
CHANGED
|
@@ -6,6 +6,7 @@ import { DynamicBorder, type ExtensionAPI, type ExtensionContext, keyHint } from
|
|
|
6
6
|
import { Container, type SelectItem, SelectList, Text, type TUI } from "@earendil-works/pi-tui";
|
|
7
7
|
import {
|
|
8
8
|
effectiveRunState,
|
|
9
|
+
findRuntimeFile,
|
|
9
10
|
getAgentDir,
|
|
10
11
|
inboxDir,
|
|
11
12
|
isRunAlive,
|
|
@@ -33,7 +34,7 @@ function ensureCliShim(): void {
|
|
|
33
34
|
if (process.platform !== "win32") return;
|
|
34
35
|
try {
|
|
35
36
|
const binDir = join(getAgentDir(), "bin");
|
|
36
|
-
const cliPath =
|
|
37
|
+
const cliPath = findRuntimeFile("subagent");
|
|
37
38
|
const cliPathUnix = cliPath.replace(/\\/g, "/");
|
|
38
39
|
|
|
39
40
|
const cmdMarker = "rem pi-subagent-windows shim";
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagent-windows",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Windows port of pi-subagent: hidden, persistent Pi subagents without tmux",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"subagent",
|
|
9
|
+
"windows"
|
|
10
|
+
],
|
|
6
11
|
"type": "module",
|
|
7
12
|
"license": "MIT",
|
|
8
13
|
"author": "Muntasim Ul Haque",
|
|
@@ -15,7 +20,7 @@
|
|
|
15
20
|
"url": "https://github.com/muntasimulhaque/pi-subagent-windows/issues"
|
|
16
21
|
},
|
|
17
22
|
"files": [
|
|
18
|
-
"
|
|
23
|
+
"dist",
|
|
19
24
|
"index.ts",
|
|
20
25
|
"shared.ts",
|
|
21
26
|
"subagent.ts",
|
|
@@ -23,10 +28,17 @@
|
|
|
23
28
|
"skills"
|
|
24
29
|
],
|
|
25
30
|
"bin": {
|
|
26
|
-
"subagent": "
|
|
31
|
+
"subagent": "dist/subagent.js"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "node scripts/build.mjs",
|
|
35
|
+
"smoke": "node scripts/smoke.mjs",
|
|
36
|
+
"prepare": "npm run build"
|
|
27
37
|
},
|
|
28
38
|
"pi": {
|
|
29
|
-
"extensions": [
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./index.ts"
|
|
41
|
+
]
|
|
30
42
|
},
|
|
31
43
|
"engines": {
|
|
32
44
|
"node": ">=22.19.0"
|
|
@@ -34,5 +46,8 @@
|
|
|
34
46
|
"peerDependencies": {
|
|
35
47
|
"@earendil-works/pi-coding-agent": "*",
|
|
36
48
|
"@earendil-works/pi-tui": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"esbuild": "^0.28.2"
|
|
37
52
|
}
|
|
38
53
|
}
|
package/shared.ts
CHANGED
|
@@ -15,7 +15,19 @@ import { homedir } from "node:os";
|
|
|
15
15
|
import { dirname, join, resolve } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Locate a runtime entry point, preferring the compiled `dist/` output.
|
|
20
|
+
* Compiled files are required when the package is installed under `node_modules`,
|
|
21
|
+
* where Node refuses to strip TypeScript types.
|
|
22
|
+
*/
|
|
23
|
+
export function findRuntimeFile(name: string): string {
|
|
24
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const candidates = [join(here, "dist", `${name}.js`), join(here, `${name}.ts`), join(here, `${name}.js`)];
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
if (existsSync(candidate)) return candidate;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Could not locate the ${name} runtime file`);
|
|
30
|
+
}
|
|
19
31
|
|
|
20
32
|
export type RunState = "starting" | "busy" | "idle" | "exited" | "error";
|
|
21
33
|
|
|
@@ -246,7 +258,7 @@ export function tmuxSessionExists(session: string): boolean {
|
|
|
246
258
|
*/
|
|
247
259
|
export function launchRun(metadata: RunMetadata, initialArgs: string[] = []): void {
|
|
248
260
|
if (isWindows()) {
|
|
249
|
-
const supervisorPath =
|
|
261
|
+
const supervisorPath = findRuntimeFile("supervisor");
|
|
250
262
|
const child = spawn(process.execPath, [supervisorPath, metadata.runDir], {
|
|
251
263
|
cwd: metadata.cwd,
|
|
252
264
|
detached: true,
|
package/subagent.ts
CHANGED
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
} from "./shared.ts";
|
|
28
28
|
|
|
29
29
|
const VALID_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
30
|
-
const
|
|
30
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const packageRoot = existsSync(join(moduleDir, "index.ts")) ? moduleDir : dirname(moduleDir);
|
|
32
|
+
const extensionPath = join(packageRoot, "index.ts");
|
|
31
33
|
|
|
32
34
|
function fail(message: string): never {
|
|
33
35
|
throw new Error(message);
|
package/bin/subagent.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* npm/npx bin entry. Node cannot link a `.ts` file as a global command, so this
|
|
4
|
-
* thin wrapper forwards to the real CLI, which runs under Node's type stripping.
|
|
5
|
-
*/
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
-
|
|
10
|
-
const entry = join(dirname(fileURLToPath(import.meta.url)), "..", "subagent.ts");
|
|
11
|
-
const result = spawnSync(process.execPath, [entry, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
12
|
-
if (result.error) {
|
|
13
|
-
process.stderr.write(`${result.error.message}\n`);
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
process.exit(result.status ?? 1);
|