@jyork0828/pi-pilot 0.0.7 → 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/dist/index.js +614 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/public/assets/ArtifactsPanel-54DnCPD8.js +1 -0
- package/public/assets/FilesPanel-CdZRy4b8.js +56 -0
- package/public/assets/ResizablePanelShell-Cbkq9Bs-.js +1 -0
- package/public/assets/ResourcesPanel-D-VhpjYO.js +24 -0
- package/public/assets/SessionTree-C9twS6vi.js +16 -0
- package/public/assets/{SettingsPage-cULKjgtu.js → SettingsPage-D84YbQn7.js} +2 -2
- package/public/assets/TerminalPanel-9CEnUXvW.css +32 -0
- package/public/assets/TerminalPanel-DgOiuHnT.js +55 -0
- package/public/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- package/public/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- package/public/assets/index-B4g3DrFC.css +1 -0
- package/public/assets/index-DfuMmOel.js +248 -0
- package/public/index.html +2 -2
- package/public/assets/ArtifactsPanel-DUiRwx7S.js +0 -1
- package/public/assets/ResourcesPanel-Cn_gw159.js +0 -19
- package/public/assets/SessionTree-CBIw_kzf.js +0 -16
- package/public/assets/github-dark-DHJKELXO.js +0 -1
- package/public/assets/github-light-DAi9KRSo.js +0 -1
- package/public/assets/index-CX2ohSDO.js +0 -238
- package/public/assets/index-CyoTMDCN.css +0 -1
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { existsSync as existsSync2 } from "fs";
|
|
5
|
-
import { readFile as
|
|
6
|
-
import { dirname as dirname6, extname, join as
|
|
5
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
6
|
+
import { dirname as dirname6, extname, join as join17, resolve as resolve8, sep as sep3 } from "path";
|
|
7
7
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
8
|
import { serve } from "@hono/node-server";
|
|
9
9
|
import { Hono as Hono6 } from "hono";
|
|
@@ -41,20 +41,31 @@ function configureHttpProxy() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/api/workspaces.ts
|
|
44
|
-
import { readFile as
|
|
45
|
-
import { basename as basename2, isAbsolute as isAbsolute3, resolve as
|
|
44
|
+
import { readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
45
|
+
import { basename as basename2, isAbsolute as isAbsolute3, resolve as resolve6 } from "path";
|
|
46
46
|
import { Hono as Hono2 } from "hono";
|
|
47
47
|
|
|
48
48
|
// src/storage/resource-writer.ts
|
|
49
|
+
import { execFile } from "child_process";
|
|
50
|
+
import { createWriteStream } from "fs";
|
|
49
51
|
import {
|
|
52
|
+
cp,
|
|
50
53
|
mkdir,
|
|
54
|
+
mkdtemp,
|
|
55
|
+
readdir,
|
|
51
56
|
readFile,
|
|
52
57
|
rm,
|
|
53
58
|
stat,
|
|
54
59
|
unlink,
|
|
55
60
|
writeFile
|
|
56
61
|
} from "fs/promises";
|
|
62
|
+
import { tmpdir } from "os";
|
|
57
63
|
import { basename, dirname, isAbsolute, join as join2, resolve, sep } from "path";
|
|
64
|
+
import { Readable, Transform } from "stream";
|
|
65
|
+
import { pipeline } from "stream/promises";
|
|
66
|
+
import { promisify } from "util";
|
|
67
|
+
import { createGunzip } from "zlib";
|
|
68
|
+
var exec = promisify(execFile);
|
|
58
69
|
var SKILL_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
59
70
|
var PROMPT_NAME_RE = /^[a-z0-9_](?:[a-z0-9_-]{0,62}[a-z0-9_])?$/;
|
|
60
71
|
function ensureSkillName(name) {
|
|
@@ -231,6 +242,156 @@ async function deleteSkill(filePath, roots) {
|
|
|
231
242
|
assertUnder(dir, [roots.userSkills, roots.projectSkills]);
|
|
232
243
|
await rm(dir, { recursive: true, force: true });
|
|
233
244
|
}
|
|
245
|
+
var MAX_UNCOMPRESSED_BYTES = 200 * 1024 * 1024;
|
|
246
|
+
var MAX_ENTRIES = 5e3;
|
|
247
|
+
async function installSkillFromZip(opts) {
|
|
248
|
+
const base = opts.scope === "user" ? opts.roots.userSkills : opts.roots.projectSkills;
|
|
249
|
+
const maxBytes = opts.limits?.maxUncompressedBytes ?? MAX_UNCOMPRESSED_BYTES;
|
|
250
|
+
const maxEntries = opts.limits?.maxEntries ?? MAX_ENTRIES;
|
|
251
|
+
const work = await mkdtemp(join2(tmpdir(), "pi-pilot-skill-"));
|
|
252
|
+
try {
|
|
253
|
+
const extractDir = join2(work, "x");
|
|
254
|
+
await mkdir(extractDir, { recursive: true });
|
|
255
|
+
await extractArchive(opts.zip, work, extractDir, { maxBytes, maxEntries });
|
|
256
|
+
const srcDir = await locateSkillDir(extractDir);
|
|
257
|
+
await assertNoSymlinks(srcDir);
|
|
258
|
+
const name = await resolveInstalledSkillName(srcDir, extractDir);
|
|
259
|
+
const dest = join2(base, name);
|
|
260
|
+
assertUnder(dest, [opts.roots.userSkills, opts.roots.projectSkills]);
|
|
261
|
+
if (await exists(dest)) {
|
|
262
|
+
if (!opts.overwrite) {
|
|
263
|
+
throw new HttpError(409, `a skill named "${name}" already exists \u2014 delete it first or overwrite`);
|
|
264
|
+
}
|
|
265
|
+
await rm(dest, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
await mkdir(base, { recursive: true });
|
|
268
|
+
await cp(srcDir, dest, {
|
|
269
|
+
recursive: true,
|
|
270
|
+
// Strip macOS archive cruft so the installed dir stays clean. (The
|
|
271
|
+
// tree is already symlink-free — see assertNoSymlinks above.)
|
|
272
|
+
filter: (src) => {
|
|
273
|
+
const b = basename(src);
|
|
274
|
+
return b !== "__MACOSX" && b !== ".DS_Store";
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return { name, filePath: join2(dest, "SKILL.md") };
|
|
278
|
+
} finally {
|
|
279
|
+
await rm(work, { recursive: true, force: true });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function extractArchive(zip, work, extractDir, caps) {
|
|
283
|
+
const isZip = zip.length >= 4 && zip[0] === 80 && zip[1] === 75;
|
|
284
|
+
const isGzip = zip.length >= 3 && zip[0] === 31 && zip[1] === 139 && zip[2] === 8;
|
|
285
|
+
if (!isZip && !isGzip) {
|
|
286
|
+
throw new HttpError(400, "uploaded file is not a .zip or .tar.gz archive");
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
if (isZip) {
|
|
290
|
+
const zipPath = join2(work, "skill.zip");
|
|
291
|
+
await writeFile(zipPath, zip);
|
|
292
|
+
const { stdout } = await exec("unzip", ["-Z", "-t", zipPath]);
|
|
293
|
+
const m = stdout.match(/(\d+)\s+files?,\s+(\d+)\s+bytes uncompressed/);
|
|
294
|
+
if (!m) throw new HttpError(400, "could not read the archive's contents");
|
|
295
|
+
assertEntryCount(Number(m[1]), caps.maxEntries);
|
|
296
|
+
assertUncompressedSize(Number(m[2]), caps.maxBytes);
|
|
297
|
+
await exec("unzip", ["-o", "-q", zipPath, "-d", extractDir]);
|
|
298
|
+
} else {
|
|
299
|
+
const tarPath = join2(work, "skill.tar");
|
|
300
|
+
await gunzipToFileCapped(zip, tarPath, caps.maxBytes);
|
|
301
|
+
const { stdout } = await exec("tar", ["-tf", tarPath], { maxBuffer: 16 * 1024 * 1024 });
|
|
302
|
+
assertEntryCount(stdout.split("\n").filter((l) => l.length > 0).length, caps.maxEntries);
|
|
303
|
+
await exec("tar", ["-xf", tarPath, "-C", extractDir]);
|
|
304
|
+
}
|
|
305
|
+
} catch (err2) {
|
|
306
|
+
if (err2 instanceof HttpError) throw err2;
|
|
307
|
+
const e = err2;
|
|
308
|
+
if (e.code === "ENOENT") {
|
|
309
|
+
throw new HttpError(
|
|
310
|
+
500,
|
|
311
|
+
`\`${e.path ?? "unzip/tar"}\` is not available on the server host \u2014 install it or place the skill folder under your skills directory manually`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
throw new HttpError(400, `could not extract archive: ${e.stderr?.trim() || e.message}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function gunzipToFileCapped(gz, destPath, maxBytes) {
|
|
318
|
+
let total = 0;
|
|
319
|
+
const cap = new Transform({
|
|
320
|
+
transform(chunk, _enc, cb) {
|
|
321
|
+
total += chunk.length;
|
|
322
|
+
if (total > maxBytes) {
|
|
323
|
+
cb(
|
|
324
|
+
new HttpError(
|
|
325
|
+
400,
|
|
326
|
+
`archive expands beyond the ${Math.round(maxBytes / 1024 / 1024)} MB uncompressed cap`
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
cb(null, chunk);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
await pipeline(Readable.from([gz]), createGunzip(), cap, createWriteStream(destPath));
|
|
335
|
+
}
|
|
336
|
+
function assertEntryCount(entries, max) {
|
|
337
|
+
if (entries > max) {
|
|
338
|
+
throw new HttpError(400, `archive has too many files (${entries} > ${max})`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function assertUncompressedSize(uncompressed, maxBytes) {
|
|
342
|
+
if (uncompressed > maxBytes) {
|
|
343
|
+
throw new HttpError(
|
|
344
|
+
400,
|
|
345
|
+
`archive expands to ${Math.round(uncompressed / 1024 / 1024)} MB uncompressed (max ${Math.round(maxBytes / 1024 / 1024)} MB)`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function locateSkillDir(extractDir) {
|
|
350
|
+
if (await exists(join2(extractDir, "SKILL.md"))) return extractDir;
|
|
351
|
+
const dirs = (await readdir(extractDir, { withFileTypes: true })).filter(
|
|
352
|
+
(e) => e.isDirectory() && e.name !== "__MACOSX"
|
|
353
|
+
);
|
|
354
|
+
const withSkill = [];
|
|
355
|
+
for (const e of dirs) {
|
|
356
|
+
const dir = join2(extractDir, e.name);
|
|
357
|
+
if (await exists(join2(dir, "SKILL.md"))) withSkill.push(dir);
|
|
358
|
+
}
|
|
359
|
+
if (withSkill.length > 1) {
|
|
360
|
+
throw new HttpError(400, "archive contains multiple skill folders \u2014 install one skill per .zip");
|
|
361
|
+
}
|
|
362
|
+
if (withSkill.length === 1) return withSkill[0];
|
|
363
|
+
throw new HttpError(400, "archive has no SKILL.md (at the root or one level down)");
|
|
364
|
+
}
|
|
365
|
+
async function assertNoSymlinks(dir) {
|
|
366
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
367
|
+
for (const e of entries) {
|
|
368
|
+
if (e.isSymbolicLink()) {
|
|
369
|
+
throw new HttpError(
|
|
370
|
+
400,
|
|
371
|
+
"archive contains a symbolic link, which is not allowed in a skill package"
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (e.isDirectory()) await assertNoSymlinks(join2(dir, e.name));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function resolveInstalledSkillName(srcDir, extractDir) {
|
|
378
|
+
const text = await readFile(join2(srcDir, "SKILL.md"), "utf8");
|
|
379
|
+
const fmName = parseFile(text).frontmatter.name;
|
|
380
|
+
if (typeof fmName === "string" && fmName.trim()) {
|
|
381
|
+
const n2 = fmName.trim();
|
|
382
|
+
ensureSkillName(n2);
|
|
383
|
+
return n2;
|
|
384
|
+
}
|
|
385
|
+
if (resolve(srcDir) === resolve(extractDir)) {
|
|
386
|
+
throw new HttpError(
|
|
387
|
+
400,
|
|
388
|
+
"SKILL.md has no `name:` field and the archive isn't wrapped in a folder \u2014 add a `name:` to the skill's frontmatter"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const n = basename(srcDir);
|
|
392
|
+
ensureSkillName(n);
|
|
393
|
+
return n;
|
|
394
|
+
}
|
|
234
395
|
async function createPrompt(opts) {
|
|
235
396
|
const file = promptFileFor(opts.scope, opts.name, opts.roots);
|
|
236
397
|
assertUnder(file, [opts.roots.userPrompts, opts.roots.projectPrompts]);
|
|
@@ -477,9 +638,9 @@ async function reorderWorkspaces(ids) {
|
|
|
477
638
|
}
|
|
478
639
|
|
|
479
640
|
// src/storage/workspace-stats.ts
|
|
480
|
-
import { execFile } from "child_process";
|
|
481
|
-
import { promisify } from "util";
|
|
482
|
-
var
|
|
641
|
+
import { execFile as execFile2 } from "child_process";
|
|
642
|
+
import { promisify as promisify2 } from "util";
|
|
643
|
+
var exec2 = promisify2(execFile2);
|
|
483
644
|
var CACHE_TTL_MS = 3e4;
|
|
484
645
|
var cache2 = /* @__PURE__ */ new Map();
|
|
485
646
|
var inflight = /* @__PURE__ */ new Map();
|
|
@@ -555,7 +716,7 @@ async function probeStats(path) {
|
|
|
555
716
|
return { gitBranch, fileCount };
|
|
556
717
|
}
|
|
557
718
|
async function runGit(cwd, args) {
|
|
558
|
-
const { stdout } = await
|
|
719
|
+
const { stdout } = await exec2("git", args, {
|
|
559
720
|
cwd,
|
|
560
721
|
timeout: 2e3,
|
|
561
722
|
maxBuffer: 5 * 1024 * 1024,
|
|
@@ -1007,7 +1168,7 @@ function waitForAnswer({
|
|
|
1007
1168
|
sessionFile,
|
|
1008
1169
|
signal
|
|
1009
1170
|
}) {
|
|
1010
|
-
return new Promise((
|
|
1171
|
+
return new Promise((resolve9, reject) => {
|
|
1011
1172
|
let settled = false;
|
|
1012
1173
|
let timeoutHandle;
|
|
1013
1174
|
const cleanup = () => {
|
|
@@ -1019,7 +1180,7 @@ function waitForAnswer({
|
|
|
1019
1180
|
if (settled) return;
|
|
1020
1181
|
settled = true;
|
|
1021
1182
|
cleanup();
|
|
1022
|
-
|
|
1183
|
+
resolve9(a);
|
|
1023
1184
|
};
|
|
1024
1185
|
const finishErr = (err2) => {
|
|
1025
1186
|
if (settled) return;
|
|
@@ -1295,13 +1456,13 @@ function toolsField(value) {
|
|
|
1295
1456
|
// src/extensions/subagent/child.ts
|
|
1296
1457
|
import { spawn } from "child_process";
|
|
1297
1458
|
import {
|
|
1298
|
-
createWriteStream,
|
|
1459
|
+
createWriteStream as createWriteStream2,
|
|
1299
1460
|
mkdirSync,
|
|
1300
1461
|
mkdtempSync,
|
|
1301
1462
|
writeFileSync
|
|
1302
1463
|
} from "fs";
|
|
1303
1464
|
import { rm as rm3 } from "fs/promises";
|
|
1304
|
-
import { tmpdir } from "os";
|
|
1465
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1305
1466
|
import { join as join8 } from "path";
|
|
1306
1467
|
|
|
1307
1468
|
// src/extensions/subagent/schema.ts
|
|
@@ -1422,18 +1583,18 @@ function take(sessionKey) {
|
|
|
1422
1583
|
}
|
|
1423
1584
|
function acquireChildSlot(signal, sessionFile) {
|
|
1424
1585
|
const sessionKey = keyOf2(sessionFile);
|
|
1425
|
-
return new Promise((
|
|
1586
|
+
return new Promise((resolve9, reject) => {
|
|
1426
1587
|
if (signal?.aborted) {
|
|
1427
1588
|
reject(new Error("Aborted by user"));
|
|
1428
1589
|
return;
|
|
1429
1590
|
}
|
|
1430
1591
|
if (hasCapacity(sessionKey)) {
|
|
1431
1592
|
take(sessionKey);
|
|
1432
|
-
|
|
1593
|
+
resolve9(makeRelease(sessionKey));
|
|
1433
1594
|
return;
|
|
1434
1595
|
}
|
|
1435
1596
|
const waiter = {
|
|
1436
|
-
grant: () =>
|
|
1597
|
+
grant: () => resolve9(makeRelease(sessionKey)),
|
|
1437
1598
|
sessionKey,
|
|
1438
1599
|
signal,
|
|
1439
1600
|
onAbort: void 0
|
|
@@ -1484,7 +1645,7 @@ function pump() {
|
|
|
1484
1645
|
|
|
1485
1646
|
// src/extensions/subagent/child.ts
|
|
1486
1647
|
var PROMPT_DIR_PREFIX = "pi-pilot-subagent-";
|
|
1487
|
-
var TRANSCRIPTS_DIR = join8(
|
|
1648
|
+
var TRANSCRIPTS_DIR = join8(tmpdir2(), "pi-pilot-subagents", "transcripts");
|
|
1488
1649
|
var ACTIVITY_MAX = 30;
|
|
1489
1650
|
var LABEL_MAX = 160;
|
|
1490
1651
|
var STDERR_TAIL_MAX = 2048;
|
|
@@ -1493,7 +1654,7 @@ var SIGKILL_DELAY_MS = 5e3;
|
|
|
1493
1654
|
async function runChild(opts) {
|
|
1494
1655
|
const startedAt = Date.now();
|
|
1495
1656
|
const cli = opts.cliPath ?? process.env.PI_PILOT_SUBAGENT_CLI ?? resolvePinnedPiCli();
|
|
1496
|
-
const promptDir = mkdtempSync(join8(
|
|
1657
|
+
const promptDir = mkdtempSync(join8(tmpdir2(), PROMPT_DIR_PREFIX));
|
|
1497
1658
|
const promptPath = join8(promptDir, "prompt.md");
|
|
1498
1659
|
writeFileSync(promptPath, opts.appendSystemPrompt, { mode: 384 });
|
|
1499
1660
|
let transcriptPath;
|
|
@@ -1501,7 +1662,7 @@ async function runChild(opts) {
|
|
|
1501
1662
|
try {
|
|
1502
1663
|
mkdirSync(TRANSCRIPTS_DIR, { recursive: true });
|
|
1503
1664
|
transcriptPath = join8(TRANSCRIPTS_DIR, `${sanitizeId(opts.toolCallId)}.ndjson`);
|
|
1504
|
-
tee =
|
|
1665
|
+
tee = createWriteStream2(transcriptPath, { flags: "w" });
|
|
1505
1666
|
tee.on("error", () => {
|
|
1506
1667
|
});
|
|
1507
1668
|
} catch {
|
|
@@ -1649,12 +1810,12 @@ ${process.pid}`);
|
|
|
1649
1810
|
armStallTimer();
|
|
1650
1811
|
stderrAccum = (stderrAccum + chunk.toString("utf8")).slice(-STDERR_TAIL_MAX);
|
|
1651
1812
|
});
|
|
1652
|
-
const exitCode = await new Promise((
|
|
1813
|
+
const exitCode = await new Promise((resolve9) => {
|
|
1653
1814
|
child.on("error", (err2) => {
|
|
1654
1815
|
errorMessage ??= err2 instanceof Error ? err2.message : String(err2);
|
|
1655
|
-
|
|
1816
|
+
resolve9(-1);
|
|
1656
1817
|
});
|
|
1657
|
-
child.on("close", (code) =>
|
|
1818
|
+
child.on("close", (code) => resolve9(code ?? -1));
|
|
1658
1819
|
});
|
|
1659
1820
|
if (buf) handleLine(buf);
|
|
1660
1821
|
clearTimeout(timeoutTimer);
|
|
@@ -2501,9 +2662,9 @@ function reconcileAfterRestart(sessionManager) {
|
|
|
2501
2662
|
}
|
|
2502
2663
|
|
|
2503
2664
|
// src/extensions/subagent/cleanup.ts
|
|
2504
|
-
import { execFile as
|
|
2505
|
-
import { readdir, readFile as readFile6, rm as rm4 } from "fs/promises";
|
|
2506
|
-
import { tmpdir as
|
|
2665
|
+
import { execFile as execFile3 } from "child_process";
|
|
2666
|
+
import { readdir as readdir2, readFile as readFile6, rm as rm4 } from "fs/promises";
|
|
2667
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2507
2668
|
import { join as join11 } from "path";
|
|
2508
2669
|
var CUSTOM_TYPE2 = "subagent-restart-cancelled";
|
|
2509
2670
|
function reconcileAfterRestart2(sessionManager) {
|
|
@@ -2558,10 +2719,10 @@ function reconcileAfterRestart2(sessionManager) {
|
|
|
2558
2719
|
{ ids: danglingIds }
|
|
2559
2720
|
);
|
|
2560
2721
|
}
|
|
2561
|
-
async function sweepOrphanedChildrenOnBoot(rootDir =
|
|
2722
|
+
async function sweepOrphanedChildrenOnBoot(rootDir = tmpdir3()) {
|
|
2562
2723
|
let dirNames;
|
|
2563
2724
|
try {
|
|
2564
|
-
dirNames = (await
|
|
2725
|
+
dirNames = (await readdir2(rootDir)).filter((n) => n.startsWith(PROMPT_DIR_PREFIX));
|
|
2565
2726
|
} catch {
|
|
2566
2727
|
return;
|
|
2567
2728
|
}
|
|
@@ -2598,14 +2759,14 @@ function isLiveNodeProcess(pid) {
|
|
|
2598
2759
|
} catch {
|
|
2599
2760
|
return Promise.resolve(false);
|
|
2600
2761
|
}
|
|
2601
|
-
return new Promise((
|
|
2602
|
-
|
|
2762
|
+
return new Promise((resolve9) => {
|
|
2763
|
+
execFile3("ps", ["-o", "ucomm=", "-p", String(pid)], (err2, stdout) => {
|
|
2603
2764
|
if (err2) {
|
|
2604
|
-
|
|
2765
|
+
resolve9(false);
|
|
2605
2766
|
return;
|
|
2606
2767
|
}
|
|
2607
2768
|
const name = stdout.trim().toLowerCase();
|
|
2608
|
-
|
|
2769
|
+
resolve9(name === "node" || name === "pi");
|
|
2609
2770
|
});
|
|
2610
2771
|
});
|
|
2611
2772
|
}
|
|
@@ -2615,8 +2776,10 @@ function translatePiEvent(ev) {
|
|
|
2615
2776
|
switch (ev.type) {
|
|
2616
2777
|
case "agent_start":
|
|
2617
2778
|
return { kind: "agent_start" };
|
|
2618
|
-
case "agent_end":
|
|
2619
|
-
|
|
2779
|
+
case "agent_end": {
|
|
2780
|
+
const error = ev.willRetry ? void 0 : finalAssistantError(ev.messages);
|
|
2781
|
+
return { kind: "agent_end", willRetry: ev.willRetry, ...error ? { error } : {} };
|
|
2782
|
+
}
|
|
2620
2783
|
case "turn_start":
|
|
2621
2784
|
return { kind: "turn_start" };
|
|
2622
2785
|
case "turn_end":
|
|
@@ -2711,6 +2874,21 @@ function translatePiEvent(ev) {
|
|
|
2711
2874
|
return void 0;
|
|
2712
2875
|
}
|
|
2713
2876
|
}
|
|
2877
|
+
function assistantErrorText(message) {
|
|
2878
|
+
const m = message;
|
|
2879
|
+
if (!m || m.stopReason !== "error") return void 0;
|
|
2880
|
+
const text = typeof m.errorMessage === "string" ? m.errorMessage.trim() : "";
|
|
2881
|
+
return text || "The model stream ended with an error.";
|
|
2882
|
+
}
|
|
2883
|
+
function finalAssistantError(messages) {
|
|
2884
|
+
if (!Array.isArray(messages)) return void 0;
|
|
2885
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
2886
|
+
const m = messages[i];
|
|
2887
|
+
if (!m || m.role !== "assistant") continue;
|
|
2888
|
+
return assistantErrorText(m);
|
|
2889
|
+
}
|
|
2890
|
+
return void 0;
|
|
2891
|
+
}
|
|
2714
2892
|
var warnedUnknownRoles = /* @__PURE__ */ new Set();
|
|
2715
2893
|
function roleOf(message) {
|
|
2716
2894
|
const role = message?.role;
|
|
@@ -2837,6 +3015,97 @@ function extractText(result) {
|
|
|
2837
3015
|
return parts.length === 0 ? void 0 : parts.join("");
|
|
2838
3016
|
}
|
|
2839
3017
|
|
|
3018
|
+
// src/history-builder.ts
|
|
3019
|
+
function isAssistantSupersededByRetry(branch, index) {
|
|
3020
|
+
for (let j = index + 1; j < branch.length; j++) {
|
|
3021
|
+
const e = branch[j];
|
|
3022
|
+
if (e?.type !== "message") continue;
|
|
3023
|
+
return e.message?.role === "assistant";
|
|
3024
|
+
}
|
|
3025
|
+
return false;
|
|
3026
|
+
}
|
|
3027
|
+
function buildHistoryItems(branch) {
|
|
3028
|
+
const items = [];
|
|
3029
|
+
const argsByCallId = /* @__PURE__ */ new Map();
|
|
3030
|
+
for (let i = 0; i < branch.length; i++) {
|
|
3031
|
+
const entry = branch[i];
|
|
3032
|
+
if (!entry || entry.type !== "message") continue;
|
|
3033
|
+
const msg = entry.message;
|
|
3034
|
+
const role = msg.role;
|
|
3035
|
+
if (role === "user") {
|
|
3036
|
+
const text = extractUserText2(msg);
|
|
3037
|
+
if (text) items.push({ kind: "user", text, entryId: entry.id ?? "" });
|
|
3038
|
+
} else if (role === "assistant") {
|
|
3039
|
+
const { text, thinking, toolCalls } = extractAssistantContent(
|
|
3040
|
+
msg
|
|
3041
|
+
);
|
|
3042
|
+
for (const tc of toolCalls) {
|
|
3043
|
+
argsByCallId.set(tc.id, tc.args);
|
|
3044
|
+
}
|
|
3045
|
+
const error = isAssistantSupersededByRetry(branch, i) ? void 0 : assistantErrorText(msg);
|
|
3046
|
+
if (text || thinking || error) {
|
|
3047
|
+
items.push({ kind: "assistant", text, thinking, ...error ? { error } : {} });
|
|
3048
|
+
}
|
|
3049
|
+
} else if (role === "toolResult") {
|
|
3050
|
+
const tr = msg;
|
|
3051
|
+
items.push({
|
|
3052
|
+
kind: "tool",
|
|
3053
|
+
toolCallId: tr.toolCallId,
|
|
3054
|
+
toolName: tr.toolName,
|
|
3055
|
+
args: argsByCallId.get(tr.toolCallId) ?? "",
|
|
3056
|
+
text: extractContentText(tr.content),
|
|
3057
|
+
isError: tr.isError,
|
|
3058
|
+
// Mirror live wire whitelist (bridge.ts): only ship details for
|
|
3059
|
+
// tools whose cards need the structured shape, so the history
|
|
3060
|
+
// payload stays small for bash / edit / read.
|
|
3061
|
+
...shouldForwardDetails(tr.toolName) && tr.details !== void 0 ? { details: tr.details } : {}
|
|
3062
|
+
});
|
|
3063
|
+
} else if (role === "bashExecution") {
|
|
3064
|
+
const be = msg;
|
|
3065
|
+
items.push({
|
|
3066
|
+
kind: "bash",
|
|
3067
|
+
command: be.command,
|
|
3068
|
+
output: be.output,
|
|
3069
|
+
exitCode: be.exitCode
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
return items;
|
|
3074
|
+
}
|
|
3075
|
+
function extractUserText2(msg) {
|
|
3076
|
+
if (typeof msg.content === "string") return msg.content;
|
|
3077
|
+
return extractContentText(msg.content);
|
|
3078
|
+
}
|
|
3079
|
+
function extractAssistantContent(msg) {
|
|
3080
|
+
const textParts = [];
|
|
3081
|
+
const thinkingParts = [];
|
|
3082
|
+
const toolCalls = [];
|
|
3083
|
+
for (const block of msg.content ?? []) {
|
|
3084
|
+
if (!block || typeof block !== "object") continue;
|
|
3085
|
+
const b = block;
|
|
3086
|
+
if (b.type === "text" && typeof b.text === "string") textParts.push(b.text);
|
|
3087
|
+
else if (b.type === "thinking" && typeof b.thinking === "string") thinkingParts.push(b.thinking);
|
|
3088
|
+
else if (b.type === "toolCall" && typeof b.id === "string") {
|
|
3089
|
+
toolCalls.push({
|
|
3090
|
+
id: b.id,
|
|
3091
|
+
args: b.arguments != null ? JSON.stringify(b.arguments) : ""
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return { text: textParts.join(""), thinking: thinkingParts.join(""), toolCalls };
|
|
3096
|
+
}
|
|
3097
|
+
function extractContentText(content) {
|
|
3098
|
+
if (!Array.isArray(content)) return "";
|
|
3099
|
+
const parts = [];
|
|
3100
|
+
for (const block of content) {
|
|
3101
|
+
if (block && typeof block === "object" && block.type === "text") {
|
|
3102
|
+
const text = block.text;
|
|
3103
|
+
if (typeof text === "string") parts.push(text);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return parts.join("");
|
|
3107
|
+
}
|
|
3108
|
+
|
|
2840
3109
|
// src/ws/extension-ui.ts
|
|
2841
3110
|
var ExtensionUIBridge = class {
|
|
2842
3111
|
/** Symmetric with the old bridge so workspace-manager's dispose path
|
|
@@ -3213,48 +3482,7 @@ ${err2.stack}` : "")
|
|
|
3213
3482
|
if (!runtime) return { items: [], isStreaming: false };
|
|
3214
3483
|
const isStreaming = runtime.session.isStreaming ?? false;
|
|
3215
3484
|
const branch = runtime.session.sessionManager.getBranch();
|
|
3216
|
-
|
|
3217
|
-
const argsByCallId = /* @__PURE__ */ new Map();
|
|
3218
|
-
for (const entry of branch) {
|
|
3219
|
-
if (entry.type !== "message") continue;
|
|
3220
|
-
const msg = entry.message;
|
|
3221
|
-
const role = msg.role;
|
|
3222
|
-
if (role === "user") {
|
|
3223
|
-
const text = extractUserText2(msg);
|
|
3224
|
-
if (text) items.push({ kind: "user", text, entryId: entry.id });
|
|
3225
|
-
} else if (role === "assistant") {
|
|
3226
|
-
const { text, thinking, toolCalls } = extractAssistantContent(
|
|
3227
|
-
msg
|
|
3228
|
-
);
|
|
3229
|
-
for (const tc of toolCalls) {
|
|
3230
|
-
argsByCallId.set(tc.id, tc.args);
|
|
3231
|
-
}
|
|
3232
|
-
if (text || thinking) items.push({ kind: "assistant", text, thinking });
|
|
3233
|
-
} else if (role === "toolResult") {
|
|
3234
|
-
const tr = msg;
|
|
3235
|
-
items.push({
|
|
3236
|
-
kind: "tool",
|
|
3237
|
-
toolCallId: tr.toolCallId,
|
|
3238
|
-
toolName: tr.toolName,
|
|
3239
|
-
args: argsByCallId.get(tr.toolCallId) ?? "",
|
|
3240
|
-
text: extractContentText(tr.content),
|
|
3241
|
-
isError: tr.isError,
|
|
3242
|
-
// Mirror live wire whitelist (bridge.ts): only ship details
|
|
3243
|
-
// for tools whose cards need the structured shape, so the
|
|
3244
|
-
// history payload stays small for bash / edit / read.
|
|
3245
|
-
...shouldForwardDetails(tr.toolName) && tr.details !== void 0 ? { details: tr.details } : {}
|
|
3246
|
-
});
|
|
3247
|
-
} else if (role === "bashExecution") {
|
|
3248
|
-
const be = msg;
|
|
3249
|
-
items.push({
|
|
3250
|
-
kind: "bash",
|
|
3251
|
-
command: be.command,
|
|
3252
|
-
output: be.output,
|
|
3253
|
-
exitCode: be.exitCode
|
|
3254
|
-
});
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
return { items, isStreaming };
|
|
3485
|
+
return { items: buildHistoryItems(branch), isStreaming };
|
|
3258
3486
|
}
|
|
3259
3487
|
/**
|
|
3260
3488
|
* Delete a session JSONL file belonging to this workspace.
|
|
@@ -3394,39 +3622,6 @@ function toSessionSummary(info, running2) {
|
|
|
3394
3622
|
...running2 ? { running: true } : {}
|
|
3395
3623
|
};
|
|
3396
3624
|
}
|
|
3397
|
-
function extractUserText2(msg) {
|
|
3398
|
-
if (typeof msg.content === "string") return msg.content;
|
|
3399
|
-
return extractContentText(msg.content);
|
|
3400
|
-
}
|
|
3401
|
-
function extractAssistantContent(msg) {
|
|
3402
|
-
const textParts = [];
|
|
3403
|
-
const thinkingParts = [];
|
|
3404
|
-
const toolCalls = [];
|
|
3405
|
-
for (const block of msg.content ?? []) {
|
|
3406
|
-
if (!block || typeof block !== "object") continue;
|
|
3407
|
-
const b = block;
|
|
3408
|
-
if (b.type === "text" && typeof b.text === "string") textParts.push(b.text);
|
|
3409
|
-
else if (b.type === "thinking" && typeof b.thinking === "string") thinkingParts.push(b.thinking);
|
|
3410
|
-
else if (b.type === "toolCall" && typeof b.id === "string") {
|
|
3411
|
-
toolCalls.push({
|
|
3412
|
-
id: b.id,
|
|
3413
|
-
args: b.arguments != null ? JSON.stringify(b.arguments) : ""
|
|
3414
|
-
});
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
return { text: textParts.join(""), thinking: thinkingParts.join(""), toolCalls };
|
|
3418
|
-
}
|
|
3419
|
-
function extractContentText(content) {
|
|
3420
|
-
if (!Array.isArray(content)) return "";
|
|
3421
|
-
const parts = [];
|
|
3422
|
-
for (const block of content) {
|
|
3423
|
-
if (block && typeof block === "object" && block.type === "text") {
|
|
3424
|
-
const text = block.text;
|
|
3425
|
-
if (typeof text === "string") parts.push(text);
|
|
3426
|
-
}
|
|
3427
|
-
}
|
|
3428
|
-
return parts.join("");
|
|
3429
|
-
}
|
|
3430
3625
|
var workspaceManager = new SessionRuntimeManager();
|
|
3431
3626
|
function broadcastTo(subscribers, msg) {
|
|
3432
3627
|
const wire = JSON.stringify(msg);
|
|
@@ -3630,11 +3825,11 @@ function mountConfigRoutes(app2) {
|
|
|
3630
3825
|
}
|
|
3631
3826
|
|
|
3632
3827
|
// src/api/files.ts
|
|
3633
|
-
import { execFile as
|
|
3634
|
-
import { readdir as
|
|
3635
|
-
import { join as join12, relative, sep as sep2 } from "path";
|
|
3636
|
-
import { promisify as
|
|
3637
|
-
var
|
|
3828
|
+
import { execFile as execFile4 } from "child_process";
|
|
3829
|
+
import { readdir as readdir3, readFile as readFile7, realpath, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
3830
|
+
import { join as join12, relative, resolve as resolve5, sep as sep2 } from "path";
|
|
3831
|
+
import { promisify as promisify3 } from "util";
|
|
3832
|
+
var exec3 = promisify3(execFile4);
|
|
3638
3833
|
var LIST_TTL_MS = 1e4;
|
|
3639
3834
|
var MAX_CACHED_WORKSPACES = 16;
|
|
3640
3835
|
var MAX_FILES_TRACKED = 2e4;
|
|
@@ -3685,7 +3880,7 @@ async function getFileList(workspacePath) {
|
|
|
3685
3880
|
}
|
|
3686
3881
|
async function probeFileList(workspacePath) {
|
|
3687
3882
|
try {
|
|
3688
|
-
const { stdout } = await
|
|
3883
|
+
const { stdout } = await exec3(
|
|
3689
3884
|
"git",
|
|
3690
3885
|
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
3691
3886
|
{
|
|
@@ -3714,7 +3909,7 @@ async function walkDir(root, dir, depth, out) {
|
|
|
3714
3909
|
if (depth > WALK_MAX_DEPTH) return;
|
|
3715
3910
|
let dirents;
|
|
3716
3911
|
try {
|
|
3717
|
-
dirents = await
|
|
3912
|
+
dirents = await readdir3(dir, { withFileTypes: true });
|
|
3718
3913
|
} catch {
|
|
3719
3914
|
return;
|
|
3720
3915
|
}
|
|
@@ -3744,6 +3939,21 @@ async function ensureWorkspaceExists(id) {
|
|
|
3744
3939
|
const ws = await getWorkspace(id);
|
|
3745
3940
|
return ws ? ws.path : null;
|
|
3746
3941
|
}
|
|
3942
|
+
var MAX_EDIT_BYTES = 1024 * 1024;
|
|
3943
|
+
var TREE_MAX_FILES = 5e3;
|
|
3944
|
+
function resolveInWorkspace(workspacePath, relPath) {
|
|
3945
|
+
const root = resolve5(workspacePath);
|
|
3946
|
+
const abs = resolve5(root, relPath);
|
|
3947
|
+
if (abs !== root && !abs.startsWith(root + sep2)) return null;
|
|
3948
|
+
return abs;
|
|
3949
|
+
}
|
|
3950
|
+
async function resolveExistingInWorkspace(workspacePath, relPath) {
|
|
3951
|
+
const abs = resolveInWorkspace(workspacePath, relPath);
|
|
3952
|
+
if (!abs) return null;
|
|
3953
|
+
const [rootReal, targetReal] = await Promise.all([realpath(workspacePath), realpath(abs)]);
|
|
3954
|
+
if (targetReal !== rootReal && !targetReal.startsWith(rootReal + sep2)) return null;
|
|
3955
|
+
return targetReal;
|
|
3956
|
+
}
|
|
3747
3957
|
function mountFilesRoute(app2) {
|
|
3748
3958
|
app2.get("/:id/files/search", async (c) => {
|
|
3749
3959
|
const id = c.req.param("id");
|
|
@@ -3791,12 +4001,81 @@ function mountFilesRoute(app2) {
|
|
|
3791
4001
|
return c.json({ ok: false, error: message }, 500);
|
|
3792
4002
|
}
|
|
3793
4003
|
});
|
|
4004
|
+
app2.get("/:id/files/list", async (c) => {
|
|
4005
|
+
const id = c.req.param("id");
|
|
4006
|
+
const workspacePath = await ensureWorkspaceExists(id);
|
|
4007
|
+
if (!workspacePath) return c.json({ ok: false, error: "not found" }, 404);
|
|
4008
|
+
try {
|
|
4009
|
+
const all = await getFileList(workspacePath);
|
|
4010
|
+
const entries = all.slice(0, TREE_MAX_FILES);
|
|
4011
|
+
const body = {
|
|
4012
|
+
workspacePath,
|
|
4013
|
+
entries,
|
|
4014
|
+
truncated: all.length > TREE_MAX_FILES
|
|
4015
|
+
};
|
|
4016
|
+
return c.json(body);
|
|
4017
|
+
} catch (err2) {
|
|
4018
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4019
|
+
console.error(`[api/files] list for ${id} failed:`, err2);
|
|
4020
|
+
return c.json({ ok: false, error: message }, 500);
|
|
4021
|
+
}
|
|
4022
|
+
});
|
|
4023
|
+
app2.get("/:id/file", async (c) => {
|
|
4024
|
+
const id = c.req.param("id");
|
|
4025
|
+
const workspacePath = await ensureWorkspaceExists(id);
|
|
4026
|
+
if (!workspacePath) return c.json({ ok: false, error: "not found" }, 404);
|
|
4027
|
+
const relPath = c.req.query("path");
|
|
4028
|
+
if (!relPath) return c.json({ ok: false, error: "path query is required" }, 400);
|
|
4029
|
+
try {
|
|
4030
|
+
const abs = await resolveExistingInWorkspace(workspacePath, relPath);
|
|
4031
|
+
if (!abs) return c.json({ ok: false, error: "path escapes workspace" }, 400);
|
|
4032
|
+
const st = await stat2(abs);
|
|
4033
|
+
if (!st.isFile()) return c.json({ ok: false, error: "not a file" }, 400);
|
|
4034
|
+
if (st.size > MAX_EDIT_BYTES) {
|
|
4035
|
+
return c.json({ ok: false, error: "file too large to edit (>1 MB)" }, 400);
|
|
4036
|
+
}
|
|
4037
|
+
const buf = await readFile7(abs);
|
|
4038
|
+
if (buf.includes(0)) return c.json({ ok: false, error: "binary file" }, 400);
|
|
4039
|
+
const body = { relPath, content: buf.toString("utf-8") };
|
|
4040
|
+
return c.json(body);
|
|
4041
|
+
} catch (err2) {
|
|
4042
|
+
const code = err2.code;
|
|
4043
|
+
const msg = code === "ENOENT" ? "not found" : code === "EACCES" ? "permission denied" : "read failed";
|
|
4044
|
+
return c.json({ ok: false, error: msg }, 400);
|
|
4045
|
+
}
|
|
4046
|
+
});
|
|
4047
|
+
app2.put("/:id/file", async (c) => {
|
|
4048
|
+
const id = c.req.param("id");
|
|
4049
|
+
const workspacePath = await ensureWorkspaceExists(id);
|
|
4050
|
+
if (!workspacePath) return c.json({ ok: false, error: "not found" }, 404);
|
|
4051
|
+
const body = await c.req.json().catch(() => null);
|
|
4052
|
+
if (!body || typeof body.path !== "string" || typeof body.content !== "string") {
|
|
4053
|
+
return c.json({ ok: false, error: "path and content are required" }, 400);
|
|
4054
|
+
}
|
|
4055
|
+
if (Buffer.byteLength(body.content, "utf-8") > MAX_EDIT_BYTES) {
|
|
4056
|
+
return c.json({ ok: false, error: "content too large (>1 MB)" }, 400);
|
|
4057
|
+
}
|
|
4058
|
+
try {
|
|
4059
|
+
const abs = await resolveExistingInWorkspace(workspacePath, body.path);
|
|
4060
|
+
if (!abs) return c.json({ ok: false, error: "path escapes workspace" }, 400);
|
|
4061
|
+
const st = await stat2(abs);
|
|
4062
|
+
if (!st.isFile()) return c.json({ ok: false, error: "not a file" }, 400);
|
|
4063
|
+
await writeFile4(abs, body.content, "utf-8");
|
|
4064
|
+
const ok = { ok: true };
|
|
4065
|
+
return c.json(ok);
|
|
4066
|
+
} catch (err2) {
|
|
4067
|
+
const code = err2.code;
|
|
4068
|
+
const msg = code === "ENOENT" ? "not found" : code === "EACCES" ? "permission denied" : "write failed";
|
|
4069
|
+
return c.json({ ok: false, error: msg }, 400);
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
3794
4072
|
}
|
|
3795
4073
|
|
|
3796
4074
|
// src/api/resources.ts
|
|
3797
|
-
import { readdir as
|
|
4075
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
3798
4076
|
import { join as join13 } from "path";
|
|
3799
4077
|
import { getAgentDir as getAgentDir3 } from "@earendil-works/pi-coding-agent";
|
|
4078
|
+
var MAX_SKILL_ZIP_BYTES = 25 * 1024 * 1024;
|
|
3800
4079
|
function toResourceSource(info) {
|
|
3801
4080
|
return {
|
|
3802
4081
|
scope: info.scope,
|
|
@@ -3809,7 +4088,7 @@ async function scanExtensionDirs(workspaceCwd) {
|
|
|
3809
4088
|
const found = [];
|
|
3810
4089
|
for (const dir of dirs) {
|
|
3811
4090
|
try {
|
|
3812
|
-
const entries = await
|
|
4091
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3813
4092
|
for (const entry of entries) {
|
|
3814
4093
|
if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
3815
4094
|
found.push(join13(dir, entry.name));
|
|
@@ -3989,6 +4268,39 @@ function mountResourcesRoute(app2) {
|
|
|
3989
4268
|
return respondError(c, err2);
|
|
3990
4269
|
}
|
|
3991
4270
|
});
|
|
4271
|
+
app2.post("/:id/resources/skills/install", async (c) => {
|
|
4272
|
+
const id = c.req.param("id");
|
|
4273
|
+
const ws = await getWorkspace(id);
|
|
4274
|
+
if (!ws) return c.json({ ok: false, error: "not found" }, 404);
|
|
4275
|
+
const scope = c.req.query("scope");
|
|
4276
|
+
if (!isScope(scope)) {
|
|
4277
|
+
return c.json({ ok: false, error: "scope query must be 'user' or 'project'" }, 400);
|
|
4278
|
+
}
|
|
4279
|
+
const overwrite = c.req.query("overwrite") === "1";
|
|
4280
|
+
const declared = Number(c.req.header("content-length") ?? "0");
|
|
4281
|
+
if (declared > MAX_SKILL_ZIP_BYTES) {
|
|
4282
|
+
return c.json({ ok: false, error: "archive too large (max 25 MB)" }, 400);
|
|
4283
|
+
}
|
|
4284
|
+
let zip;
|
|
4285
|
+
try {
|
|
4286
|
+
zip = Buffer.from(await c.req.arrayBuffer());
|
|
4287
|
+
} catch {
|
|
4288
|
+
return c.json({ ok: false, error: "could not read upload" }, 400);
|
|
4289
|
+
}
|
|
4290
|
+
if (zip.length === 0) return c.json({ ok: false, error: "empty upload" }, 400);
|
|
4291
|
+
if (zip.length > MAX_SKILL_ZIP_BYTES) {
|
|
4292
|
+
return c.json({ ok: false, error: "archive too large (max 25 MB)" }, 400);
|
|
4293
|
+
}
|
|
4294
|
+
try {
|
|
4295
|
+
await workspaceManager.getOrCreate(id);
|
|
4296
|
+
const { roots, workspaceCwd } = await rootsFor(id);
|
|
4297
|
+
await installSkillFromZip({ roots, scope, zip, overwrite });
|
|
4298
|
+
await reload(id);
|
|
4299
|
+
return c.json(await snapshot(id, roots, workspaceCwd));
|
|
4300
|
+
} catch (err2) {
|
|
4301
|
+
return respondError(c, err2);
|
|
4302
|
+
}
|
|
4303
|
+
});
|
|
3992
4304
|
app2.put("/:id/resources/skills", async (c) => {
|
|
3993
4305
|
const id = c.req.param("id");
|
|
3994
4306
|
const ws = await getWorkspace(id);
|
|
@@ -4400,7 +4712,7 @@ workspacesRoute.get("/:id/export", async (c) => {
|
|
|
4400
4712
|
}
|
|
4401
4713
|
const runtime = await workspaceManager.getOrCreate(id, sessionPath || void 0);
|
|
4402
4714
|
const outputPath = await runtime.session.exportToHtml();
|
|
4403
|
-
const html = await
|
|
4715
|
+
const html = await readFile8(outputPath, "utf-8");
|
|
4404
4716
|
const filename = basename2(outputPath);
|
|
4405
4717
|
const body = { html, filename };
|
|
4406
4718
|
return c.json(body);
|
|
@@ -4433,9 +4745,9 @@ workspacesRoute.post("/", async (c) => {
|
|
|
4433
4745
|
if (!isAbsolute3(body.path)) {
|
|
4434
4746
|
return c.json({ ok: false, error: "path must be absolute" }, 400);
|
|
4435
4747
|
}
|
|
4436
|
-
const resolved =
|
|
4748
|
+
const resolved = resolve6(body.path);
|
|
4437
4749
|
try {
|
|
4438
|
-
const st = await
|
|
4750
|
+
const st = await stat3(resolved);
|
|
4439
4751
|
if (!st.isDirectory()) {
|
|
4440
4752
|
return c.json({ ok: false, error: "path is not a directory" }, 400);
|
|
4441
4753
|
}
|
|
@@ -4476,18 +4788,18 @@ mountFilesRoute(workspacesRoute);
|
|
|
4476
4788
|
workspacesRoute.route("/:id/tree", treeRoute);
|
|
4477
4789
|
|
|
4478
4790
|
// src/api/fs.ts
|
|
4479
|
-
import { readdir as
|
|
4791
|
+
import { readdir as readdir5 } from "fs/promises";
|
|
4480
4792
|
import { homedir as homedir3 } from "os";
|
|
4481
|
-
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join14, resolve as
|
|
4793
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join14, resolve as resolve7 } from "path";
|
|
4482
4794
|
import { Hono as Hono3 } from "hono";
|
|
4483
4795
|
var fsRoute = new Hono3();
|
|
4484
4796
|
fsRoute.get("/browse", async (c) => {
|
|
4485
4797
|
const rawPath = c.req.query("path");
|
|
4486
4798
|
const showHidden = c.req.query("showHidden") === "1";
|
|
4487
|
-
const target = rawPath && isAbsolute4(rawPath) ?
|
|
4799
|
+
const target = rawPath && isAbsolute4(rawPath) ? resolve7(rawPath) : homedir3();
|
|
4488
4800
|
let dirents;
|
|
4489
4801
|
try {
|
|
4490
|
-
dirents = await
|
|
4802
|
+
dirents = await readdir5(target, { withFileTypes: true });
|
|
4491
4803
|
} catch (err2) {
|
|
4492
4804
|
const code = err2.code;
|
|
4493
4805
|
const msg = code === "EACCES" ? "permission denied" : code === "ENOENT" ? "not found" : "read failed";
|
|
@@ -4507,7 +4819,7 @@ fsRoute.get("/browse", async (c) => {
|
|
|
4507
4819
|
});
|
|
4508
4820
|
|
|
4509
4821
|
// src/api/model-configs.ts
|
|
4510
|
-
import { readFile as
|
|
4822
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4511
4823
|
import { join as join15 } from "path";
|
|
4512
4824
|
import { Hono as Hono4 } from "hono";
|
|
4513
4825
|
import {
|
|
@@ -4563,7 +4875,7 @@ function modelsPath() {
|
|
|
4563
4875
|
}
|
|
4564
4876
|
async function readModelsJson() {
|
|
4565
4877
|
try {
|
|
4566
|
-
const raw = await
|
|
4878
|
+
const raw = await readFile9(modelsPath(), "utf-8");
|
|
4567
4879
|
return JSON.parse(raw);
|
|
4568
4880
|
} catch (err2) {
|
|
4569
4881
|
if (err2?.code === "ENOENT") {
|
|
@@ -4847,6 +5159,132 @@ function isAllowedWsOrigin(origin) {
|
|
|
4847
5159
|
return allowedWsOrigins.has(origin);
|
|
4848
5160
|
}
|
|
4849
5161
|
|
|
5162
|
+
// src/ws/terminals.ts
|
|
5163
|
+
import { createRequire } from "module";
|
|
5164
|
+
import { accessSync, chmodSync, constants, statSync } from "fs";
|
|
5165
|
+
import { join as join16 } from "path";
|
|
5166
|
+
import { spawn as spawn2 } from "node-pty";
|
|
5167
|
+
var FALLBACK_SHELL = process.platform === "win32" ? "powershell.exe" : "/bin/bash";
|
|
5168
|
+
function send(ws, msg) {
|
|
5169
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
5170
|
+
ws.send(JSON.stringify(msg));
|
|
5171
|
+
}
|
|
5172
|
+
function ptyEnv() {
|
|
5173
|
+
const env = {};
|
|
5174
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
5175
|
+
if (typeof v === "string") env[k] = v;
|
|
5176
|
+
}
|
|
5177
|
+
env.TERM = "xterm-256color";
|
|
5178
|
+
env.COLORTERM = "truecolor";
|
|
5179
|
+
return env;
|
|
5180
|
+
}
|
|
5181
|
+
async function handleTerminalMessage(ws, terminals, msg) {
|
|
5182
|
+
switch (msg.type) {
|
|
5183
|
+
case "terminal_open": {
|
|
5184
|
+
ensureSpawnHelperExecutable();
|
|
5185
|
+
const existing = terminals.get(msg.terminalId);
|
|
5186
|
+
if (existing) {
|
|
5187
|
+
try {
|
|
5188
|
+
existing.kill();
|
|
5189
|
+
} catch {
|
|
5190
|
+
}
|
|
5191
|
+
terminals.delete(msg.terminalId);
|
|
5192
|
+
}
|
|
5193
|
+
const ws_ = await getWorkspace(msg.workspaceId);
|
|
5194
|
+
if (!ws_) {
|
|
5195
|
+
send(ws, { type: "terminal_error", terminalId: msg.terminalId, message: "workspace not found" });
|
|
5196
|
+
return;
|
|
5197
|
+
}
|
|
5198
|
+
const shell = process.env.SHELL || FALLBACK_SHELL;
|
|
5199
|
+
let pty;
|
|
5200
|
+
try {
|
|
5201
|
+
pty = spawn2(shell, [], {
|
|
5202
|
+
name: "xterm-256color",
|
|
5203
|
+
cols: clampDim(msg.cols, 80),
|
|
5204
|
+
rows: clampDim(msg.rows, 24),
|
|
5205
|
+
cwd: ws_.path,
|
|
5206
|
+
env: ptyEnv()
|
|
5207
|
+
});
|
|
5208
|
+
} catch (err2) {
|
|
5209
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
5210
|
+
send(ws, { type: "terminal_error", terminalId: msg.terminalId, message });
|
|
5211
|
+
return;
|
|
5212
|
+
}
|
|
5213
|
+
terminals.set(msg.terminalId, pty);
|
|
5214
|
+
pty.onData((data) => {
|
|
5215
|
+
send(ws, { type: "terminal_output", terminalId: msg.terminalId, data });
|
|
5216
|
+
});
|
|
5217
|
+
pty.onExit(({ exitCode }) => {
|
|
5218
|
+
terminals.delete(msg.terminalId);
|
|
5219
|
+
send(ws, { type: "terminal_exit", terminalId: msg.terminalId, exitCode });
|
|
5220
|
+
});
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
case "terminal_input": {
|
|
5224
|
+
terminals.get(msg.terminalId)?.write(msg.data);
|
|
5225
|
+
return;
|
|
5226
|
+
}
|
|
5227
|
+
case "terminal_resize": {
|
|
5228
|
+
const pty = terminals.get(msg.terminalId);
|
|
5229
|
+
if (pty) {
|
|
5230
|
+
try {
|
|
5231
|
+
pty.resize(clampDim(msg.cols, 80), clampDim(msg.rows, 24));
|
|
5232
|
+
} catch {
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
case "terminal_close": {
|
|
5238
|
+
const pty = terminals.get(msg.terminalId);
|
|
5239
|
+
if (pty) {
|
|
5240
|
+
terminals.delete(msg.terminalId);
|
|
5241
|
+
try {
|
|
5242
|
+
pty.kill();
|
|
5243
|
+
} catch {
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
return;
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
function closeAllTerminals(terminals) {
|
|
5251
|
+
for (const pty of terminals.values()) {
|
|
5252
|
+
try {
|
|
5253
|
+
pty.kill();
|
|
5254
|
+
} catch {
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
terminals.clear();
|
|
5258
|
+
}
|
|
5259
|
+
function clampDim(n, fallback) {
|
|
5260
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
5261
|
+
}
|
|
5262
|
+
var spawnHelperChecked = false;
|
|
5263
|
+
function ensureSpawnHelperExecutable() {
|
|
5264
|
+
if (spawnHelperChecked || process.platform === "win32") return;
|
|
5265
|
+
spawnHelperChecked = true;
|
|
5266
|
+
try {
|
|
5267
|
+
const require2 = createRequire(import.meta.url);
|
|
5268
|
+
const root = join16(require2.resolve("node-pty/package.json"), "..");
|
|
5269
|
+
const candidates = [
|
|
5270
|
+
join16(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
|
|
5271
|
+
join16(root, "build", "Release", "spawn-helper")
|
|
5272
|
+
];
|
|
5273
|
+
for (const path of candidates) {
|
|
5274
|
+
try {
|
|
5275
|
+
accessSync(path, constants.X_OK);
|
|
5276
|
+
} catch {
|
|
5277
|
+
try {
|
|
5278
|
+
const mode = statSync(path).mode;
|
|
5279
|
+
chmodSync(path, mode | 73);
|
|
5280
|
+
} catch {
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
} catch {
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
|
|
4850
5288
|
// src/ws/hub.ts
|
|
4851
5289
|
var BACKGROUND_CAP = 4;
|
|
4852
5290
|
var replacementLocks = /* @__PURE__ */ new Map();
|
|
@@ -4877,7 +5315,7 @@ function attachWsHub(httpServer) {
|
|
|
4877
5315
|
}
|
|
4878
5316
|
});
|
|
4879
5317
|
wss.on("connection", (ws) => {
|
|
4880
|
-
const state = { background: /* @__PURE__ */ new Map() };
|
|
5318
|
+
const state = { background: /* @__PURE__ */ new Map(), terminals: /* @__PURE__ */ new Map() };
|
|
4881
5319
|
let inbound = Promise.resolve();
|
|
4882
5320
|
ws.on("message", (raw) => {
|
|
4883
5321
|
inbound = inbound.then(async () => {
|
|
@@ -4885,14 +5323,14 @@ function attachWsHub(httpServer) {
|
|
|
4885
5323
|
try {
|
|
4886
5324
|
msg = JSON.parse(raw.toString());
|
|
4887
5325
|
} catch {
|
|
4888
|
-
|
|
5326
|
+
send2(ws, { type: "error", message: "invalid JSON" });
|
|
4889
5327
|
return;
|
|
4890
5328
|
}
|
|
4891
5329
|
try {
|
|
4892
5330
|
await handle(ws, state, msg);
|
|
4893
5331
|
} catch (err2) {
|
|
4894
5332
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4895
|
-
|
|
5333
|
+
send2(ws, { type: "error", message, command: msg.type });
|
|
4896
5334
|
}
|
|
4897
5335
|
}).catch((err2) => {
|
|
4898
5336
|
console.error("[ws] inbound chain error:", err2);
|
|
@@ -4912,12 +5350,12 @@ async function handle(ws, state, msg) {
|
|
|
4912
5350
|
runtime = await workspaceManager.getOrCreate(msg.workspaceId, msg.sessionPath);
|
|
4913
5351
|
} catch (err2) {
|
|
4914
5352
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4915
|
-
|
|
4916
|
-
|
|
5353
|
+
send2(ws, { type: "error", message, command: "subscribe" });
|
|
5354
|
+
send2(ws, { type: "ack", command: "subscribe" });
|
|
4917
5355
|
return;
|
|
4918
5356
|
}
|
|
4919
5357
|
promoteToPrimary(ws, state, msg.workspaceId, runtime);
|
|
4920
|
-
|
|
5358
|
+
send2(ws, { type: "ack", command: "subscribe" });
|
|
4921
5359
|
return;
|
|
4922
5360
|
}
|
|
4923
5361
|
case "unsubscribe": {
|
|
@@ -4929,23 +5367,23 @@ async function handle(ws, state, msg) {
|
|
|
4929
5367
|
case "prompt": {
|
|
4930
5368
|
const primary = state.primary;
|
|
4931
5369
|
if (!primary) {
|
|
4932
|
-
|
|
5370
|
+
send2(ws, { type: "error", message: "not subscribed", command: "prompt" });
|
|
4933
5371
|
return;
|
|
4934
5372
|
}
|
|
4935
5373
|
if (replacementLocks.has(primary.workspaceId)) {
|
|
4936
|
-
|
|
5374
|
+
send2(ws, { type: "error", message: "session switching in progress", command: "prompt" });
|
|
4937
5375
|
return;
|
|
4938
5376
|
}
|
|
4939
5377
|
void primary.runtime.session.prompt(msg.message, { streamingBehavior: msg.streamingBehavior }).catch((err2) => {
|
|
4940
5378
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4941
|
-
|
|
5379
|
+
send2(ws, { type: "error", message, command: "prompt" });
|
|
4942
5380
|
});
|
|
4943
5381
|
return;
|
|
4944
5382
|
}
|
|
4945
5383
|
case "abort": {
|
|
4946
5384
|
const primary = state.primary;
|
|
4947
5385
|
if (!primary) {
|
|
4948
|
-
|
|
5386
|
+
send2(ws, { type: "error", message: "not subscribed", command: "abort" });
|
|
4949
5387
|
return;
|
|
4950
5388
|
}
|
|
4951
5389
|
await primary.runtime.session.abort();
|
|
@@ -4959,7 +5397,7 @@ async function handle(ws, state, msg) {
|
|
|
4959
5397
|
runtime = await workspaceManager.createSession(workspaceId);
|
|
4960
5398
|
} catch (err2) {
|
|
4961
5399
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4962
|
-
|
|
5400
|
+
send2(ws, { type: "error", message, command: "new_session" });
|
|
4963
5401
|
return;
|
|
4964
5402
|
}
|
|
4965
5403
|
promoteToPrimary(ws, state, workspaceId, runtime);
|
|
@@ -4969,22 +5407,22 @@ async function handle(ws, state, msg) {
|
|
|
4969
5407
|
case "fork": {
|
|
4970
5408
|
const primary = state.primary;
|
|
4971
5409
|
if (!primary) {
|
|
4972
|
-
|
|
5410
|
+
send2(ws, { type: "error", message: "not subscribed", command: "fork" });
|
|
4973
5411
|
return;
|
|
4974
5412
|
}
|
|
4975
5413
|
const workspaceId = primary.workspaceId;
|
|
4976
5414
|
await withReplacementLock(workspaceId, async () => {
|
|
4977
5415
|
const source = state.primary;
|
|
4978
5416
|
if (!source) {
|
|
4979
|
-
|
|
5417
|
+
send2(ws, { type: "error", message: "not subscribed", command: "fork" });
|
|
4980
5418
|
return;
|
|
4981
5419
|
}
|
|
4982
5420
|
if (!source.sessionPath) {
|
|
4983
|
-
|
|
5421
|
+
send2(ws, { type: "error", message: "cannot fork an unsaved session", command: "fork" });
|
|
4984
5422
|
return;
|
|
4985
5423
|
}
|
|
4986
5424
|
if (source.runtime.session.isStreaming) {
|
|
4987
|
-
|
|
5425
|
+
send2(ws, { type: "error", message: "cannot fork while streaming", command: "fork" });
|
|
4988
5426
|
return;
|
|
4989
5427
|
}
|
|
4990
5428
|
let result;
|
|
@@ -4992,11 +5430,11 @@ async function handle(ws, state, msg) {
|
|
|
4992
5430
|
result = await workspaceManager.fork(workspaceId, source.sessionPath, msg.entryId);
|
|
4993
5431
|
} catch (err2) {
|
|
4994
5432
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
4995
|
-
|
|
5433
|
+
send2(ws, { type: "error", message, command: "fork" });
|
|
4996
5434
|
return;
|
|
4997
5435
|
}
|
|
4998
5436
|
if (result.cancelled || !result.runtime) {
|
|
4999
|
-
|
|
5437
|
+
send2(ws, { type: "error", message: "fork cancelled", command: "fork" });
|
|
5000
5438
|
return;
|
|
5001
5439
|
}
|
|
5002
5440
|
promoteToPrimary(ws, state, workspaceId, result.runtime);
|
|
@@ -5006,7 +5444,7 @@ async function handle(ws, state, msg) {
|
|
|
5006
5444
|
case "answer_question": {
|
|
5007
5445
|
const primary = state.primary;
|
|
5008
5446
|
if (!primary) {
|
|
5009
|
-
|
|
5447
|
+
send2(ws, { type: "error", message: "not subscribed", command: "answer_question" });
|
|
5010
5448
|
return;
|
|
5011
5449
|
}
|
|
5012
5450
|
resolveAnswer(msg.toolCallId, msg.answer, primary.runtime.session.sessionFile ?? null);
|
|
@@ -5015,28 +5453,28 @@ async function handle(ws, state, msg) {
|
|
|
5015
5453
|
case "navigate_tree": {
|
|
5016
5454
|
const primary = state.primary;
|
|
5017
5455
|
if (!primary) {
|
|
5018
|
-
|
|
5456
|
+
send2(ws, { type: "error", message: "not subscribed", command: "navigate_tree" });
|
|
5019
5457
|
return;
|
|
5020
5458
|
}
|
|
5021
5459
|
if (msg.workspaceId !== primary.workspaceId) {
|
|
5022
|
-
|
|
5460
|
+
send2(ws, { type: "error", message: "workspace mismatch", command: "navigate_tree" });
|
|
5023
5461
|
return;
|
|
5024
5462
|
}
|
|
5025
5463
|
await withReplacementLock(primary.workspaceId, async () => {
|
|
5026
5464
|
const current = state.primary;
|
|
5027
5465
|
if (!current) {
|
|
5028
|
-
|
|
5466
|
+
send2(ws, { type: "error", message: "not subscribed", command: "navigate_tree" });
|
|
5029
5467
|
return;
|
|
5030
5468
|
}
|
|
5031
5469
|
if (current.runtime.session.isStreaming) {
|
|
5032
|
-
|
|
5470
|
+
send2(ws, { type: "error", message: "cannot navigate tree while streaming", command: "navigate_tree" });
|
|
5033
5471
|
return;
|
|
5034
5472
|
}
|
|
5035
5473
|
const result = await current.runtime.session.navigateTree(msg.targetId, {
|
|
5036
5474
|
summarize: msg.summarize,
|
|
5037
5475
|
customInstructions: msg.customInstructions
|
|
5038
5476
|
});
|
|
5039
|
-
|
|
5477
|
+
send2(ws, {
|
|
5040
5478
|
type: "navigate_tree_result",
|
|
5041
5479
|
workspaceId: current.workspaceId,
|
|
5042
5480
|
editorText: result.editorText,
|
|
@@ -5048,27 +5486,34 @@ async function handle(ws, state, msg) {
|
|
|
5048
5486
|
case "compact": {
|
|
5049
5487
|
const primary = state.primary;
|
|
5050
5488
|
if (!primary) {
|
|
5051
|
-
|
|
5489
|
+
send2(ws, { type: "error", message: "not subscribed", command: "compact" });
|
|
5052
5490
|
return;
|
|
5053
5491
|
}
|
|
5054
5492
|
if (primary.runtime.session.isStreaming) {
|
|
5055
|
-
|
|
5493
|
+
send2(ws, { type: "error", message: "cannot compact while streaming", command: "compact" });
|
|
5056
5494
|
return;
|
|
5057
5495
|
}
|
|
5058
5496
|
if (primary.runtime.session.isCompacting) {
|
|
5059
|
-
|
|
5497
|
+
send2(ws, { type: "error", message: "compaction already in progress", command: "compact" });
|
|
5060
5498
|
return;
|
|
5061
5499
|
}
|
|
5062
5500
|
primary.runtime.session.compact().catch((err2) => {
|
|
5063
5501
|
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
5064
|
-
|
|
5502
|
+
send2(ws, { type: "error", message, command: "compact" });
|
|
5065
5503
|
});
|
|
5066
5504
|
return;
|
|
5067
5505
|
}
|
|
5506
|
+
case "terminal_open":
|
|
5507
|
+
case "terminal_input":
|
|
5508
|
+
case "terminal_resize":
|
|
5509
|
+
case "terminal_close": {
|
|
5510
|
+
await handleTerminalMessage(ws, state.terminals, msg);
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5068
5513
|
default: {
|
|
5069
5514
|
const _ = msg;
|
|
5070
5515
|
void _;
|
|
5071
|
-
|
|
5516
|
+
send2(ws, { type: "error", message: "unknown command" });
|
|
5072
5517
|
}
|
|
5073
5518
|
}
|
|
5074
5519
|
}
|
|
@@ -5100,7 +5545,7 @@ function bindPrimary(ws, state, workspaceId, runtime) {
|
|
|
5100
5545
|
} else if (payload.kind === "message_update" && payload.delta.kind === "text" && assistantStartAt !== void 0 && assistantFirstTokenAt === void 0) {
|
|
5101
5546
|
assistantFirstTokenAt = performance.now();
|
|
5102
5547
|
}
|
|
5103
|
-
|
|
5548
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
5104
5549
|
if (payload.kind === "message_end" && payload.role === "assistant" && assistantStartAt !== void 0) {
|
|
5105
5550
|
const now = performance.now();
|
|
5106
5551
|
const timing = {
|
|
@@ -5108,7 +5553,7 @@ function bindPrimary(ws, state, workspaceId, runtime) {
|
|
|
5108
5553
|
firstTokenMs: assistantFirstTokenAt !== void 0 ? Math.round(assistantFirstTokenAt - assistantStartAt) : null,
|
|
5109
5554
|
totalMs: Math.round(now - assistantStartAt)
|
|
5110
5555
|
};
|
|
5111
|
-
|
|
5556
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload: timing });
|
|
5112
5557
|
assistantStartAt = void 0;
|
|
5113
5558
|
assistantFirstTokenAt = void 0;
|
|
5114
5559
|
}
|
|
@@ -5124,16 +5569,16 @@ function bindPrimary(ws, state, workspaceId, runtime) {
|
|
|
5124
5569
|
runtime.session.state.pendingToolCalls,
|
|
5125
5570
|
scanMessages
|
|
5126
5571
|
)) {
|
|
5127
|
-
|
|
5572
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
5128
5573
|
}
|
|
5129
5574
|
const inFlight = inFlightAssistantSnapshot(streamingMessage);
|
|
5130
5575
|
if (inFlight) {
|
|
5131
5576
|
for (const payload of inFlight) {
|
|
5132
|
-
|
|
5577
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
5133
5578
|
}
|
|
5134
5579
|
}
|
|
5135
5580
|
for (const payload of inFlightToolCallsSnapshot(sessionPath)) {
|
|
5136
|
-
|
|
5581
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
5137
5582
|
}
|
|
5138
5583
|
sendContextUsage(ws, runtime, workspaceId, sessionPath);
|
|
5139
5584
|
}
|
|
@@ -5147,7 +5592,7 @@ function demotePrimaryToBackground(ws, state) {
|
|
|
5147
5592
|
const unsubscribeSession = session.subscribe((ev) => {
|
|
5148
5593
|
const payload = translatePiEvent(ev);
|
|
5149
5594
|
if (!payload) return;
|
|
5150
|
-
|
|
5595
|
+
send2(ws, { type: "event", workspaceId: primary.workspaceId, sessionPath, payload });
|
|
5151
5596
|
if (payload.kind === "agent_end" || payload.kind === "compaction_end" || payload.kind === "session_info_changed" || payload.kind === "thinking_level_changed") {
|
|
5152
5597
|
sendContextUsage(ws, primary.runtime, primary.workspaceId, sessionPath);
|
|
5153
5598
|
}
|
|
@@ -5163,7 +5608,7 @@ function demotePrimaryToBackground(ws, state) {
|
|
|
5163
5608
|
const evicted = state.background.get(oldestKey);
|
|
5164
5609
|
teardownBackground(state, oldestKey, ws);
|
|
5165
5610
|
if (evicted) {
|
|
5166
|
-
|
|
5611
|
+
send2(ws, {
|
|
5167
5612
|
type: "background_evicted",
|
|
5168
5613
|
workspaceId: evicted.workspaceId,
|
|
5169
5614
|
sessionPath: evicted.sessionPath
|
|
@@ -5179,7 +5624,7 @@ function teardownBackground(state, runtimeKey, ws) {
|
|
|
5179
5624
|
if (ws) unrefWorkspaceSubscriber(state, bg.workspaceId, ws);
|
|
5180
5625
|
}
|
|
5181
5626
|
function sendSubscribed(ws, workspaceId, runtime) {
|
|
5182
|
-
|
|
5627
|
+
send2(ws, {
|
|
5183
5628
|
type: "subscribed",
|
|
5184
5629
|
workspaceId,
|
|
5185
5630
|
sessionPath: runtime.session.sessionFile ?? null,
|
|
@@ -5199,7 +5644,7 @@ function sendContextUsage(ws, runtime, workspaceId, sessionPath) {
|
|
|
5199
5644
|
contextWindow: usage.contextWindow,
|
|
5200
5645
|
percent: usage.percent
|
|
5201
5646
|
};
|
|
5202
|
-
|
|
5647
|
+
send2(ws, { type: "event", workspaceId, sessionPath, payload });
|
|
5203
5648
|
}
|
|
5204
5649
|
function detachPrimary(state, ws) {
|
|
5205
5650
|
const primary = state.primary;
|
|
@@ -5213,8 +5658,9 @@ function detach(state, ws) {
|
|
|
5213
5658
|
for (const runtimeKey of [...state.background.keys()]) {
|
|
5214
5659
|
teardownBackground(state, runtimeKey, ws);
|
|
5215
5660
|
}
|
|
5661
|
+
closeAllTerminals(state.terminals);
|
|
5216
5662
|
}
|
|
5217
|
-
function
|
|
5663
|
+
function send2(ws, msg) {
|
|
5218
5664
|
if (ws.readyState !== ws.OPEN) return;
|
|
5219
5665
|
ws.send(JSON.stringify(msg));
|
|
5220
5666
|
}
|
|
@@ -5223,8 +5669,8 @@ function send(ws, msg) {
|
|
|
5223
5669
|
configureHttpProxy();
|
|
5224
5670
|
var app = new Hono6();
|
|
5225
5671
|
var distDir = dirname6(fileURLToPath2(import.meta.url));
|
|
5226
|
-
var webRoot =
|
|
5227
|
-
var webIndexPath =
|
|
5672
|
+
var webRoot = resolve8(process.env.PI_PILOT_WEB_ROOT ?? join17(distDir, "..", "public"));
|
|
5673
|
+
var webIndexPath = join17(webRoot, "index.html");
|
|
5228
5674
|
var mimeTypes = {
|
|
5229
5675
|
".css": "text/css; charset=utf-8",
|
|
5230
5676
|
".html": "text/html; charset=utf-8",
|
|
@@ -5250,7 +5696,7 @@ function safeResolveWebPath(pathname) {
|
|
|
5250
5696
|
return void 0;
|
|
5251
5697
|
}
|
|
5252
5698
|
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
5253
|
-
const candidate =
|
|
5699
|
+
const candidate = resolve8(webRoot, relativePath);
|
|
5254
5700
|
if (candidate !== webRoot && !candidate.startsWith(`${webRoot}${sep3}`)) {
|
|
5255
5701
|
return void 0;
|
|
5256
5702
|
}
|
|
@@ -5258,7 +5704,7 @@ function safeResolveWebPath(pathname) {
|
|
|
5258
5704
|
}
|
|
5259
5705
|
async function readWebFile(path) {
|
|
5260
5706
|
try {
|
|
5261
|
-
return await
|
|
5707
|
+
return await readFile10(path);
|
|
5262
5708
|
} catch (err2) {
|
|
5263
5709
|
const code = err2.code;
|
|
5264
5710
|
if (code === "ENOENT" || code === "EISDIR") return void 0;
|
|
@@ -5271,7 +5717,7 @@ async function serveWeb(c) {
|
|
|
5271
5717
|
const assetPath = safeResolveWebPath(pathname);
|
|
5272
5718
|
if (!assetPath) return c.text("invalid asset path", 400);
|
|
5273
5719
|
const asset = await readWebFile(assetPath);
|
|
5274
|
-
const body = asset ?? await
|
|
5720
|
+
const body = asset ?? await readFile10(webIndexPath);
|
|
5275
5721
|
const filePath = asset ? assetPath : webIndexPath;
|
|
5276
5722
|
const headers = {
|
|
5277
5723
|
"Content-Type": mimeTypes[extname(filePath)] ?? "application/octet-stream",
|