@computesdk/loops 0.1.2 → 0.1.3

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.
Files changed (75) hide show
  1. package/API.md +35 -5
  2. package/README.md +67 -21
  3. package/bin/loops.mjs +13 -4
  4. package/bin/pi-rpc-startup.mjs +38 -0
  5. package/bin/pi-rpc.mjs +27 -0
  6. package/client.ts +16 -1
  7. package/deploy/loops.service.example +24 -0
  8. package/dist/assets/{abnfDiagram-VCTEODGH-CqEf5Tyi.js → abnfDiagram-VCTEODGH-BjWORKhB.js} +1 -1
  9. package/dist/assets/{arc-CrYrleHr.js → arc-iaXuj2E3.js} +1 -1
  10. package/dist/assets/{architectureDiagram-5GKGNRK7-D5K4RlB6.js → architectureDiagram-5GKGNRK7-BGYlRe5d.js} +1 -1
  11. package/dist/assets/{blockDiagram-I7D4REHJ-Co-w8wX9.js → blockDiagram-I7D4REHJ-DbRX-Eko.js} +1 -1
  12. package/dist/assets/{c4Diagram-7LVT6UL2-DlcBV2hX.js → c4Diagram-7LVT6UL2-DFNI3QWM.js} +1 -1
  13. package/dist/assets/channel-mVgxi6SM.js +1 -0
  14. package/dist/assets/{chunk-2Q5K7J3B-Dswi4LXs.js → chunk-2Q5K7J3B-DqhjvffG.js} +1 -1
  15. package/dist/assets/{chunk-5VM5RSS4-CtLKoCf3.js → chunk-5VM5RSS4-pskYIVgB.js} +1 -1
  16. package/dist/assets/{chunk-F27PBJKO-Bbck4p8J.js → chunk-F27PBJKO-CQcyYiby.js} +1 -1
  17. package/dist/assets/{chunk-IMKFNOWR-CvTEJd26.js → chunk-IMKFNOWR-B0MpoITM.js} +1 -1
  18. package/dist/assets/{chunk-JWPE2WC7-AIHpJEyX.js → chunk-JWPE2WC7-BsNxH0py.js} +1 -1
  19. package/dist/assets/{chunk-POPQ4Y6H-C_B3I7hU.js → chunk-POPQ4Y6H-VnAuAhI3.js} +1 -1
  20. package/dist/assets/{chunk-SVP7TREG-au1NQO2y.js → chunk-SVP7TREG-fiQQ7YON.js} +1 -1
  21. package/dist/assets/{chunk-TICWLB2K-DOQXr0QZ.js → chunk-TICWLB2K-agM_rWnc.js} +1 -1
  22. package/dist/assets/{chunk-XXDRQBXY-U36QglrB.js → chunk-XXDRQBXY-BrnsKK60.js} +1 -1
  23. package/dist/assets/classDiagram-ZZMXUADV-BNg4EoCj.js +1 -0
  24. package/dist/assets/classDiagram-v2-VYDZK3BY-BNg4EoCj.js +1 -0
  25. package/dist/assets/{cose-bilkent-JH36ORCC-D7GIzP1z.js → cose-bilkent-JH36ORCC-skVRi8C0.js} +1 -1
  26. package/dist/assets/{cynefin-OW5HDTMX-CjokfIiy.js → cynefin-OW5HDTMX-sugAAQ5D.js} +1 -1
  27. package/dist/assets/{cynefinDiagram-5FMLGOSQ-1aEkMuPh.js → cynefinDiagram-5FMLGOSQ-BTjQ5HD4.js} +1 -1
  28. package/dist/assets/{dagre-GXQ25YYZ-BiIp4Lpg.js → dagre-GXQ25YYZ-wS0CVEvh.js} +1 -1
  29. package/dist/assets/{diagram-S7CK7UJ4-BZSI7zSx.js → diagram-S7CK7UJ4-0P_U489Q.js} +1 -1
  30. package/dist/assets/{diagram-UQ7AKVKN-BRdBU2w8.js → diagram-UQ7AKVKN-D85SHcZz.js} +1 -1
  31. package/dist/assets/{diagram-VSXAHHWV-DQ-h_ycl.js → diagram-VSXAHHWV-CozaqKLI.js} +1 -1
  32. package/dist/assets/{diagram-VX7I27RA-DmLQWSdL.js → diagram-VX7I27RA-kzpgzRzP.js} +1 -1
  33. package/dist/assets/{diagram-Z3DM3KII-m8FGF4_n.js → diagram-Z3DM3KII-DeIAvld9.js} +1 -1
  34. package/dist/assets/{ebnfDiagram-PWID7BFC-DtQ-Rw5F.js → ebnfDiagram-PWID7BFC-Cvo7CZD4.js} +1 -1
  35. package/dist/assets/{erDiagram-RLTQ6QDP-DIf-60L3.js → erDiagram-RLTQ6QDP-BILowZs8.js} +1 -1
  36. package/dist/assets/{flowDiagram-HODETNUW-COvzkFo5.js → flowDiagram-HODETNUW-D58_1bOZ.js} +1 -1
  37. package/dist/assets/{ganttDiagram-EL5Y4UJY-Dhz_v-Sb.js → ganttDiagram-EL5Y4UJY-Wc2egLDH.js} +1 -1
  38. package/dist/assets/{gitGraphDiagram-WWUBYQGX-BwkibubM.js → gitGraphDiagram-WWUBYQGX-prvx2Pvu.js} +1 -1
  39. package/dist/assets/{highlighted-body-JLMTFACH-CbkekRih.js → highlighted-body-JLMTFACH-D3B11uXh.js} +1 -1
  40. package/dist/assets/index-C68b-bL6.css +1 -0
  41. package/dist/assets/index-CoHgGrA6.js +900 -0
  42. package/dist/assets/{infoDiagram-27XIBGKW-BltzH8hk.js → infoDiagram-27XIBGKW-S1_RXMDl.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-5VMMS53U-CKXLbULh.js → ishikawaDiagram-5VMMS53U-DPVVwIcA.js} +1 -1
  44. package/dist/assets/{journeyDiagram-3NMN7TZE-Co-OVAQM.js → journeyDiagram-3NMN7TZE-Dmlf7Ej8.js} +1 -1
  45. package/dist/assets/{kanban-definition-UXKFOSKX-QEXNvAHJ.js → kanban-definition-UXKFOSKX-D7SJAgUZ.js} +1 -1
  46. package/dist/assets/{layout-DlW7eXy-.js → layout-DWrir7bj.js} +1 -1
  47. package/dist/assets/{linear-DmNmIGNl.js → linear-B7B0s6Fb.js} +1 -1
  48. package/dist/assets/{mindmap-definition-YA3MSWOX-CfrGbvrI.js → mindmap-definition-YA3MSWOX-CvytaBvR.js} +1 -1
  49. package/dist/assets/{pegDiagram-XKGWAZYB-BRPtQX-b.js → pegDiagram-XKGWAZYB-unjus9we.js} +1 -1
  50. package/dist/assets/{pieDiagram-E7YTZNPT-cyRAi108.js → pieDiagram-E7YTZNPT-Bc0mRNN1.js} +1 -1
  51. package/dist/assets/{quadrantDiagram-AXDQQJYC-BBwMoijR.js → quadrantDiagram-AXDQQJYC-DCMBYddi.js} +1 -1
  52. package/dist/assets/{railroadDiagram-O6MQD6OU-DicocbzE.js → railroadDiagram-O6MQD6OU-Bbb5CPfd.js} +1 -1
  53. package/dist/assets/{requirementDiagram-BXWQKSXE-DDiXCN4F.js → requirementDiagram-BXWQKSXE-C8qmEESJ.js} +1 -1
  54. package/dist/assets/{sankeyDiagram-P5KCCOFB-DnDGP5-W.js → sankeyDiagram-P5KCCOFB-ChzXGPH8.js} +1 -1
  55. package/dist/assets/{sequenceDiagram-WJ2MYXX4-C1NFZjf0.js → sequenceDiagram-WJ2MYXX4-BeKUAzAe.js} +1 -1
  56. package/dist/assets/{sizeCapture-INFHLROL-CEF6O58z.js → sizeCapture-INFHLROL-YJiYQ0Mn.js} +1 -1
  57. package/dist/assets/{stateDiagram-D77RDMKH-Ibszr2iY.js → stateDiagram-D77RDMKH-D6NvtAkq.js} +1 -1
  58. package/dist/assets/stateDiagram-v2-MP3YSRHH-C9eBMjpA.js +1 -0
  59. package/dist/assets/{swimlanes-42K2YHIH-DbvtObGJ.js → swimlanes-42K2YHIH-Cjb-j5cm.js} +1 -1
  60. package/dist/assets/swimlanesDiagram-VR7AAH4N-tvNn9kix.js +8 -0
  61. package/dist/assets/{timeline-definition-24CTP7MA-C6qVzSzu.js → timeline-definition-24CTP7MA-CaBJ4VuK.js} +1 -1
  62. package/dist/assets/{vennDiagram-4TSXK5OY-B3ZZFoCj.js → vennDiagram-4TSXK5OY-D8MLav5x.js} +1 -1
  63. package/dist/assets/{wardleyDiagram-VM6X3IG4-C9pFlzx9.js → wardleyDiagram-VM6X3IG4-CSy7rn-Q.js} +1 -1
  64. package/dist/assets/{xychartDiagram-S5SC5T6Z-mUkoBUkH.js → xychartDiagram-S5SC5T6Z-BtRqCsaE.js} +1 -1
  65. package/dist/index.html +2 -2
  66. package/dist/server.mjs +646 -311
  67. package/package.json +5 -3
  68. package/src/shared/types.ts +1 -1
  69. package/dist/assets/channel-DoVi6ksL.js +0 -1
  70. package/dist/assets/classDiagram-ZZMXUADV-Cyi65D9L.js +0 -1
  71. package/dist/assets/classDiagram-v2-VYDZK3BY-Cyi65D9L.js +0 -1
  72. package/dist/assets/index-DhZYU086.js +0 -900
  73. package/dist/assets/index-NzHlk7dF.css +0 -1
  74. package/dist/assets/stateDiagram-v2-MP3YSRHH-CCH6TZvE.js +0 -1
  75. package/dist/assets/swimlanesDiagram-VR7AAH4N-DyRa4rww.js +0 -8
package/dist/server.mjs CHANGED
@@ -1,10 +1,38 @@
1
1
  // server/index.ts
2
- import fs8 from "node:fs";
3
- import { randomBytes } from "node:crypto";
2
+ import fs9 from "node:fs";
3
+ import { randomBytes, randomUUID } from "node:crypto";
4
4
  import http from "node:http";
5
- import path8 from "node:path";
5
+ import path9 from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { execFile, execFileSync as execFileSync2, spawn as spawn6 } from "node:child_process";
7
+ import { execFile as execFile2, execFileSync as execFileSync2, spawn as spawn6 } from "node:child_process";
8
+
9
+ // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/http-exception.js
10
+ var HTTPException = class extends Error {
11
+ res;
12
+ status;
13
+ /**
14
+ * Creates an instance of `HTTPException`.
15
+ * @param status - HTTP status code for the exception. Defaults to 500.
16
+ * @param options - Additional options for the exception.
17
+ */
18
+ constructor(status = 500, options) {
19
+ super(options?.message, { cause: options?.cause });
20
+ this.res = options?.res;
21
+ this.status = status;
22
+ }
23
+ /**
24
+ * Returns the response object associated with the exception.
25
+ * If a response object is not provided, a new response is created with the error message and status code.
26
+ * @returns The response object.
27
+ */
28
+ getResponse() {
29
+ if (this.res) return new Response(this.res.body, {
30
+ status: this.status,
31
+ headers: this.res.headers
32
+ });
33
+ return new Response(this.message, { status: this.status });
34
+ }
35
+ };
8
36
 
9
37
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/request/constants.js
10
38
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
@@ -79,26 +107,26 @@ var throwNestingLimitExceeded = () => {
79
107
  };
80
108
 
81
109
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/utils/url.js
82
- var splitPath = (path9) => {
83
- const paths = path9.split("/");
110
+ var splitPath = (path10) => {
111
+ const paths = path10.split("/");
84
112
  if (paths[0] === "") paths.shift();
85
113
  return paths;
86
114
  };
87
115
  var splitRoutingPath = (routePath) => {
88
- const { groups, path: path9 } = extractGroupsFromPath(routePath);
89
- const paths = splitPath(path9);
116
+ const { groups, path: path10 } = extractGroupsFromPath(routePath);
117
+ const paths = splitPath(path10);
90
118
  return replaceGroupMarks(paths, groups);
91
119
  };
92
- var extractGroupsFromPath = (path9) => {
120
+ var extractGroupsFromPath = (path10) => {
93
121
  const groups = [];
94
- path9 = path9.replace(/\{[^}]+\}/g, (match2, index) => {
122
+ path10 = path10.replace(/\{[^}]+\}/g, (match2, index) => {
95
123
  const mark = `@${index}`;
96
124
  groups.push([mark, match2]);
97
125
  return mark;
98
126
  });
99
127
  return {
100
128
  groups,
101
- path: path9
129
+ path: path10
102
130
  };
103
131
  };
104
132
  var replaceGroupMarks = (paths, groups) => {
@@ -161,8 +189,8 @@ var getPath = (request) => {
161
189
  const queryIndex = url.indexOf("?", i);
162
190
  const hashIndex = url.indexOf("#", i);
163
191
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
164
- const path9 = url.slice(start, end);
165
- return tryDecodeURI(path9.includes("%25") ? path9.replace(/%25/g, "%2525") : path9);
192
+ const path10 = url.slice(start, end);
193
+ return tryDecodeURI(path10.includes("%25") ? path10.replace(/%25/g, "%2525") : path10);
166
194
  } else if (charCode === 63 || charCode === 35) break;
167
195
  }
168
196
  return url.slice(start, i);
@@ -175,9 +203,9 @@ var mergePath = (base, sub, ...rest) => {
175
203
  if (rest.length) sub = mergePath(sub, ...rest);
176
204
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
177
205
  };
178
- var checkOptionalParameter = (path9) => {
179
- if (path9.charCodeAt(path9.length - 1) !== 63 || !path9.includes(":")) return null;
180
- const segments = path9.split("/");
206
+ var checkOptionalParameter = (path10) => {
207
+ if (path10.charCodeAt(path10.length - 1) !== 63 || !path10.includes(":")) return null;
208
+ const segments = path10.split("/");
181
209
  const results = [];
182
210
  let basePath = "";
183
211
  segments.forEach((segment) => {
@@ -288,9 +316,9 @@ var HonoRequest = class {
288
316
  */
289
317
  path;
290
318
  bodyCache = {};
291
- constructor(request, path9 = "/", matchResult = [[]]) {
319
+ constructor(request, path10 = "/", matchResult = [[]]) {
292
320
  this.raw = request;
293
- this.path = path9;
321
+ this.path = path10;
294
322
  this.#matchResult = matchResult;
295
323
  }
296
324
  param(key) {
@@ -1020,8 +1048,8 @@ var Hono = class Hono2 {
1020
1048
  return this;
1021
1049
  };
1022
1050
  });
1023
- this.on = (method, path9, ...handlers) => {
1024
- for (const p of [path9].flat()) {
1051
+ this.on = (method, path10, ...handlers) => {
1052
+ for (const p of [path10].flat()) {
1025
1053
  this.#path = p;
1026
1054
  for (const m of [method].flat()) {
1027
1055
  const methodName = m.toUpperCase();
@@ -1075,8 +1103,8 @@ var Hono = class Hono2 {
1075
1103
  * app.route("/api", app2) // GET /api/user
1076
1104
  * ```
1077
1105
  */
1078
- route(path9, app2) {
1079
- const subApp = this.basePath(path9);
1106
+ route(path10, app2) {
1107
+ const subApp = this.basePath(path10);
1080
1108
  app2.routes.map((r) => {
1081
1109
  let handler;
1082
1110
  if (app2.errorHandler === errorHandler) handler = r.handler;
@@ -1101,9 +1129,9 @@ var Hono = class Hono2 {
1101
1129
  * const api = new Hono().basePath('/api')
1102
1130
  * ```
1103
1131
  */
1104
- basePath(path9) {
1132
+ basePath(path10) {
1105
1133
  const subApp = this.#clone();
1106
- subApp._basePath = mergePath(this._basePath, path9);
1134
+ subApp._basePath = mergePath(this._basePath, path10);
1107
1135
  return subApp;
1108
1136
  }
1109
1137
  /**
@@ -1179,7 +1207,7 @@ var Hono = class Hono2 {
1179
1207
  * })
1180
1208
  * ```
1181
1209
  */
1182
- mount(path9, applicationHandler, options) {
1210
+ mount(path10, applicationHandler, options) {
1183
1211
  let replaceRequest;
1184
1212
  let optionHandler;
1185
1213
  if (options) {
@@ -1202,7 +1230,7 @@ var Hono = class Hono2 {
1202
1230
  return [c.env, executionContext];
1203
1231
  };
1204
1232
  replaceRequest ||= (() => {
1205
- const mergedPath = mergePath(this._basePath, path9);
1233
+ const mergedPath = mergePath(this._basePath, path10);
1206
1234
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
1207
1235
  return (request) => {
1208
1236
  const url = new URL(request.url);
@@ -1215,18 +1243,18 @@ var Hono = class Hono2 {
1215
1243
  if (res) return res;
1216
1244
  await next();
1217
1245
  };
1218
- this.#addRoute("ALL", mergePath(path9, "*"), handler);
1246
+ this.#addRoute("ALL", mergePath(path10, "*"), handler);
1219
1247
  return this;
1220
1248
  }
1221
- #addRoute(method, path9, handler, baseRoutePath) {
1222
- path9 = mergePath(this._basePath, path9);
1249
+ #addRoute(method, path10, handler, baseRoutePath) {
1250
+ path10 = mergePath(this._basePath, path10);
1223
1251
  const r = {
1224
1252
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
1225
- path: path9,
1253
+ path: path10,
1226
1254
  method,
1227
1255
  handler
1228
1256
  };
1229
- this.router.add(method, path9, [handler, r]);
1257
+ this.router.add(method, path10, [handler, r]);
1230
1258
  this.routes.push(r);
1231
1259
  }
1232
1260
  #handleError(err, c) {
@@ -1235,10 +1263,10 @@ var Hono = class Hono2 {
1235
1263
  }
1236
1264
  #dispatch(request, executionCtx, env, method) {
1237
1265
  if (method === "HEAD") return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
1238
- const path9 = this.getPath(request, { env });
1239
- const matchResult = this.router.match(method, path9);
1266
+ const path10 = this.getPath(request, { env });
1267
+ const matchResult = this.router.match(method, path10);
1240
1268
  const c = new Context(request, {
1241
- path: path9,
1269
+ path: path10,
1242
1270
  matchResult,
1243
1271
  env,
1244
1272
  executionCtx,
@@ -1326,19 +1354,19 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
1326
1354
 
1327
1355
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/matcher.js
1328
1356
  var emptyParam = [];
1329
- function match(method, path9) {
1357
+ function match(method, path10) {
1330
1358
  const matchers = this.buildAllMatchers();
1331
- const match2 = ((method2, path10) => {
1359
+ const match2 = ((method2, path11) => {
1332
1360
  const matcher = matchers[method2] || matchers["ALL"];
1333
- const staticMatch = matcher[2][path10];
1361
+ const staticMatch = matcher[2][path11];
1334
1362
  if (staticMatch) return staticMatch;
1335
- const match3 = path10.match(matcher[0]);
1363
+ const match3 = path11.match(matcher[0]);
1336
1364
  if (!match3) return [[], emptyParam];
1337
1365
  const index = match3.indexOf("", 1);
1338
1366
  return [matcher[1][index], match3];
1339
1367
  });
1340
1368
  this.match = match2;
1341
- return match2(method, path9);
1369
+ return match2(method, path10);
1342
1370
  }
1343
1371
 
1344
1372
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -1428,14 +1456,14 @@ var Trie = class {
1428
1456
  #root = new Node();
1429
1457
  #index = 0;
1430
1458
  paths = createNullObject();
1431
- insert(path9, isStatic) {
1459
+ insert(path10, isStatic) {
1432
1460
  if (isStatic) {
1433
- this.#root.insert(path9.split(""), 0, [], this.#context, true);
1461
+ this.#root.insert(path10.split(""), 0, [], this.#context, true);
1434
1462
  return;
1435
1463
  }
1436
1464
  const paramAssoc = [];
1437
1465
  const groups = [];
1438
- let markedPath = path9;
1466
+ let markedPath = path10;
1439
1467
  for (let i = 0; ; ) {
1440
1468
  let replaced = false;
1441
1469
  markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
@@ -1456,7 +1484,7 @@ var Trie = class {
1456
1484
  }
1457
1485
  }
1458
1486
  this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
1459
- this.paths[path9] = [this.#index++, paramAssoc];
1487
+ this.paths[path10] = [this.#index++, paramAssoc];
1460
1488
  }
1461
1489
  buildRegExp() {
1462
1490
  let regexp = this.#root.buildRegExpStr();
@@ -1489,11 +1517,11 @@ var Trie = class {
1489
1517
 
1490
1518
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/router.js
1491
1519
  var wildcardRegExpCache = createNullObject();
1492
- function buildWildcardRegExp(path9) {
1493
- return wildcardRegExpCache[path9] ??= new RegExp(`^${path9.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`);
1520
+ function buildWildcardRegExp(path10) {
1521
+ return wildcardRegExpCache[path10] ??= new RegExp(`^${path10.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`);
1494
1522
  }
1495
- function findMiddleware(middleware, path9) {
1496
- for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path9)) return [...middleware[k]];
1523
+ function findMiddleware(middleware, path10) {
1524
+ for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path10)) return [...middleware[k]];
1497
1525
  }
1498
1526
  var RegExpRouter = class {
1499
1527
  name = "RegExpRouter";
@@ -1505,14 +1533,14 @@ var RegExpRouter = class {
1505
1533
  this.#routes = { ["ALL"]: createNullObject() };
1506
1534
  this.#tries = { ["ALL"]: new Trie() };
1507
1535
  }
1508
- #insertPath(method, path9) {
1536
+ #insertPath(method, path10) {
1509
1537
  try {
1510
- this.#tries[method].insert(path9, !/\*|\/:/.test(path9));
1538
+ this.#tries[method].insert(path10, !/\*|\/:/.test(path10));
1511
1539
  } catch (e) {
1512
- throw e === PATH_ERROR ? new UnsupportedPathError(path9) : e;
1540
+ throw e === PATH_ERROR ? new UnsupportedPathError(path10) : e;
1513
1541
  }
1514
1542
  }
1515
- add(method, path9, handler) {
1543
+ add(method, path10, handler) {
1516
1544
  const middleware = this.#middleware;
1517
1545
  const routes = this.#routes;
1518
1546
  if (!middleware) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
@@ -1526,24 +1554,24 @@ var RegExpRouter = class {
1526
1554
  }
1527
1555
  }
1528
1556
  }
1529
- if (path9 === "/*") path9 = "*";
1557
+ if (path10 === "/*") path10 = "*";
1530
1558
  const methods = method === "ALL" ? Object.keys(middleware) : [method];
1531
- if (/\*$/.test(path9)) {
1532
- const re = buildWildcardRegExp(path9);
1533
- for (const m of methods) if (!middleware[m][path9]) {
1534
- this.#insertPath(m, path9);
1535
- middleware[m][path9] = findMiddleware(middleware[m], path9) || findMiddleware(middleware["ALL"], path9) || [];
1559
+ if (/\*$/.test(path10)) {
1560
+ const re = buildWildcardRegExp(path10);
1561
+ for (const m of methods) if (!middleware[m][path10]) {
1562
+ this.#insertPath(m, path10);
1563
+ middleware[m][path10] = findMiddleware(middleware[m], path10) || findMiddleware(middleware["ALL"], path10) || [];
1536
1564
  }
1537
- for (const handlerMap of [middleware, routes]) for (const m of methods) for (const p in handlerMap[m]) re.test(p) && handlerMap[m][p].push([handler, path9]);
1565
+ for (const handlerMap of [middleware, routes]) for (const m of methods) for (const p in handlerMap[m]) re.test(p) && handlerMap[m][p].push([handler, path10]);
1538
1566
  return;
1539
1567
  }
1540
- const paths = checkOptionalParameter(path9) || [path9];
1541
- for (const path10 of paths) for (const m of methods) {
1542
- if (!routes[m][path10]) {
1543
- this.#insertPath(m, path10);
1544
- routes[m][path10] = findMiddleware(middleware[m], path10) || findMiddleware(middleware["ALL"], path10) || [];
1568
+ const paths = checkOptionalParameter(path10) || [path10];
1569
+ for (const path11 of paths) for (const m of methods) {
1570
+ if (!routes[m][path11]) {
1571
+ this.#insertPath(m, path11);
1572
+ routes[m][path11] = findMiddleware(middleware[m], path11) || findMiddleware(middleware["ALL"], path11) || [];
1545
1573
  }
1546
- routes[m][path10].push([handler, path10]);
1574
+ routes[m][path11].push([handler, path11]);
1547
1575
  }
1548
1576
  }
1549
1577
  match = match;
@@ -1561,11 +1589,11 @@ var RegExpRouter = class {
1561
1589
  const staticMap = createNullObject();
1562
1590
  const handlerData = [];
1563
1591
  const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
1564
- for (const r of [middleware, routes]) for (const path9 in r) {
1565
- const handlers = r[path9];
1566
- const pathData = trie.paths[path9];
1592
+ for (const r of [middleware, routes]) for (const path10 in r) {
1593
+ const handlers = r[path10];
1594
+ const pathData = trie.paths[path10];
1567
1595
  if (!pathData) {
1568
- staticMap[path9] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
1596
+ staticMap[path10] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
1569
1597
  continue;
1570
1598
  }
1571
1599
  handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => {
@@ -1589,15 +1617,15 @@ var SmartRouter = class {
1589
1617
  constructor(init) {
1590
1618
  this.#routers = init.routers;
1591
1619
  }
1592
- add(method, path9, handler) {
1620
+ add(method, path10, handler) {
1593
1621
  if (!this.#routes) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
1594
1622
  this.#routes.push([
1595
1623
  method,
1596
- path9,
1624
+ path10,
1597
1625
  handler
1598
1626
  ]);
1599
1627
  }
1600
- match(method, path9) {
1628
+ match(method, path10) {
1601
1629
  if (!this.#routes) throw new Error("Fatal error");
1602
1630
  const routers = this.#routers;
1603
1631
  const routes = this.#routes;
@@ -1608,7 +1636,7 @@ var SmartRouter = class {
1608
1636
  const router = routers[i];
1609
1637
  try {
1610
1638
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) router.add(...routes[i2]);
1611
- res = router.match(method, path9);
1639
+ res = router.match(method, path10);
1612
1640
  } catch (e) {
1613
1641
  if (e instanceof UnsupportedPathError) continue;
1614
1642
  throw e;
@@ -1637,9 +1665,9 @@ var Node3 = class Node4 {
1637
1665
  #patterns = [];
1638
1666
  #pattern;
1639
1667
  #params = emptyParams;
1640
- insert(method, path9, handler) {
1668
+ insert(method, path10, handler) {
1641
1669
  let curNode = this;
1642
- const parts = splitRoutingPath(path9);
1670
+ const parts = splitRoutingPath(path10);
1643
1671
  const possibleKeys = /* @__PURE__ */ new Set();
1644
1672
  let i = 0;
1645
1673
  for (const p of parts) {
@@ -1675,11 +1703,11 @@ var Node3 = class Node4 {
1675
1703
  }
1676
1704
  }
1677
1705
  }
1678
- search(method, path9) {
1706
+ search(method, path10) {
1679
1707
  const handlerSets = [];
1680
1708
  this.#params = emptyParams;
1681
1709
  let curNodes = [this];
1682
- const parts = splitPath(path9);
1710
+ const parts = splitPath(path10);
1683
1711
  const curNodesQueue = [];
1684
1712
  const len = parts.length;
1685
1713
  let partOffsets = null;
@@ -1715,13 +1743,13 @@ var Node3 = class Node4 {
1715
1743
  if (matcher !== true) {
1716
1744
  if (!partOffsets) {
1717
1745
  partOffsets = [];
1718
- let offset = path9[0] === "/" ? 1 : 0;
1746
+ let offset = path10[0] === "/" ? 1 : 0;
1719
1747
  for (let p = 0; p < len; p++) {
1720
1748
  partOffsets[p] = offset;
1721
1749
  offset += parts[p].length + 1;
1722
1750
  }
1723
1751
  }
1724
- const restPathString = path9.slice(partOffsets[i]);
1752
+ const restPathString = path10.slice(partOffsets[i]);
1725
1753
  const m = matcher.exec(restPathString);
1726
1754
  if (m) {
1727
1755
  if (!Object.hasOwn(params, name)) params[name] = m[0];
@@ -1762,11 +1790,11 @@ var Node3 = class Node4 {
1762
1790
  var TrieRouter = class {
1763
1791
  name = "TrieRouter";
1764
1792
  #node = new Node3();
1765
- add(method, path9, handler) {
1766
- for (const result of checkOptionalParameter(path9) || [path9]) this.#node.insert(method, result, handler);
1793
+ add(method, path10, handler) {
1794
+ for (const result of checkOptionalParameter(path10) || [path10]) this.#node.insert(method, result, handler);
1767
1795
  }
1768
- match(method, path9) {
1769
- return this.#node.search(method, path9);
1796
+ match(method, path10) {
1797
+ return this.#node.search(method, path10);
1770
1798
  }
1771
1799
  };
1772
1800
 
@@ -2347,19 +2375,19 @@ var flushHeaders = (outgoing) => {
2347
2375
  }
2348
2376
  };
2349
2377
  var responseViaCache = async (res, outgoing) => {
2350
- let [status, body, header] = res[cacheKey];
2378
+ let [status, body, header2] = res[cacheKey];
2351
2379
  let hasContentLength = false;
2352
- if (!header) {
2353
- header = { "content-type": "text/plain; charset=UTF-8" };
2354
- } else if (header instanceof Headers) {
2355
- hasContentLength = header.has("content-length");
2356
- header = buildOutgoingHttpHeaders(header);
2357
- } else if (Array.isArray(header)) {
2358
- const headerObj = new Headers(header);
2380
+ if (!header2) {
2381
+ header2 = { "content-type": "text/plain; charset=UTF-8" };
2382
+ } else if (header2 instanceof Headers) {
2383
+ hasContentLength = header2.has("content-length");
2384
+ header2 = buildOutgoingHttpHeaders(header2);
2385
+ } else if (Array.isArray(header2)) {
2386
+ const headerObj = new Headers(header2);
2359
2387
  hasContentLength = headerObj.has("content-length");
2360
- header = buildOutgoingHttpHeaders(headerObj);
2388
+ header2 = buildOutgoingHttpHeaders(headerObj);
2361
2389
  } else {
2362
- for (const key in header) {
2390
+ for (const key in header2) {
2363
2391
  if (key.length === 14 && key.toLowerCase() === "content-length") {
2364
2392
  hasContentLength = true;
2365
2393
  break;
@@ -2368,14 +2396,14 @@ var responseViaCache = async (res, outgoing) => {
2368
2396
  }
2369
2397
  if (!hasContentLength) {
2370
2398
  if (typeof body === "string") {
2371
- header["Content-Length"] = Buffer.byteLength(body);
2399
+ header2["Content-Length"] = Buffer.byteLength(body);
2372
2400
  } else if (body instanceof Uint8Array) {
2373
- header["Content-Length"] = body.byteLength;
2401
+ header2["Content-Length"] = body.byteLength;
2374
2402
  } else if (body instanceof Blob) {
2375
- header["Content-Length"] = body.size;
2403
+ header2["Content-Length"] = body.size;
2376
2404
  }
2377
2405
  }
2378
- outgoing.writeHead(status, header);
2406
+ outgoing.writeHead(status, header2);
2379
2407
  if (typeof body === "string" || body instanceof Uint8Array) {
2380
2408
  outgoing.end(body);
2381
2409
  } else if (body instanceof Blob) {
@@ -2572,6 +2600,9 @@ var PiRpc = class {
2572
2600
  options;
2573
2601
  constructor(options) {
2574
2602
  this.options = options;
2603
+ if (options.onEvent) this.listeners.add(options.onEvent);
2604
+ if (options.onUiRequest) this.uiListeners.add(options.onUiRequest);
2605
+ if (options.onExit) this.exitListeners.add(options.onExit);
2575
2606
  }
2576
2607
  get running() {
2577
2608
  return !!this.proc && !this.exited;
@@ -2584,12 +2615,16 @@ var PiRpc = class {
2584
2615
  }
2585
2616
  async start() {
2586
2617
  if (this.proc) return;
2587
- const proc = spawn(this.options.command ?? "pi", ["--mode", "rpc", ...this.options.args ?? []], {
2618
+ const command = this.options.command ?? "pi";
2619
+ const script = /\.m?js$/i.test(command);
2620
+ const proc = spawn(script ? process.execPath : command, [...script ? [command] : [], "--mode", "rpc", ...this.options.args ?? []], {
2588
2621
  cwd: this.options.cwd,
2589
2622
  env: { ...process.env, ...this.options.env },
2590
2623
  stdio: ["pipe", "pipe", "pipe"]
2591
2624
  });
2592
2625
  this.proc = proc;
2626
+ proc.stdin.on("error", () => {
2627
+ });
2593
2628
  proc.stdout.setEncoding("utf8");
2594
2629
  proc.stdout.on("data", (chunk) => {
2595
2630
  this.buffer += chunk;
@@ -2627,7 +2662,7 @@ var PiRpc = class {
2627
2662
  }
2628
2663
  }
2629
2664
  };
2630
- proc.on("exit", done);
2665
+ proc.on("close", done);
2631
2666
  proc.on("error", (e) => {
2632
2667
  this.stderr += `
2633
2668
  spawn error: ${e.message}`;
@@ -2699,7 +2734,9 @@ spawn error: ${e.message}`;
2699
2734
  const entry = this.pending.get(id);
2700
2735
  this.pending.set(id, { resolve: (d) => settle(() => resolve(d)), reject: (e) => settle(() => reject(e)) });
2701
2736
  try {
2702
- this.proc.stdin.write(payload);
2737
+ this.proc.stdin.write(payload, (error) => {
2738
+ if (error && this.pending.delete(id)) settle(() => reject(error));
2739
+ });
2703
2740
  } catch (e) {
2704
2741
  this.pending.delete(id);
2705
2742
  settle(() => reject(e));
@@ -2708,12 +2745,11 @@ spawn error: ${e.message}`;
2708
2745
  }
2709
2746
  /** Answer an extension dialog. Without an answer the extension's tool call waits forever. */
2710
2747
  async respondUi(id, response) {
2711
- if (!this.proc || this.exited) return;
2712
- const record = { type: "extension_ui_response", id, ...response };
2713
- try {
2714
- this.proc.stdin.write(JSON.stringify(record) + "\n");
2715
- } catch {
2716
- }
2748
+ if (!this.proc || this.exited) throw new Error("Pi is not running. Retry the agent before answering.");
2749
+ const record = { ...response, type: "extension_ui_response", id };
2750
+ await new Promise((resolve, reject) => {
2751
+ this.proc.stdin.write(JSON.stringify(record) + "\n", (error) => error ? reject(error) : resolve());
2752
+ });
2717
2753
  }
2718
2754
  prompt(message, behavior) {
2719
2755
  return this.send({ type: "prompt", message, ...behavior ? { streamingBehavior: behavior } : {} });
@@ -2732,9 +2768,8 @@ spawn error: ${e.message}`;
2732
2768
  proc.kill("SIGKILL");
2733
2769
  } catch {
2734
2770
  }
2735
- resolve();
2736
2771
  }, 3e3);
2737
- proc.once("exit", () => {
2772
+ proc.once("close", () => {
2738
2773
  clearTimeout(t);
2739
2774
  resolve();
2740
2775
  });
@@ -3069,6 +3104,13 @@ function itemsFromMessages(messages) {
3069
3104
  }
3070
3105
  return items;
3071
3106
  }
3107
+ function readSessionMessages(file) {
3108
+ try {
3109
+ return fs2.readFileSync(file, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l)).filter((e) => e.type === "message" && e.message).map((e) => e.message);
3110
+ } catch {
3111
+ return [];
3112
+ }
3113
+ }
3072
3114
  var Chat = class {
3073
3115
  id;
3074
3116
  options;
@@ -3079,6 +3121,8 @@ var Chat = class {
3079
3121
  live = null;
3080
3122
  liveLoops = /* @__PURE__ */ new Map();
3081
3123
  dialogs = [];
3124
+ answeringUi = /* @__PURE__ */ new Set();
3125
+ dialogTimers = /* @__PURE__ */ new Map();
3082
3126
  notices = [];
3083
3127
  runs = [];
3084
3128
  model = null;
@@ -3086,6 +3130,9 @@ var Chat = class {
3086
3130
  refreshing = null;
3087
3131
  lastBroadcast = 0;
3088
3132
  stderr = "";
3133
+ exitError = "";
3134
+ refreshError = "";
3135
+ stopping = false;
3089
3136
  file = null;
3090
3137
  sessionName = null;
3091
3138
  autoNamed = false;
@@ -3095,14 +3142,22 @@ var Chat = class {
3095
3142
  this.model = options.model ?? null;
3096
3143
  this.pi = new PiRpc({
3097
3144
  command: options.piCommand,
3098
- args: ["--session-dir", options.sessionDir, "--session-id", id, ...options.args ?? [], ...options.model ? ["--model", options.model] : []],
3145
+ args: ["--session-dir", options.sessionDir, ...options.sessionFile ? ["--session", options.sessionFile] : ["--session-id", id], ...options.args ?? [], ...options.model ? ["--model", options.model] : []],
3099
3146
  cwd: options.cwd,
3100
3147
  onEvent: (event) => this.onEvent(event),
3101
- onExit: (code) => {
3102
- this.stderr = this.pi.stderrText;
3148
+ onExit: (code, stderr) => {
3149
+ this.stderr = stderr;
3150
+ const hadDialog = this.dialogs.length > 0;
3151
+ this.dialogs = [];
3152
+ for (const { timer } of this.dialogTimers.values()) clearTimeout(timer);
3153
+ this.dialogTimers.clear();
3103
3154
  this.live = null;
3155
+ if (!this.stopping) {
3156
+ const diagnostic = stderr.split("\n").map((l) => l.trim()).filter((l) => l && !/^warn(ing)?\b/i.test(l)).slice(-3).join(" \xB7 ").slice(-300);
3157
+ this.exitError = hadDialog ? "Pi exited while an extension decision was pending. Retry the agent; if you selected a custom Pi executable, use the default Loops runner for startup-dialog support." : code === 0 ? "Pi exited unexpectedly." : `Pi exited (code ${code ?? "unknown"}).`;
3158
+ if (diagnostic) this.exitError += ` ${diagnostic}`;
3159
+ }
3104
3160
  this.broadcast(true);
3105
- if (code != null) this.touch();
3106
3161
  }
3107
3162
  });
3108
3163
  }
@@ -3133,10 +3188,21 @@ var Chat = class {
3133
3188
  async start() {
3134
3189
  if (this.started) return;
3135
3190
  this.started = true;
3136
- await this.pi.start();
3191
+ let signalDialog;
3192
+ const dialogReady = new Promise((resolve) => {
3193
+ signalDialog = resolve;
3194
+ });
3137
3195
  this.pi.onUiRequest((request) => {
3138
- if (["select", "confirm", "input", "editor"].includes(request.method)) this.dialogs.push(request);
3139
- else if (request.method === "notify") {
3196
+ if (["select", "confirm", "input", "editor"].includes(request.method)) {
3197
+ this.dialogs.push(request);
3198
+ if (typeof request.timeout === "number" && Number.isFinite(request.timeout) && request.timeout > 0) {
3199
+ const timeout = Math.min(request.timeout, 2 ** 31 - 1);
3200
+ const timer = setTimeout(() => this.closeDialog(request.id), timeout);
3201
+ timer.unref();
3202
+ this.dialogTimers.set(request.id, { timer, deadline: Date.now() + timeout });
3203
+ }
3204
+ signalDialog();
3205
+ } else if (request.method === "notify") {
3140
3206
  console.log(`[${this.id}] ${request.level ?? "info"}: ${request.message ?? ""}`);
3141
3207
  this.notices.push({ t: Date.now(), level: String(request.level ?? "info"), message: String(request.message ?? "") });
3142
3208
  this.notices = this.notices.slice(-20);
@@ -3144,13 +3210,15 @@ var Chat = class {
3144
3210
  void this.refresh();
3145
3211
  this.broadcast(true);
3146
3212
  });
3213
+ await this.pi.start();
3147
3214
  if (!this.pi.running) {
3148
3215
  this.stderr = this.pi.stderrText;
3149
3216
  return;
3150
3217
  }
3151
- await this.refresh(true);
3218
+ await Promise.race([this.refresh(true), dialogReady]);
3152
3219
  }
3153
3220
  async stop() {
3221
+ this.stopping = true;
3154
3222
  await this.pi.stop();
3155
3223
  }
3156
3224
  /** Re-read the session's entries (cursor-based) and messages, then rebuild the runs. */
@@ -3178,9 +3246,10 @@ var Chat = class {
3178
3246
  await this.rename(auto).catch(() => {
3179
3247
  });
3180
3248
  }
3249
+ this.refreshError = "";
3181
3250
  } catch (e) {
3182
3251
  console.error(`[${this.id}] refresh failed:`, e.message ?? e);
3183
- this.stderr = String(e.message ?? e);
3252
+ this.refreshError = String(e.message ?? e);
3184
3253
  } finally {
3185
3254
  this.refreshing = null;
3186
3255
  this.broadcast(true);
@@ -3235,6 +3304,8 @@ var Chat = class {
3235
3304
  void this.refresh();
3236
3305
  this.broadcast(true);
3237
3306
  } else if (type === "extension_ui_request") {
3307
+ } else if (type === "extension_ui_request_cancelled") {
3308
+ this.closeDialog(String(event.id));
3238
3309
  } else if (type.startsWith("session_") || type === "compaction_start" || type === "compaction_end") {
3239
3310
  if (type === "session_idle" || type === "session_start") void this.refresh(true);
3240
3311
  }
@@ -3265,11 +3336,36 @@ var Chat = class {
3265
3336
  } catch {
3266
3337
  }
3267
3338
  }
3268
- async answerUi(id, response) {
3339
+ closeDialog(id) {
3340
+ const timed = this.dialogTimers.get(id);
3341
+ if (timed) {
3342
+ clearTimeout(timed.timer);
3343
+ this.dialogTimers.delete(id);
3344
+ }
3269
3345
  this.dialogs = this.dialogs.filter((d) => d.id !== id);
3270
- await this.pi.respondUi(id, response);
3271
3346
  this.broadcast(true);
3272
3347
  }
3348
+ async answerUi(id, response) {
3349
+ if (!this.isUp) throw new Error("Pi is not running. Retry the agent before answering.");
3350
+ if ((this.dialogTimers.get(id)?.deadline ?? Infinity) <= Date.now()) this.closeDialog(id);
3351
+ const dialog = this.dialogs.find((d) => d.id === id);
3352
+ if (!dialog) throw new Error("This decision is no longer pending. Refresh the chat.");
3353
+ if (this.answeringUi.has(id)) throw new Error("An answer to this decision is already being submitted.");
3354
+ if (!response || typeof response !== "object" || Array.isArray(response)) throw new Error("Invalid decision response.");
3355
+ if (response.cancelled !== true) {
3356
+ if (dialog.method === "select" && (typeof response.value !== "string" || !dialog.options?.includes(response.value))) throw new Error("Choose one of the available options.");
3357
+ if (dialog.method === "confirm" && typeof response.confirmed !== "boolean") throw new Error("A confirmation is required.");
3358
+ if (["input", "editor"].includes(dialog.method) && typeof response.value !== "string") throw new Error("A text response is required.");
3359
+ }
3360
+ this.answeringUi.add(id);
3361
+ try {
3362
+ await this.pi.respondUi(id, response);
3363
+ if (!this.isUp || !this.dialogs.some((d) => d.id === id)) throw new Error("This decision is no longer pending. Refresh the chat.");
3364
+ this.closeDialog(id);
3365
+ } finally {
3366
+ this.answeringUi.delete(id);
3367
+ }
3368
+ }
3273
3369
  async abort() {
3274
3370
  await this.pi.send({ type: "abort" }).catch(() => {
3275
3371
  });
@@ -3341,7 +3437,7 @@ var Chat = class {
3341
3437
  live: this.live,
3342
3438
  runs,
3343
3439
  model: this.model,
3344
- error: this.pi.running ? void 0 : this.stderr || void 0,
3440
+ error: this.pi.running ? this.refreshError || void 0 : this.exitError || this.refreshError || void 0,
3345
3441
  questions: runs.flatMap((r) => r.questions),
3346
3442
  dialogs: this.dialogs,
3347
3443
  notices: this.notices.slice(-6),
@@ -3357,11 +3453,17 @@ function listChats(sessionDir, cwd = process.cwd()) {
3357
3453
  const full = path2.join(sessionDir, f);
3358
3454
  let title;
3359
3455
  let firstUser;
3456
+ let recordedCwd;
3457
+ let sessionId;
3360
3458
  const entries = [];
3361
3459
  try {
3362
3460
  for (const line of fs2.readFileSync(full, "utf8").split("\n")) {
3363
3461
  if (!line.trim()) continue;
3364
3462
  const e = JSON.parse(line);
3463
+ if (e.type === "session" && typeof e.cwd === "string") {
3464
+ recordedCwd = e.cwd;
3465
+ sessionId = e.id;
3466
+ }
3365
3467
  entries.push(e);
3366
3468
  if ((e.type === "session_name" || e.type === "name") && e.name) title = String(e.name);
3367
3469
  if (!firstUser && e.type === "message" && e.message?.role === "user") {
@@ -3372,15 +3474,23 @@ function listChats(sessionDir, cwd = process.cwd()) {
3372
3474
  } catch {
3373
3475
  }
3374
3476
  summary = summarize(cwd, entries);
3375
- return { id: path2.basename(f, ".jsonl").replace(/^.*_/, ""), last: fs2.statSync(full).mtimeMs, title: title ?? (firstUser ? firstUser.replace(/\s+/g, " ").slice(0, 46) : void 0), summary };
3477
+ return { id: path2.basename(f, ".jsonl").replace(/^.*_/, ""), last: fs2.statSync(full).mtimeMs, title: title ?? (firstUser ? firstUser.replace(/\s+/g, " ").slice(0, 46) : void 0), summary, cwd: recordedCwd, sessionId };
3376
3478
  });
3377
3479
  } catch {
3378
3480
  return [];
3379
3481
  }
3380
- return files.filter((f) => /^chat-/.test(f.id)).sort((a, b) => b.last - a.last).map((f) => ({ id: f.id, title: f.title ?? goalName("New chat"), ts: f.last, last: f.last, summary: f.summary }));
3482
+ const canonical2 = (p) => {
3483
+ try {
3484
+ return fs2.realpathSync(p);
3485
+ } catch {
3486
+ return path2.resolve(p);
3487
+ }
3488
+ };
3489
+ return files.filter((f) => /^chat-/.test(f.id) && (f.sessionId === void 0 || f.sessionId === f.id) && f.cwd && canonical2(f.cwd) === canonical2(cwd)).sort((a, b) => b.last - a.last).map((f) => ({ id: f.id, title: f.title ?? goalName("New chat"), ts: f.last, last: f.last, summary: f.summary }));
3381
3490
  }
3382
3491
  var ChatManager = class {
3383
3492
  chats = /* @__PURE__ */ new Map();
3493
+ operations = /* @__PURE__ */ new Map();
3384
3494
  listeners = /* @__PURE__ */ new Set();
3385
3495
  base;
3386
3496
  /** Where a chat lives. Set by the server from the workspace registry, which owns the persistence. */
@@ -3404,33 +3514,66 @@ var ChatManager = class {
3404
3514
  get(id) {
3405
3515
  return this.chats.get(id) ?? null;
3406
3516
  }
3407
- async open(id) {
3408
- const existing = this.chats.get(id);
3409
- if (existing?.isUp) {
3410
- await existing.start();
3411
- return existing;
3517
+ async serialize(id, operation) {
3518
+ const result = (this.operations.get(id) ?? Promise.resolve()).then(operation);
3519
+ const settled = result.then(() => {
3520
+ }, () => {
3521
+ });
3522
+ this.operations.set(id, settled);
3523
+ try {
3524
+ return await result;
3525
+ } finally {
3526
+ if (this.operations.get(id) === settled) this.operations.delete(id);
3412
3527
  }
3413
- const where = this.bind(id);
3414
- const chat = new Chat(id, { ...this.base, ...where, onUpdate: (c) => {
3415
- for (const fn of this.listeners) {
3416
- try {
3417
- fn(c);
3418
- } catch {
3528
+ }
3529
+ open(id) {
3530
+ return this.serialize(id, async () => {
3531
+ const where = this.bind(id);
3532
+ const existing = this.chats.get(id);
3533
+ if (existing && existing.workspacePath !== where.cwd) throw new Error("chat directory does not match its Pi session");
3534
+ if (existing) return existing;
3535
+ const chat = new Chat(id, { ...this.base, ...where, onUpdate: (c) => {
3536
+ for (const fn of this.listeners) {
3537
+ try {
3538
+ fn(c);
3539
+ } catch {
3540
+ }
3419
3541
  }
3420
- }
3421
- } });
3422
- this.chats.set(id, chat);
3423
- await chat.start();
3424
- return chat;
3542
+ } });
3543
+ this.chats.set(id, chat);
3544
+ await chat.start();
3545
+ return chat;
3546
+ });
3547
+ }
3548
+ retry(id) {
3549
+ return this.serialize(id, async () => {
3550
+ const where = this.bind(id);
3551
+ const existing = this.chats.get(id);
3552
+ if (existing?.isUp) throw new Error("chat is already running");
3553
+ await existing?.stop();
3554
+ const chat = new Chat(id, { ...this.base, ...where, onUpdate: (c) => {
3555
+ for (const fn of this.listeners) {
3556
+ try {
3557
+ fn(c);
3558
+ } catch {
3559
+ }
3560
+ }
3561
+ } });
3562
+ this.chats.set(id, chat);
3563
+ await chat.start();
3564
+ return chat;
3565
+ });
3425
3566
  }
3426
- async close(id) {
3427
- const chat = this.chats.get(id);
3428
- this.chats.delete(id);
3429
- await chat?.stop();
3567
+ close(id, afterClose) {
3568
+ return this.serialize(id, async () => {
3569
+ const chat = this.chats.get(id);
3570
+ await chat?.stop();
3571
+ this.chats.delete(id);
3572
+ afterClose?.();
3573
+ });
3430
3574
  }
3431
3575
  async closeAll() {
3432
- await Promise.all([...this.chats.values()].map((c) => c.stop()));
3433
- this.chats.clear();
3576
+ await Promise.all([.../* @__PURE__ */ new Set([...this.chats.keys(), ...this.operations.keys()])].map((id) => this.close(id)));
3434
3577
  }
3435
3578
  openChats() {
3436
3579
  return [...this.chats.values()];
@@ -4184,13 +4327,52 @@ import fs7 from "node:fs";
4184
4327
  import path7 from "node:path";
4185
4328
  import { execFileSync } from "node:child_process";
4186
4329
  var slug = (p) => path7.basename(p).replace(/[^a-z0-9._-]+/gi, "-").toLowerCase() || "repo";
4330
+ var canonical = (p) => {
4331
+ const abs = path7.resolve(p);
4332
+ try {
4333
+ return fs7.realpathSync(abs);
4334
+ } catch {
4335
+ return abs;
4336
+ }
4337
+ };
4338
+ var available = (p) => {
4339
+ try {
4340
+ return fs7.statSync(p).isDirectory();
4341
+ } catch {
4342
+ return false;
4343
+ }
4344
+ };
4345
+ function header(file) {
4346
+ const fd = fs7.openSync(file, "r");
4347
+ try {
4348
+ const chunks = [];
4349
+ let size = 0;
4350
+ while (size < 1024 * 1024) {
4351
+ const buf = Buffer.alloc(4096);
4352
+ const n = fs7.readSync(fd, buf, 0, buf.length, null);
4353
+ if (!n) break;
4354
+ const end = buf.subarray(0, n).indexOf(10);
4355
+ chunks.push(buf.subarray(0, end < 0 ? n : end));
4356
+ size += end < 0 ? n : end;
4357
+ if (end >= 0) break;
4358
+ }
4359
+ if (size >= 1024 * 1024) throw new Error("session header too large");
4360
+ const result = JSON.parse(Buffer.concat(chunks).toString("utf8"));
4361
+ if (typeof result.cwd !== "string" || !result.cwd || result.id !== void 0 && typeof result.id !== "string") throw new Error("invalid session header");
4362
+ return result;
4363
+ } finally {
4364
+ fs7.closeSync(fd);
4365
+ }
4366
+ }
4187
4367
  var Workspaces = class {
4188
- /** Where workspaces.json and the per-repo session dirs live. */
4189
4368
  root;
4190
4369
  file;
4191
4370
  data;
4192
4371
  byId = /* @__PURE__ */ new Map();
4193
4372
  chatMap;
4373
+ files = /* @__PURE__ */ new Map();
4374
+ // A new chat may not have written its first session file yet. This is never persisted.
4375
+ pending = /* @__PURE__ */ new Set();
4194
4376
  constructor({ root, cwd }) {
4195
4377
  this.root = root;
4196
4378
  this.file = path7.join(root, "workspaces.json");
@@ -4198,139 +4380,185 @@ var Workspaces = class {
4198
4380
  let data = { workspaces: [], chats: {} };
4199
4381
  if (fs7.existsSync(this.file)) {
4200
4382
  try {
4201
- data = JSON.parse(fs7.readFileSync(this.file, "utf8"));
4383
+ const parsed = JSON.parse(fs7.readFileSync(this.file, "utf8"));
4384
+ if (!Array.isArray(parsed.workspaces) || !parsed.chats || typeof parsed.chats !== "object" || Array.isArray(parsed.chats)) throw new Error("invalid registry");
4385
+ data = parsed;
4202
4386
  } catch {
4203
- try {
4204
- fs7.renameSync(this.file, `${this.file}.corrupt-${Date.now()}`);
4205
- } catch {
4206
- }
4207
- console.error(`[workspaces] could not read ${this.file}; kept a copy and rebuilding from sessions`);
4387
+ fs7.renameSync(this.file, `${this.file}.corrupt-${Date.now()}`);
4208
4388
  }
4209
4389
  }
4210
- this.data = { workspaces: data.workspaces ?? [], chats: data.chats ?? {}, default: data.default };
4211
- this.chatMap = new Map(Object.entries(this.data.chats));
4212
- const first = this.add(cwd, { persist: false });
4213
- this.restore();
4214
- if (!this.data.default || !this.byId.has(this.data.default)) this.data.default = first.id;
4390
+ this.data = { version: 2, workspaces: [], chats: {}, default: data.default };
4391
+ this.chatMap = new Map(Object.entries(data.chats));
4392
+ if (data.files && typeof data.files === "object") {
4393
+ for (const [chat, files] of Object.entries(data.files)) {
4394
+ if (Array.isArray(files) && files.every((f) => typeof f === "string")) this.files.set(chat, new Set(files));
4395
+ }
4396
+ }
4397
+ for (const w of data.workspaces) {
4398
+ if (!w || typeof w.id !== "string" || typeof w.path !== "string" || this.byId.has(w.id)) continue;
4399
+ const p = canonical(w.path);
4400
+ const legacy = path7.join(p, ".pi", "loops-ui", "sessions");
4401
+ const sessionDir = w.sessionDir ?? (fs7.existsSync(legacy) ? legacy : path7.join(root, "workspaces", w.id, "sessions"));
4402
+ this.byId.set(w.id, { id: w.id, path: p, sessionDir, title: path7.basename(p) || p, open: w.open !== false, available: available(p) });
4403
+ }
4404
+ this.recoverSessions(void 0, data.version !== 2);
4405
+ if (cwd && ![...this.byId.values()].some((w) => w.path === canonical(cwd))) {
4406
+ this.add(cwd);
4407
+ }
4215
4408
  this.save();
4216
4409
  }
4217
4410
  save() {
4411
+ this.data.workspaces = [...this.byId.values()].map(({ id, path: p, sessionDir, open }) => ({ id, path: p, sessionDir, open }));
4218
4412
  this.data.chats = Object.fromEntries(this.chatMap);
4219
- try {
4220
- const tmp = `${this.file}.tmp`;
4221
- fs7.writeFileSync(tmp, JSON.stringify(this.data, null, 2) + "\n");
4222
- fs7.renameSync(tmp, this.file);
4223
- } catch {
4224
- }
4413
+ this.data.files = Object.fromEntries([...this.files].map(([chat, files]) => [chat, [...files]]));
4414
+ const tmp = `${this.file}.tmp`;
4415
+ fs7.writeFileSync(tmp, JSON.stringify(this.data, null, 2) + "\n");
4416
+ fs7.renameSync(tmp, this.file);
4225
4417
  }
4226
4418
  get defaultId() {
4227
- return this.data.default ?? [...this.byId.keys()][0] ?? "";
4419
+ return this.get()?.id ?? "";
4228
4420
  }
4229
4421
  list() {
4230
- return [...this.byId.values()];
4422
+ return [...this.byId.values()].map((w) => ({ ...w, available: available(w.path) }));
4231
4423
  }
4232
4424
  get(id) {
4233
- return this.byId.get(id ?? "") ?? this.byId.get(this.defaultId) ?? null;
4425
+ const w = id != null ? this.byId.get(id) : this.byId.get(this.data.default ?? "")?.open ? this.byId.get(this.data.default) : [...this.byId.values()].find((x) => x.open);
4426
+ return w ? { ...w, available: available(w.path) } : null;
4234
4427
  }
4235
- /** Add (or re-find) a repo. `persist: false` is used for the startup cwd. */
4236
- add(dir, { persist = true } = {}) {
4237
- const abs = path7.resolve(dir);
4238
- const existing = [...this.byId.values()].find((w) => w.path === abs);
4428
+ register(p, sessionDir, preferredId) {
4429
+ const existing = [...this.byId.values()].find((w) => w.path === p);
4239
4430
  if (existing) return existing;
4240
- if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) throw new Error(`${abs} is not a directory`);
4241
- const st = fs7.statSync(abs);
4242
- if (!st.isDirectory()) throw new Error(`${abs} is not a directory`);
4243
- let id = slug(abs);
4244
- for (let i = 2; this.byId.has(id); i++) id = `${slug(abs)}-${i}`;
4245
- const legacy = path7.join(abs, ".pi", "loops-ui", "sessions");
4246
- const sessionDir = fs7.existsSync(legacy) ? legacy : path7.join(this.root, "workspaces", id, "sessions");
4247
- const ws = { id, path: abs, sessionDir, title: path7.basename(abs) || abs };
4248
- fs7.mkdirSync(ws.sessionDir, { recursive: true });
4431
+ let id = preferredId && !this.byId.has(preferredId) ? preferredId : slug(p);
4432
+ for (let i = 2; this.byId.has(id); i++) id = `${slug(p)}-${i}`;
4433
+ const legacy = path7.join(p, ".pi", "loops-ui", "sessions");
4434
+ const ws = { id, path: p, sessionDir: sessionDir ?? (fs7.existsSync(legacy) ? legacy : path7.join(this.root, "workspaces", id, "sessions")), title: path7.basename(p) || p, open: true, available: available(p) };
4249
4435
  this.byId.set(id, ws);
4250
- if (persist) {
4251
- this.data.workspaces = [...this.data.workspaces.filter((w) => w.path !== abs), { id, path: abs }];
4252
- this.save();
4253
- }
4254
4436
  return ws;
4255
4437
  }
4256
- /** Register the persisted workspaces (their paths may have been deleted since), then rebuild from Pi sessions. */
4438
+ add(dir) {
4439
+ const p = canonical(dir);
4440
+ if (!available(p)) throw new Error(`${p} is not a directory`);
4441
+ const ws = this.register(p);
4442
+ ws.open = true;
4443
+ fs7.mkdirSync(ws.sessionDir, { recursive: true });
4444
+ if (!this.data.default || !this.byId.get(this.data.default)?.open) this.data.default = ws.id;
4445
+ this.recoverSessions([ws.sessionDir], false);
4446
+ this.save();
4447
+ return this.get(ws.id);
4448
+ }
4449
+ /** Re-scan sessions, including legacy session directories. */
4257
4450
  restore() {
4258
- for (const w of this.data.workspaces) {
4259
- try {
4260
- this.add(w.path);
4261
- } catch {
4262
- }
4263
- }
4264
- this.recoverSessions();
4451
+ this.recoverSessions(void 0, false);
4265
4452
  this.save();
4266
4453
  }
4267
- /**
4268
- * Chat -> repo is recoverable from Pi itself: each session header carries `cwd`, and the session dir names the
4269
- * workspace. This runs on startup, so a lost or stale `workspaces.json` is a cache miss, not lost history.
4270
- */
4271
- recoverSessions() {
4272
- const root = path7.join(this.root, "workspaces");
4273
- let ids = [];
4274
- try {
4275
- ids = fs7.readdirSync(root);
4276
- } catch {
4277
- return;
4278
- }
4279
- for (const id of ids) {
4280
- const dir = path7.join(root, id, "sessions");
4281
- let files = [];
4454
+ recoverSessions(onlyDirs, rebind = true) {
4455
+ const dirs = new Set(onlyDirs ?? [...this.byId.values()].map((w) => w.sessionDir));
4456
+ const base = path7.join(this.root, "workspaces");
4457
+ if (!onlyDirs && fs7.existsSync(base)) for (const id of fs7.readdirSync(base)) dirs.add(path7.join(base, id, "sessions"));
4458
+ for (const dir of dirs) {
4459
+ let names;
4282
4460
  try {
4283
- files = fs7.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
4461
+ names = fs7.readdirSync(dir).filter((n) => n.endsWith(".jsonl"));
4284
4462
  } catch {
4285
4463
  continue;
4286
4464
  }
4287
- let cwd = null;
4288
- for (const f of files) {
4289
- try {
4290
- const head = JSON.parse(fs7.readFileSync(path7.join(dir, f), "utf8").split("\n")[0]);
4291
- if (typeof head?.cwd === "string" && head.cwd) {
4292
- cwd = head.cwd;
4293
- break;
4294
- }
4295
- } catch {
4296
- }
4297
- }
4298
- let ws = this.byId.get(id) ?? null;
4299
- if (!ws && cwd) {
4465
+ for (const name of names) {
4466
+ const chat = path7.basename(name, ".jsonl").replace(/^.*_/, "");
4467
+ if (!chat.startsWith("chat-")) continue;
4468
+ const file = path7.join(dir, name);
4469
+ const files = this.files.get(chat) ?? /* @__PURE__ */ new Set();
4470
+ files.add(file);
4471
+ this.files.set(chat, files);
4472
+ let cwd;
4300
4473
  try {
4301
- ws = this.add(cwd);
4302
- if (ws.id !== id) ws.sessionDir = dir;
4474
+ const head = header(file);
4475
+ if (head.id !== void 0 && head.id !== chat) continue;
4476
+ cwd = canonical(head.cwd);
4303
4477
  } catch {
4478
+ continue;
4304
4479
  }
4305
- }
4306
- if (!ws) continue;
4307
- for (const f of files) {
4308
- const chat = f.replace(/\.jsonl$/, "").replace(/^.*_/, "");
4309
- if (/^chat-/.test(chat) && !this.chatMap.has(chat)) this.chatMap.set(chat, ws.id);
4480
+ const parent = path7.dirname(dir);
4481
+ const preferred = path7.dirname(parent) === base ? path7.basename(parent) : void 0;
4482
+ const ws = this.register(cwd, dir, preferred);
4483
+ if (rebind || !this.chatMap.has(chat)) this.chatMap.set(chat, ws.id);
4310
4484
  }
4311
4485
  }
4312
4486
  }
4313
4487
  remove(id) {
4314
- if (id === this.defaultId) throw new Error("cannot remove the workspace this app was started in");
4315
- this.byId.delete(id);
4316
- this.data.workspaces = this.data.workspaces.filter((w) => w.id !== id);
4317
- for (const [chat, ws] of this.chatMap) if (ws === id) this.chatMap.delete(chat);
4488
+ const ws = this.byId.get(id);
4489
+ if (!ws) throw new Error(`unknown workspace: ${id}`);
4490
+ ws.open = false;
4318
4491
  this.save();
4319
4492
  }
4320
- /** Which workspace a chat belongs to. Unknown ids fall back to the default (older chats predate the registry). */
4493
+ hasChat(chat) {
4494
+ return this.chatMap.has(chat) && this.byId.has(this.chatMap.get(chat));
4495
+ }
4321
4496
  ofChat(chat) {
4322
- return this.get(this.chatMap.get(chat)) ?? this.get();
4497
+ const ws = this.get(this.chatMap.get(chat) ?? "");
4498
+ if (!ws) throw new Error(`unknown chat: ${chat}`);
4499
+ const files = this.candidates(chat, ws);
4500
+ if (files.length === 0 && !this.pending.has(chat)) throw new Error(`missing session for chat: ${chat}`);
4501
+ if (files.length > 1) throw new Error(`ambiguous session for chat: ${chat}`);
4502
+ if (files.length === 1 && !this.validFile(files[0], chat, ws)) throw new Error(`invalid session for chat: ${chat}`);
4503
+ return ws;
4323
4504
  }
4324
4505
  bindChat(chat, ws) {
4325
- if (this.byId.has(ws)) {
4326
- this.chatMap.set(chat, ws);
4506
+ if (!this.byId.has(ws)) throw new Error(`unknown workspace: ${ws}`);
4507
+ const existing = this.chatMap.get(chat);
4508
+ if (existing && existing !== ws) throw new Error(`chat ${chat} is already bound to ${existing}`);
4509
+ if (!existing) this.pending.add(chat);
4510
+ this.chatMap.set(chat, ws);
4511
+ this.save();
4512
+ }
4513
+ unbindChat(chat) {
4514
+ if (this.chatMap.delete(chat)) {
4515
+ this.pending.delete(chat);
4516
+ this.files.delete(chat);
4327
4517
  this.save();
4328
4518
  }
4329
4519
  }
4330
- unbindChat(chat) {
4331
- if (this.chatMap.delete(chat)) this.save();
4520
+ /** All known session directories for this workspace, including recovered secondary directories. */
4521
+ sessionDirs(id) {
4522
+ const ws = this.get(id);
4523
+ if (!ws) return [];
4524
+ const dirs = /* @__PURE__ */ new Set([ws.sessionDir]);
4525
+ for (const [chat, files] of this.files) {
4526
+ if (this.chatMap.get(chat) === id) for (const file of files) dirs.add(path7.dirname(file));
4527
+ }
4528
+ return [...dirs];
4529
+ }
4530
+ candidates(chat, ws) {
4531
+ const files = new Set(this.files.get(chat) ?? []);
4532
+ const oldSize = files.size;
4533
+ for (const dir of this.sessionDirs(ws.id)) {
4534
+ try {
4535
+ for (const name of fs7.readdirSync(dir)) {
4536
+ if (name.endsWith(".jsonl") && path7.basename(name, ".jsonl").replace(/^.*_/, "") === chat) files.add(path7.join(dir, name));
4537
+ }
4538
+ } catch {
4539
+ }
4540
+ }
4541
+ if (files.size) this.pending.delete(chat);
4542
+ if (files.size !== oldSize) {
4543
+ this.files.set(chat, files);
4544
+ this.save();
4545
+ }
4546
+ return [...files];
4547
+ }
4548
+ validFile(file, chat, ws) {
4549
+ try {
4550
+ const h = header(file);
4551
+ return (h.id === void 0 || h.id === chat) && canonical(h.cwd) === ws.path;
4552
+ } catch {
4553
+ return false;
4554
+ }
4555
+ }
4556
+ sessionFile(chat) {
4557
+ if (!this.hasChat(chat)) return null;
4558
+ const ws = this.get(this.chatMap.get(chat));
4559
+ const files = this.candidates(chat, ws);
4560
+ return files.length === 1 && this.validFile(files[0], chat, ws) ? files[0] : null;
4332
4561
  }
4333
- /** A repo is loopable when git can name a commit in it; that is what the sidebar shows. */
4334
4562
  static loopable(dir) {
4335
4563
  try {
4336
4564
  execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, stdio: "pipe" });
@@ -4341,9 +4569,48 @@ var Workspaces = class {
4341
4569
  }
4342
4570
  };
4343
4571
 
4572
+ // server/directories.ts
4573
+ import fs8 from "node:fs/promises";
4574
+ import os4 from "node:os";
4575
+ import path8 from "node:path";
4576
+ import { execFile } from "node:child_process";
4577
+ function directoryPath(value) {
4578
+ if (typeof value !== "string" || !value.trim()) throw new Error("an absolute directory path is required");
4579
+ const expanded = value.trim().replace(/^~(?=\/|$)/, os4.homedir());
4580
+ if (!path8.isAbsolute(expanded)) throw new Error("use an absolute path on the server, or ~/ for its home directory");
4581
+ return expanded;
4582
+ }
4583
+ async function browseDirectories(value, hidden = false) {
4584
+ const requested = value === void 0 ? os4.homedir() : directoryPath(value);
4585
+ const dir = await fs8.realpath(requested);
4586
+ const entries = [];
4587
+ let scanned = 0, truncated = false;
4588
+ const handle = await fs8.opendir(dir);
4589
+ for await (const entry of handle) {
4590
+ if (++scanned > 1e4) {
4591
+ truncated = true;
4592
+ break;
4593
+ }
4594
+ if (!hidden && entry.name.startsWith(".")) continue;
4595
+ const full = path8.join(dir, entry.name);
4596
+ const symlink = entry.isSymbolicLink();
4597
+ if (!entry.isDirectory() && !(symlink && await fs8.stat(full).then((s) => s.isDirectory(), () => false))) continue;
4598
+ entries.push({ name: entry.name, path: full, symlink });
4599
+ if (entries.length >= 500) {
4600
+ truncated = true;
4601
+ break;
4602
+ }
4603
+ }
4604
+ entries.sort((a, b) => a.name.localeCompare(b.name));
4605
+ const loopable = await new Promise((resolve) => {
4606
+ execFile("git", ["rev-parse", "--verify", "HEAD"], { cwd: dir, timeout: 2e3 }, (e) => resolve(!e));
4607
+ });
4608
+ return { path: dir, parent: path8.dirname(dir) === dir ? null : path8.dirname(dir), home: os4.homedir(), entries, truncated, loopable };
4609
+ }
4610
+
4344
4611
  // server/index.ts
4345
- var HERE = path8.dirname(fileURLToPath(import.meta.url));
4346
- var APP_ROOT = path8.resolve(HERE, "..");
4612
+ var HERE = path9.dirname(fileURLToPath(import.meta.url));
4613
+ var APP_ROOT = path9.resolve(HERE, "..");
4347
4614
  var arg = (name, fallback) => {
4348
4615
  const i = process.argv.indexOf(`--${name}`);
4349
4616
  return i >= 0 ? process.argv[i + 1] : process.env[`LOOPS_UI_${name.toUpperCase().replace(/-/g, "_")}`] ?? fallback;
@@ -4351,16 +4618,24 @@ var arg = (name, fallback) => {
4351
4618
  var PORT = Number(arg("port", "8001"));
4352
4619
  var HOST = arg("host", "127.0.0.1");
4353
4620
  var LOOPBACK = ["127.0.0.1", "localhost", "::1"].includes(HOST);
4354
- var CWD = path8.resolve(arg("cwd", process.cwd()));
4621
+ var PUBLIC_ORIGINS = [...new Set((arg("public-origin", "") ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
4622
+ const url = new URL(s);
4623
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
4624
+ throw new Error(`--public-origin must contain HTTP(S) origins, not paths or credentials: ${s}`);
4625
+ }
4626
+ return url.origin;
4627
+ }))];
4628
+ var SEED = arg("cwd");
4629
+ var CWD = SEED ? path9.resolve(SEED) : null;
4355
4630
  var DEV = process.argv.includes("--dev") || process.env.NODE_ENV === "development";
4356
4631
  var VITE = arg("vite", "http://localhost:5173");
4357
- var PI = arg("pi", "pi");
4358
- var DATA = path8.resolve(arg("data", process.env.LOOPS_UI_DATA ?? path8.join(process.env.HOME ?? process.cwd(), ".pi", "loops-ui")));
4632
+ var PI = arg("pi", path9.join(APP_ROOT, "bin", "pi-rpc.mjs"));
4633
+ var DATA = path9.resolve(arg("data", process.env.LOOPS_UI_DATA ?? path9.join(process.env.HOME ?? process.cwd(), ".pi", "loops-ui")));
4359
4634
  var MODEL = arg("model", void 0) || null;
4360
- var EXTENSIONS = (arg("extensions", path8.resolve(APP_ROOT, "../pi-loops/extensions/loop.ts")) ?? "").split(",").filter(Boolean);
4635
+ var EXTENSIONS = (arg("extensions", path9.resolve(APP_ROOT, "../pi-loops/extensions/loop.ts")) ?? "").split(",").filter(Boolean);
4361
4636
  function prerequisites() {
4362
4637
  const out = [];
4363
- const onPath = PI.includes("/") || /^[A-Za-z]:\\/.test(PI) ? fs8.existsSync(PI) : (() => {
4638
+ const onPath = PI.includes("/") || /^[A-Za-z]:\\/.test(PI) ? fs9.existsSync(PI) : (() => {
4364
4639
  try {
4365
4640
  execFileSync2(process.platform === "win32" ? "where" : "which", [PI], { stdio: ["ignore", "pipe", "ignore"] });
4366
4641
  return true;
@@ -4370,18 +4645,18 @@ function prerequisites() {
4370
4645
  })();
4371
4646
  if (!onPath) out.push({ name: "pi", detail: `\`${PI}\` is not on PATH`, fix: "install Pi, or pass --pi <path to the pi binary>" });
4372
4647
  for (const e of EXTENSIONS) {
4373
- if (!fs8.existsSync(e)) out.push({ name: "extension", detail: `${e} does not exist`, fix: "pi install npm:@computesdk/pi-loops, or pass --extensions <path to extensions/loop.ts>" });
4648
+ if (!fs9.existsSync(e)) out.push({ name: "extension", detail: `${e} does not exist`, fix: "pi install npm:@computesdk/pi-loops, or pass --extensions <path to extensions/loop.ts>" });
4374
4649
  }
4375
4650
  return out;
4376
4651
  }
4377
4652
  var PROBLEMS = prerequisites();
4378
- fs8.mkdirSync(DATA, { recursive: true });
4653
+ fs9.mkdirSync(DATA, { recursive: true });
4379
4654
  var TOKEN = (() => {
4380
4655
  const explicit = arg("token", void 0);
4381
- const file = path8.join(DATA, "token");
4656
+ const file = path9.join(DATA, "token");
4382
4657
  const stored = (() => {
4383
4658
  try {
4384
- return fs8.readFileSync(file, "utf8").trim() || null;
4659
+ return fs9.readFileSync(file, "utf8").trim() || null;
4385
4660
  } catch {
4386
4661
  return null;
4387
4662
  }
@@ -4389,7 +4664,7 @@ var TOKEN = (() => {
4389
4664
  if (explicit) {
4390
4665
  if (!LOOPBACK) {
4391
4666
  try {
4392
- fs8.writeFileSync(file, explicit + "\n", { mode: 384 });
4667
+ fs9.writeFileSync(file, explicit + "\n", { mode: 384 });
4393
4668
  } catch {
4394
4669
  }
4395
4670
  }
@@ -4398,32 +4673,53 @@ var TOKEN = (() => {
4398
4673
  if (stored) return stored;
4399
4674
  if (LOOPBACK) return null;
4400
4675
  const made = randomBytes(24).toString("base64url");
4401
- fs8.writeFileSync(file, made + "\n", { mode: 384 });
4676
+ fs9.writeFileSync(file, made + "\n", { mode: 384 });
4402
4677
  return made;
4403
4678
  })();
4404
4679
  if (!LOOPBACK && !TOKEN) {
4405
4680
  console.error(`refusing to listen on ${HOST} without a token: this UI drives an agent that has bash. Pass --token <secret> (or let it generate one in ${DATA}/token).`);
4406
4681
  process.exit(2);
4407
4682
  }
4408
- var workspaces = new Workspaces({ root: DATA, cwd: CWD });
4683
+ var workspaces = new Workspaces({ root: DATA, ...CWD ? { cwd: CWD } : {} });
4684
+ var requireWorkspace = (id) => {
4685
+ const ws = workspaces.get(id);
4686
+ if (!ws) throw new HTTPException(404, { message: id ? `unknown workspace: ${id}` : "choose a workspace first" });
4687
+ if (!ws.available) throw new HTTPException(409, { message: `workspace directory is unavailable: ${ws.path}` });
4688
+ return ws;
4689
+ };
4690
+ var requireChatWorkspace = (id) => {
4691
+ if (!workspaces.hasChat(id)) throw new HTTPException(404, { message: `unknown chat: ${id}` });
4692
+ let ws;
4693
+ try {
4694
+ ws = workspaces.ofChat(id);
4695
+ } catch (e) {
4696
+ throw new HTTPException(409, { message: e.message });
4697
+ }
4698
+ return requireWorkspace(ws.id);
4699
+ };
4409
4700
  var manager = new ChatManager({
4410
4701
  piCommand: PI,
4411
4702
  args: EXTENSIONS.flatMap((e) => ["-e", e]),
4412
4703
  model: MODEL,
4413
4704
  // a chat's repo comes from the registry: the browser never hands us a path
4414
4705
  bind: (chat) => {
4415
- const ws = workspaces.ofChat(chat);
4416
- return { cwd: ws.path, sessionDir: ws.sessionDir };
4706
+ const ws = requireChatWorkspace(chat);
4707
+ return { cwd: ws.path, sessionDir: ws.sessionDir, sessionFile: workspaces.sessionFile(chat) ?? void 0 };
4417
4708
  }
4418
4709
  });
4419
- var wsOf = (chat) => chat ? workspaces.ofChat(chat) : workspaces.get();
4710
+ var wsOf = (chat) => chat ? requireChatWorkspace(chat) : requireWorkspace();
4420
4711
  var wsOfRun = (run3) => {
4421
- const byIndex = workspaces.list().find((w) => fs8.existsSync(path8.join(w.path, STATE_DIR, `${run3}.json`)));
4712
+ const byIndex = workspaces.list().find((w) => fs9.existsSync(path9.join(w.path, STATE_DIR, `${run3}.json`)));
4422
4713
  if (byIndex) return byIndex;
4423
4714
  const chat = manager.findRun(run3);
4424
- return chat ? workspaces.ofChat(chat.id) : workspaces.get();
4715
+ if (chat) return requireChatWorkspace(chat.id);
4716
+ throw new HTTPException(404, { message: `unknown run: ${run3}` });
4717
+ };
4718
+ var listWorkspaceChats = (ws) => {
4719
+ const all = workspaces.sessionDirs(ws.id).flatMap((sessionDir) => manager.list({ ...ws, sessionDir }));
4720
+ return [...new Map(all.map((chat) => [chat.id, chat])).values()].sort((a, b) => b.last - a.last);
4425
4721
  };
4426
- var chatsOf = (wsId) => wsId ? manager.list(workspaces.get(wsId)) : workspaces.list().flatMap((w) => manager.list(w));
4722
+ var chatsOf = (wsId) => wsId ? listWorkspaceChats(requireWorkspace(wsId)) : workspaces.list().filter((w) => w.available).flatMap(listWorkspaceChats);
4427
4723
  var withLive = (state) => {
4428
4724
  for (const run3 of state.runs ?? []) {
4429
4725
  const url = liveUrl(run3.id);
@@ -4446,7 +4742,7 @@ var watchOf = (wsId) => {
4446
4742
  if (!w) {
4447
4743
  const read = () => {
4448
4744
  try {
4449
- return JSON.parse(fs8.readFileSync(path8.join(ws.path, ".pi", "loops.json"), "utf8"));
4745
+ return JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
4450
4746
  } catch {
4451
4747
  return {};
4452
4748
  }
@@ -4454,7 +4750,7 @@ var watchOf = (wsId) => {
4454
4750
  w = { cwd: ws.path, accepted: read() };
4455
4751
  watches.set(ws.id, w);
4456
4752
  try {
4457
- fs8.watch(path8.join(ws.path, ".pi"), (_e, name) => {
4753
+ fs9.watch(path9.join(ws.path, ".pi"), (_e, name) => {
4458
4754
  if (name && name !== "loops.json") return;
4459
4755
  clearTimeout(watchTimers.get(ws.id));
4460
4756
  watchTimers.set(ws.id, setTimeout(() => checkConfig(ws.id), 120));
@@ -4476,10 +4772,10 @@ function notifyProposals() {
4476
4772
  }
4477
4773
  function checkConfig(wsId) {
4478
4774
  const w = watchOf(wsId);
4479
- const CONFIG = path8.join(w.cwd, ".pi", "loops.json");
4775
+ const CONFIG = path9.join(w.cwd, ".pi", "loops.json");
4480
4776
  const raw2 = (() => {
4481
4777
  try {
4482
- return fs8.readFileSync(CONFIG, "utf8");
4778
+ return fs9.readFileSync(CONFIG, "utf8");
4483
4779
  } catch {
4484
4780
  return "";
4485
4781
  }
@@ -4500,13 +4796,13 @@ function checkConfig(wsId) {
4500
4796
  notifyProposals();
4501
4797
  }
4502
4798
  var inside = (root, p) => {
4503
- const abs = path8.resolve(root, p);
4504
- return abs === root || abs.startsWith(root + path8.sep) ? abs : null;
4799
+ const abs = path9.resolve(root, p);
4800
+ return abs === root || abs.startsWith(root + path9.sep) ? abs : null;
4505
4801
  };
4506
4802
  var stateOf = (run3) => manager.findRun(run3)?.stateFor(run3) ?? readState(wsOfRun(run3).path, run3);
4507
4803
  var app = new Hono3();
4508
- var OWN_HOSTS = /* @__PURE__ */ new Set([`127.0.0.1:${PORT}`, `localhost:${PORT}`, `[::1]:${PORT}`]);
4509
- var OWN_ORIGINS = new Set([...OWN_HOSTS].map((h) => `http://${h}`));
4804
+ var OWN_HOSTS = /* @__PURE__ */ new Set([`127.0.0.1:${PORT}`, `localhost:${PORT}`, `[::1]:${PORT}`, ...PUBLIC_ORIGINS.map((o) => new URL(o).host)]);
4805
+ var OWN_ORIGINS = /* @__PURE__ */ new Set([`http://127.0.0.1:${PORT}`, `http://localhost:${PORT}`, `http://[::1]:${PORT}`, ...PUBLIC_ORIGINS]);
4510
4806
  var ALLOWED_ORIGINS = (arg("allow-origin", "") ?? "").split(",").map((o) => o.trim()).filter(Boolean);
4511
4807
  var originAllowed = (origin) => {
4512
4808
  if (ALLOWED_ORIGINS.includes("*")) return true;
@@ -4544,6 +4840,11 @@ app.use("/api/v1/*", async (c, next) => {
4544
4840
  await next();
4545
4841
  });
4546
4842
  var api = new Hono3();
4843
+ api.onError((error, c) => {
4844
+ if (error instanceof HTTPException) return c.json({ error: error.message }, error.status);
4845
+ console.error(error);
4846
+ return c.json({ error: error.message }, 500);
4847
+ });
4547
4848
  api.get("/health", (c) => c.json({ ok: PROBLEMS.length === 0, problems: PROBLEMS, cwd: CWD, data: DATA, pi: PI, extensions: EXTENSIONS, workspaces: workspaces.list() }));
4548
4849
  api.get("/credentials", (c) => c.json({ dir: agentDir2(), providers: credentialStatus() }));
4549
4850
  api.post("/credentials", async (c) => {
@@ -4564,7 +4865,7 @@ api.post("/credentials", async (c) => {
4564
4865
  }
4565
4866
  }
4566
4867
  }
4567
- const verified = model ? { model, ...await verifyModel({ pi: PI, model, cwd: CWD }) } : null;
4868
+ const verified = model ? { model, ...await verifyModel({ pi: PI, model, cwd: CWD ?? DATA }) } : null;
4568
4869
  return c.json({ ok: true, provider, file, configured: credentialStatus().find((s) => s.id === provider)?.configured ?? true, verified });
4569
4870
  } catch (e) {
4570
4871
  return c.json({ error: String(e.message ?? e).slice(0, 200) }, 400);
@@ -4578,16 +4879,20 @@ api.get("/chats/:id/events", async (c) => {
4578
4879
  return streamSSE(c, async (stream2) => {
4579
4880
  const send = (event, data, cursor) => stream2.writeSSE({ event, data: JSON.stringify(data), ...cursor ? { id: cursor } : {} }).catch(() => {
4580
4881
  });
4581
- await send("state", snapshotOf(chat, space.id), chat.entryCursor);
4582
4882
  const entry = { chat: id, send };
4583
4883
  streams.add(entry);
4884
+ const aborted = new Promise((resolve) => stream2.onAbort(resolve));
4584
4885
  const ping = setInterval(() => {
4585
4886
  void stream2.writeSSE({ event: "ping", data: "{}" }).catch(() => {
4586
4887
  });
4587
4888
  }, 2e4);
4588
- await new Promise((resolve) => stream2.onAbort(resolve));
4589
- clearInterval(ping);
4590
- streams.delete(entry);
4889
+ try {
4890
+ await send("state", snapshotOf(chat, space.id), chat.entryCursor);
4891
+ await aborted;
4892
+ } finally {
4893
+ clearInterval(ping);
4894
+ streams.delete(entry);
4895
+ }
4591
4896
  });
4592
4897
  });
4593
4898
  api.get("/state", async (c) => {
@@ -4599,34 +4904,48 @@ api.get("/state", async (c) => {
4599
4904
  });
4600
4905
  api.post("/chat", async (c) => {
4601
4906
  const body = await c.req.json().catch(() => ({}));
4602
- const ws = workspaces.get(body.ws) ?? workspaces.get();
4603
- const id = String(body.id ?? `chat-${Date.now().toString(36)}`).replace(/[^a-zA-Z0-9._-]/g, "").slice(0, 60);
4604
- workspaces.bindChat(id, ws.id);
4907
+ if (!body || typeof body !== "object") return c.json({ error: "a chat request object is required" }, 400);
4908
+ let id;
4909
+ let ws;
4910
+ if (body.id !== void 0) {
4911
+ id = String(body.id);
4912
+ ws = requireChatWorkspace(id);
4913
+ if (body.ws !== void 0 && body.ws !== ws.id) return c.json({ error: "an existing chat cannot move to another workspace" }, 409);
4914
+ } else {
4915
+ if (typeof body.ws !== "string" || !body.ws) return c.json({ error: "workspace required for a new chat" }, 400);
4916
+ ws = requireWorkspace(body.ws);
4917
+ if (!ws.open) return c.json({ error: "open the workspace before creating a chat" }, 409);
4918
+ id = `chat-${randomUUID()}`;
4919
+ workspaces.bindChat(id, ws.id);
4920
+ }
4605
4921
  watchOf(ws.id);
4606
4922
  await manager.open(id);
4607
4923
  return c.json({ chat: id, ws: ws.id, chats: chatsOf(ws.id) });
4608
4924
  });
4609
4925
  api.post("/chat/delete", async (c) => {
4610
4926
  const { chat: id } = await c.req.json();
4611
- const chat = manager.get(String(id));
4612
- const file = chat?.sessionFile ?? null;
4613
- await manager.close(String(id));
4614
- workspaces.unbindChat(String(id));
4615
- if (file && fs8.existsSync(file)) {
4616
- try {
4617
- fs8.rmSync(file);
4618
- } catch {
4619
- }
4620
- }
4927
+ requireChatWorkspace(String(id));
4928
+ await manager.close(String(id), () => {
4929
+ const file = workspaces.sessionFile(String(id));
4930
+ if (file && fs9.existsSync(file)) fs9.unlinkSync(file);
4931
+ workspaces.unbindChat(String(id));
4932
+ });
4621
4933
  return c.json({ ok: true });
4622
4934
  });
4623
4935
  api.get("/workspaces", (c) => c.json(workspaces.list()));
4936
+ api.get("/directories", async (c) => {
4937
+ try {
4938
+ return c.json(await browseDirectories(c.req.query("path"), c.req.query("hidden") === "1"));
4939
+ } catch (e) {
4940
+ const error = e;
4941
+ return c.json({ error: error.message }, error.code === "EACCES" || error.code === "EPERM" ? 403 : error.code === "ENOENT" || error.code === "ENOTDIR" ? 404 : 400);
4942
+ }
4943
+ });
4624
4944
  api.post("/workspaces", async (c) => {
4625
4945
  const body = await c.req.json().catch(() => ({}));
4626
4946
  try {
4627
- const dir = path8.resolve(String(body.path ?? "").replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
4628
- if (!dir || dir === "/") throw new Error("a path is required");
4629
- if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) throw new Error(`${dir} is not a directory`);
4947
+ const dir = directoryPath(body.path);
4948
+ if (!fs9.existsSync(dir) || !fs9.statSync(dir).isDirectory()) throw new Error(`${dir} is not a directory`);
4630
4949
  const ws = workspaces.add(dir);
4631
4950
  watchOf(ws.id);
4632
4951
  return c.json({ ok: true, workspace: ws, loopable: Workspaces.loopable(ws.path) });
@@ -4636,6 +4955,7 @@ api.post("/workspaces", async (c) => {
4636
4955
  });
4637
4956
  api.post("/workspaces/remove", async (c) => {
4638
4957
  const { id } = await c.req.json();
4958
+ if (!workspaces.get(String(id))) return c.json({ error: `unknown workspace: ${id}` }, 404);
4639
4959
  try {
4640
4960
  workspaces.remove(String(id));
4641
4961
  return c.json({ ok: true });
@@ -4661,11 +4981,25 @@ api.post("/abort", async (c) => {
4661
4981
  await chat.abort();
4662
4982
  return c.json({ ok: true });
4663
4983
  });
4984
+ api.post("/chat/retry", async (c) => {
4985
+ const { chat: id } = await c.req.json();
4986
+ requireChatWorkspace(String(id));
4987
+ try {
4988
+ await manager.retry(String(id));
4989
+ return c.json({ ok: true });
4990
+ } catch (e) {
4991
+ return c.json({ ok: false, error: String(e.message ?? e) }, 409);
4992
+ }
4993
+ });
4664
4994
  api.post("/ui", async (c) => {
4665
4995
  const { chat: id, id: dialogId, response } = await c.req.json();
4666
4996
  const chat = await manager.open(String(id));
4667
- await chat.answerUi(String(dialogId), response ?? {});
4668
- return c.json({ ok: true });
4997
+ try {
4998
+ await chat.answerUi(String(dialogId), response ?? {});
4999
+ return c.json({ ok: true });
5000
+ } catch (e) {
5001
+ return c.json({ ok: false, error: String(e.message ?? e) }, 409);
5002
+ }
4669
5003
  });
4670
5004
  api.post("/answer", async (c) => {
4671
5005
  const { chat: id, question, choice } = await c.req.json();
@@ -4710,9 +5044,8 @@ api.get("/session", async (c) => {
4710
5044
  if (!run3 || !name) return c.json([]);
4711
5045
  const state = stateOf(run3);
4712
5046
  const file = state?.rounds?.flatMap((r) => r.attempts).find((a) => a.label === name)?.session ?? state?.rounds?.flatMap((r) => r.attempts).find((a) => a.label?.toLowerCase() === name.toLowerCase())?.session;
4713
- if (!file || !fs8.existsSync(file)) return c.json([]);
4714
- const chat = await manager.open(c.req.query("chat") ?? manager.list()[0]?.id ?? "");
4715
- return c.json(chat.sessionItems(file));
5047
+ if (!file || !fs9.existsSync(file)) return c.json([]);
5048
+ return c.json(itemsFromMessages(readSessionMessages(file)));
4716
5049
  });
4717
5050
  api.post("/talk", async (c) => {
4718
5051
  const { chat: id, run: run3, name, text } = await c.req.json();
@@ -4729,21 +5062,22 @@ api.get("/shot", (c) => {
4729
5062
  const repo = wsOfRun(run3).path;
4730
5063
  if (file.startsWith(STATE_DIR)) {
4731
5064
  const abs2 = inside(repo, file);
4732
- if (!abs2 || !fs8.existsSync(abs2)) return c.notFound();
4733
- return new Response(fs8.readFileSync(abs2), { headers: { "content-type": abs2.endsWith(".jpg") || abs2.endsWith(".jpeg") ? "image/jpeg" : "image/png", "cache-control": "no-cache" } });
5065
+ if (!abs2 || !fs9.existsSync(abs2)) return c.notFound();
5066
+ return new Response(fs9.readFileSync(abs2), { headers: { "content-type": abs2.endsWith(".jpg") || abs2.endsWith(".jpeg") ? "image/jpeg" : "image/png", "cache-control": "no-cache" } });
4734
5067
  }
4735
5068
  const dir = attempt === "CURRENT" ? state?.best?.dir ?? null : state?.rounds?.flatMap((r) => r.attempts).find((a) => a.label === attempt || a.label?.toLowerCase() === attempt.toLowerCase())?.dir;
4736
5069
  if (!dir) return c.notFound();
4737
- const abs = inside(repo, path8.join(dir, file));
4738
- if (!abs || !fs8.existsSync(abs)) return c.notFound();
4739
- return new Response(fs8.readFileSync(abs), { headers: { "content-type": abs.endsWith(".jpg") || abs.endsWith(".jpeg") ? "image/jpeg" : "image/png", "cache-control": "no-cache" } });
5070
+ const abs = inside(repo, path9.join(dir, file));
5071
+ if (!abs || !fs9.existsSync(abs)) return c.notFound();
5072
+ return new Response(fs9.readFileSync(abs), { headers: { "content-type": abs.endsWith(".jpg") || abs.endsWith(".jpeg") ? "image/jpeg" : "image/png", "cache-control": "no-cache" } });
4740
5073
  });
4741
5074
  api.get("/sheet", (c) => {
4742
5075
  const file = c.req.query("f") ?? "";
4743
- const ws = workspaces.list().find((w) => fs8.existsSync(path8.join(w.path, file))) ?? workspaces.get();
5076
+ const ws = c.req.query("run") ? wsOfRun(c.req.query("run")) : workspaces.list().find((w) => fs9.existsSync(path9.join(w.path, file)));
5077
+ if (!ws) return c.notFound();
4744
5078
  const abs = inside(ws.path, file);
4745
- if (!abs || !abs.includes(STATE_DIR) || !fs8.existsSync(abs)) return c.notFound();
4746
- return new Response(fs8.readFileSync(abs), { headers: { "content-type": "image/png", "cache-control": "no-cache" } });
5079
+ if (!abs || !abs.includes(STATE_DIR) || !fs9.existsSync(abs)) return c.notFound();
5080
+ return new Response(fs9.readFileSync(abs), { headers: { "content-type": "image/png", "cache-control": "no-cache" } });
4747
5081
  });
4748
5082
  api.get("/diff", async (c) => {
4749
5083
  const run3 = c.req.query("run") ?? "";
@@ -4756,8 +5090,8 @@ api.get("/diff", async (c) => {
4756
5090
  const from = state.baseCommit;
4757
5091
  if (!to || !from) return c.text("");
4758
5092
  return c.text(await new Promise((resolve) => {
4759
- execFile("git", ["diff", "--stat", `${from}..${to}`], { cwd: wsOfRun(run3).path, maxBuffer: 1 << 26 }, (_e, stat) => {
4760
- execFile("git", ["diff", `${from}..${to}`], { cwd: wsOfRun(run3).path, maxBuffer: 1 << 26 }, (_e2, diff) => resolve(`${stat}
5093
+ execFile2("git", ["diff", "--stat", `${from}..${to}`], { cwd: wsOfRun(run3).path, maxBuffer: 1 << 26 }, (_e, stat) => {
5094
+ execFile2("git", ["diff", `${from}..${to}`], { cwd: wsOfRun(run3).path, maxBuffer: 1 << 26 }, (_e2, diff) => resolve(`${stat}
4761
5095
  ${diff}`.slice(0, 4e5)));
4762
5096
  });
4763
5097
  }));
@@ -4780,7 +5114,7 @@ api.get("/proposals", (c) => c.json(proposals));
4780
5114
  api.post("/proposals/:id/apply", (c) => {
4781
5115
  const p = proposals.find((x) => x.id === c.req.param("id"));
4782
5116
  if (!p) return c.json({ ok: false, error: "no such proposal" }, 404);
4783
- const cwd = p.ws ? workspaces.get(p.ws).path : CWD;
5117
+ const cwd = requireWorkspace(p.ws).path;
4784
5118
  acceptConfig(cwd, p.after);
4785
5119
  const w = p.ws ? watches.get(p.ws) : null;
4786
5120
  if (w) w.accepted = p.after;
@@ -4807,7 +5141,7 @@ api.post("/config", async (c) => {
4807
5141
  writeSetting(ws.path, String(key), value);
4808
5142
  const w = watchOf(ws.id);
4809
5143
  try {
4810
- w.accepted = JSON.parse(fs8.readFileSync(path8.join(ws.path, ".pi", "loops.json"), "utf8"));
5144
+ w.accepted = JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
4811
5145
  } catch {
4812
5146
  }
4813
5147
  return c.json({ ok: true });
@@ -4824,7 +5158,7 @@ api.get("/size", async (c) => {
4824
5158
  const state = stateOf(run3);
4825
5159
  if (!state) return c.json({ bytes: 0, dirs: 0 });
4826
5160
  const dirs = (state.rounds ?? []).flatMap((r) => r.attempts).map((a) => a.dir).filter(Boolean);
4827
- const bytes = await new Promise((resolve) => execFile("du", ["-sk", ...dirs], { cwd: wsOfRun(run3).path }, (_e, out) => resolve(Number(String(out ?? "").split("\n").map((l) => Number(l.split(/\s+/)[0]) || 0).reduce((a, b) => a + b, 0)) * 1024)));
5161
+ const bytes = await new Promise((resolve) => execFile2("du", ["-sk", ...dirs], { cwd: wsOfRun(run3).path }, (_e, out) => resolve(Number(String(out ?? "").split("\n").map((l) => Number(l.split(/\s+/)[0]) || 0).reduce((a, b) => a + b, 0)) * 1024)));
4828
5162
  return c.json({ bytes, dirs: dirs.length });
4829
5163
  });
4830
5164
  api.get("/files", (c) => {
@@ -4854,7 +5188,8 @@ api.get("/meta", (c) => c.json({
4854
5188
  }));
4855
5189
  app.route("/api", api);
4856
5190
  app.route("/api/v1", api);
4857
- var DIST = path8.join(APP_ROOT, "dist");
5191
+ app.all("/api/*", (c) => c.json({ error: `unknown API route: ${c.req.path}` }, 404));
5192
+ var DIST = path9.join(APP_ROOT, "dist");
4858
5193
  app.get("*", async (c) => {
4859
5194
  const url = c.req.path;
4860
5195
  if (DEV) {
@@ -4863,14 +5198,14 @@ app.get("*", async (c) => {
4863
5198
  return new Response(res.body, { status: res.status, headers: { "content-type": res.headers.get("content-type") ?? "text/html" } });
4864
5199
  }
4865
5200
  const abs = inside(DIST, url === "/" ? "index.html" : url.replace(/^\//, ""));
4866
- if (abs && fs8.existsSync(abs) && fs8.statSync(abs).isFile()) {
4867
- const ext = path8.extname(abs);
5201
+ if (abs && fs9.existsSync(abs) && fs9.statSync(abs).isFile()) {
5202
+ const ext = path9.extname(abs);
4868
5203
  const type = ext === ".js" ? "text/javascript" : ext === ".css" ? "text/css" : ext === ".woff2" ? "font/woff2" : ext === ".svg" ? "image/svg+xml" : ext === ".png" ? "image/png" : "text/html";
4869
- return new Response(fs8.readFileSync(abs), { headers: { "content-type": type } });
5204
+ return new Response(fs9.readFileSync(abs), { headers: { "content-type": type } });
4870
5205
  }
4871
- const index = path8.join(DIST, "index.html");
4872
- if (!fs8.existsSync(index)) return c.text("run `npm run build` first", 500);
4873
- return new Response(fs8.readFileSync(index), { headers: { "content-type": "text/html" } });
5206
+ const index = path9.join(DIST, "index.html");
5207
+ if (!fs9.existsSync(index)) return c.text("run `npm run build` first", 500);
5208
+ return new Response(fs9.readFileSync(index), { headers: { "content-type": "text/html" } });
4874
5209
  });
4875
5210
  var server = http.createServer(getRequestListener(app.fetch));
4876
5211
  setInterval(() => {
@@ -4890,7 +5225,7 @@ function openBrowser(url) {
4890
5225
  server.listen(PORT, HOST, () => {
4891
5226
  const reach = HOST === "0.0.0.0" ? "127.0.0.1" : HOST;
4892
5227
  const shown = reach.includes(":") ? `[${reach}]` : reach;
4893
- const base = `http://${shown}:${PORT}`;
5228
+ const base = PUBLIC_ORIGINS[0] ?? `http://${shown}:${PORT}`;
4894
5229
  const url = TOKEN ? `${base}/?server=${encodeURIComponent(base)}&token=${encodeURIComponent(TOKEN)}` : base;
4895
5230
  console.log(`
4896
5231
  open ${url}`);