@computesdk/loops 0.1.10 → 0.1.12

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 (72) hide show
  1. package/API.md +34 -0
  2. package/README.md +45 -2
  3. package/bin/pi-rpc-startup.mjs +8 -0
  4. package/client.ts +27 -0
  5. package/dist/assets/{abnfDiagram-VCTEODGH-DCBDcsG9.js → abnfDiagram-VCTEODGH-D2bigoxc.js} +1 -1
  6. package/dist/assets/{arc-XHL_gOXE.js → arc-BZl7sgf7.js} +1 -1
  7. package/dist/assets/{architectureDiagram-5GKGNRK7-D2AZUwQa.js → architectureDiagram-5GKGNRK7-BEVh04as.js} +1 -1
  8. package/dist/assets/{blockDiagram-I7D4REHJ-B4gIbwtQ.js → blockDiagram-I7D4REHJ-BXHXs9X2.js} +1 -1
  9. package/dist/assets/{c4Diagram-7LVT6UL2-BVQsOVAp.js → c4Diagram-7LVT6UL2-BDJNDCcc.js} +1 -1
  10. package/dist/assets/channel-C47Lbhx6.js +1 -0
  11. package/dist/assets/{chunk-2Q5K7J3B-JT4Vadns.js → chunk-2Q5K7J3B-BdUkLQYN.js} +1 -1
  12. package/dist/assets/{chunk-5VM5RSS4-CDeArlJ5.js → chunk-5VM5RSS4-3N4vBacE.js} +1 -1
  13. package/dist/assets/{chunk-F27PBJKO-CCc4sEwP.js → chunk-F27PBJKO-ByjH2OJK.js} +1 -1
  14. package/dist/assets/{chunk-IMKFNOWR-CXd4ZLrB.js → chunk-IMKFNOWR-D8jAt4ka.js} +1 -1
  15. package/dist/assets/{chunk-JWPE2WC7-DtGxXURA.js → chunk-JWPE2WC7-B7DWQYA4.js} +1 -1
  16. package/dist/assets/{chunk-POPQ4Y6H-TYAe-WXZ.js → chunk-POPQ4Y6H-D5nv_lqi.js} +1 -1
  17. package/dist/assets/{chunk-SVP7TREG-DmyN3wGH.js → chunk-SVP7TREG-CBg3GrIs.js} +1 -1
  18. package/dist/assets/{chunk-TICWLB2K-CoQRzD2R.js → chunk-TICWLB2K-BL_wE86-.js} +1 -1
  19. package/dist/assets/{chunk-XXDRQBXY-Cdv4d7MB.js → chunk-XXDRQBXY-ipZwP-Tw.js} +1 -1
  20. package/dist/assets/classDiagram-ZZMXUADV-BkejU3Ym.js +1 -0
  21. package/dist/assets/classDiagram-v2-VYDZK3BY-BkejU3Ym.js +1 -0
  22. package/dist/assets/{cose-bilkent-JH36ORCC-B_tFtaAU.js → cose-bilkent-JH36ORCC-tIdDJe7h.js} +1 -1
  23. package/dist/assets/{cynefin-OW5HDTMX-Cwqvcd49.js → cynefin-OW5HDTMX-C5BZ6yCh.js} +1 -1
  24. package/dist/assets/{cynefinDiagram-5FMLGOSQ-DABTV6Vb.js → cynefinDiagram-5FMLGOSQ-KZkQ_Fs5.js} +1 -1
  25. package/dist/assets/{dagre-GXQ25YYZ-TYrmYsuz.js → dagre-GXQ25YYZ-8g1xXz_z.js} +1 -1
  26. package/dist/assets/{diagram-S7CK7UJ4-DHSEu2H3.js → diagram-S7CK7UJ4-Civ7O2b1.js} +1 -1
  27. package/dist/assets/{diagram-UQ7AKVKN-C019YqL0.js → diagram-UQ7AKVKN-CsiklyIt.js} +1 -1
  28. package/dist/assets/{diagram-VSXAHHWV-D4MEzffI.js → diagram-VSXAHHWV-eXAMoOzJ.js} +1 -1
  29. package/dist/assets/{diagram-VX7I27RA-kFJ0JvUv.js → diagram-VX7I27RA-Bb913WNj.js} +1 -1
  30. package/dist/assets/{diagram-Z3DM3KII-BcO31l9N.js → diagram-Z3DM3KII-CiGD7-UB.js} +1 -1
  31. package/dist/assets/{ebnfDiagram-PWID7BFC-DZj4QQh4.js → ebnfDiagram-PWID7BFC-DFPxxkTf.js} +1 -1
  32. package/dist/assets/{erDiagram-RLTQ6QDP-CmJHu3yl.js → erDiagram-RLTQ6QDP-DkPhQteJ.js} +1 -1
  33. package/dist/assets/{flowDiagram-HODETNUW-Cs2i9PHj.js → flowDiagram-HODETNUW-B8DTvq1w.js} +1 -1
  34. package/dist/assets/{ganttDiagram-EL5Y4UJY-DeROwj00.js → ganttDiagram-EL5Y4UJY-DPfnHIkg.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-WWUBYQGX--qyeTDhG.js → gitGraphDiagram-WWUBYQGX-BzYuJVan.js} +1 -1
  36. package/dist/assets/{highlighted-body-JLMTFACH-ChXoKzQk.js → highlighted-body-JLMTFACH-D3y_nfFu.js} +1 -1
  37. package/dist/assets/index-DvyWv1zp.js +902 -0
  38. package/dist/assets/index-vH6Ei526.css +1 -0
  39. package/dist/assets/{infoDiagram-27XIBGKW-CL4TN3Oz.js → infoDiagram-27XIBGKW-BCF2o-MF.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-5VMMS53U-BOpnwC1K.js → ishikawaDiagram-5VMMS53U-CNe6ETgR.js} +1 -1
  41. package/dist/assets/{journeyDiagram-3NMN7TZE-B9WrcTg4.js → journeyDiagram-3NMN7TZE-BVF1wnNZ.js} +1 -1
  42. package/dist/assets/{kanban-definition-UXKFOSKX-k-zPWS3C.js → kanban-definition-UXKFOSKX-CiWrBpoV.js} +1 -1
  43. package/dist/assets/{layout-3kvHth78.js → layout-B2YGfZNT.js} +1 -1
  44. package/dist/assets/{linear-BWj_7-Qu.js → linear-CecQQcNl.js} +1 -1
  45. package/dist/assets/{mindmap-definition-YA3MSWOX-CHQiqXfo.js → mindmap-definition-YA3MSWOX-CTRfx8rM.js} +1 -1
  46. package/dist/assets/{pegDiagram-XKGWAZYB-CbU0Toll.js → pegDiagram-XKGWAZYB-BW5JUykP.js} +1 -1
  47. package/dist/assets/{pieDiagram-E7YTZNPT-DMet_oui.js → pieDiagram-E7YTZNPT-rtJftIcC.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-AXDQQJYC-BebzO8sd.js → quadrantDiagram-AXDQQJYC-C2DaPj2w.js} +1 -1
  49. package/dist/assets/{railroadDiagram-O6MQD6OU-Bh8-vPzn.js → railroadDiagram-O6MQD6OU-CcPfXEFl.js} +1 -1
  50. package/dist/assets/{requirementDiagram-BXWQKSXE-BPUXWj9N.js → requirementDiagram-BXWQKSXE-ozaClcIW.js} +1 -1
  51. package/dist/assets/{sankeyDiagram-P5KCCOFB-py5UjYxE.js → sankeyDiagram-P5KCCOFB-74tHF7mP.js} +1 -1
  52. package/dist/assets/{sequenceDiagram-WJ2MYXX4-7dG5yYB3.js → sequenceDiagram-WJ2MYXX4-CTexKnGo.js} +1 -1
  53. package/dist/assets/{sizeCapture-INFHLROL-84MVWGh6.js → sizeCapture-INFHLROL-2kVX0pT9.js} +1 -1
  54. package/dist/assets/{stateDiagram-D77RDMKH-BI7nUn04.js → stateDiagram-D77RDMKH-CgAkifQk.js} +1 -1
  55. package/dist/assets/stateDiagram-v2-MP3YSRHH-CH_iWHXT.js +1 -0
  56. package/dist/assets/{swimlanes-42K2YHIH-kmjOxJ3M.js → swimlanes-42K2YHIH-B-p2fQK8.js} +1 -1
  57. package/dist/assets/swimlanesDiagram-VR7AAH4N-BVcRccxJ.js +8 -0
  58. package/dist/assets/{timeline-definition-24CTP7MA-x-0bw4L3.js → timeline-definition-24CTP7MA-DhfUmVud.js} +1 -1
  59. package/dist/assets/{vennDiagram-4TSXK5OY-CPpgLcl3.js → vennDiagram-4TSXK5OY-ubMqyg9F.js} +1 -1
  60. package/dist/assets/{wardleyDiagram-VM6X3IG4-B32JnOAm.js → wardleyDiagram-VM6X3IG4-DGDiyaT7.js} +1 -1
  61. package/dist/assets/{xychartDiagram-S5SC5T6Z-HnFQQ24s.js → xychartDiagram-S5SC5T6Z-Cj_dbVk_.js} +1 -1
  62. package/dist/index.html +2 -2
  63. package/dist/server.mjs +691 -129
  64. package/package.json +4 -4
  65. package/src/shared/types.ts +40 -0
  66. package/dist/assets/channel-BflrI4kn.js +0 -1
  67. package/dist/assets/classDiagram-ZZMXUADV-hGKBk6GI.js +0 -1
  68. package/dist/assets/classDiagram-v2-VYDZK3BY-hGKBk6GI.js +0 -1
  69. package/dist/assets/index-C1H-VPU1.css +0 -1
  70. package/dist/assets/index-D-f-6Xq7.js +0 -902
  71. package/dist/assets/stateDiagram-v2-MP3YSRHH-C1raQMUd.js +0 -1
  72. package/dist/assets/swimlanesDiagram-VR7AAH4N-Du4mhJod.js +0 -8
package/dist/server.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  // server/index.ts
2
2
  import fs9 from "node:fs";
3
- import { randomBytes, randomUUID } from "node:crypto";
3
+ import { randomBytes, randomUUID as randomUUID2 } from "node:crypto";
4
4
  import http from "node:http";
5
- import path9 from "node:path";
5
+ import path10 from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFile as execFile2, execFileSync as execFileSync2, spawn as spawn6 } from "node:child_process";
8
8
 
@@ -107,26 +107,26 @@ var throwNestingLimitExceeded = () => {
107
107
  };
108
108
 
109
109
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/utils/url.js
110
- var splitPath = (path10) => {
111
- const paths = path10.split("/");
110
+ var splitPath = (path11) => {
111
+ const paths = path11.split("/");
112
112
  if (paths[0] === "") paths.shift();
113
113
  return paths;
114
114
  };
115
115
  var splitRoutingPath = (routePath) => {
116
- const { groups, path: path10 } = extractGroupsFromPath(routePath);
117
- const paths = splitPath(path10);
116
+ const { groups, path: path11 } = extractGroupsFromPath(routePath);
117
+ const paths = splitPath(path11);
118
118
  return replaceGroupMarks(paths, groups);
119
119
  };
120
- var extractGroupsFromPath = (path10) => {
120
+ var extractGroupsFromPath = (path11) => {
121
121
  const groups = [];
122
- path10 = path10.replace(/\{[^}]+\}/g, (match2, index) => {
122
+ path11 = path11.replace(/\{[^}]+\}/g, (match2, index) => {
123
123
  const mark = `@${index}`;
124
124
  groups.push([mark, match2]);
125
125
  return mark;
126
126
  });
127
127
  return {
128
128
  groups,
129
- path: path10
129
+ path: path11
130
130
  };
131
131
  };
132
132
  var replaceGroupMarks = (paths, groups) => {
@@ -189,8 +189,8 @@ var getPath = (request) => {
189
189
  const queryIndex = url.indexOf("?", i);
190
190
  const hashIndex = url.indexOf("#", i);
191
191
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
192
- const path10 = url.slice(start, end);
193
- return tryDecodeURI(path10.includes("%25") ? path10.replace(/%25/g, "%2525") : path10);
192
+ const path11 = url.slice(start, end);
193
+ return tryDecodeURI(path11.includes("%25") ? path11.replace(/%25/g, "%2525") : path11);
194
194
  } else if (charCode === 63 || charCode === 35) break;
195
195
  }
196
196
  return url.slice(start, i);
@@ -203,9 +203,9 @@ var mergePath = (base, sub, ...rest) => {
203
203
  if (rest.length) sub = mergePath(sub, ...rest);
204
204
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
205
205
  };
206
- var checkOptionalParameter = (path10) => {
207
- if (path10.charCodeAt(path10.length - 1) !== 63 || !path10.includes(":")) return null;
208
- const segments = path10.split("/");
206
+ var checkOptionalParameter = (path11) => {
207
+ if (path11.charCodeAt(path11.length - 1) !== 63 || !path11.includes(":")) return null;
208
+ const segments = path11.split("/");
209
209
  const results = [];
210
210
  let basePath = "";
211
211
  segments.forEach((segment) => {
@@ -316,9 +316,9 @@ var HonoRequest = class {
316
316
  */
317
317
  path;
318
318
  bodyCache = {};
319
- constructor(request, path10 = "/", matchResult = [[]]) {
319
+ constructor(request, path11 = "/", matchResult = [[]]) {
320
320
  this.raw = request;
321
- this.path = path10;
321
+ this.path = path11;
322
322
  this.#matchResult = matchResult;
323
323
  }
324
324
  param(key) {
@@ -1048,8 +1048,8 @@ var Hono = class Hono2 {
1048
1048
  return this;
1049
1049
  };
1050
1050
  });
1051
- this.on = (method, path10, ...handlers) => {
1052
- for (const p of [path10].flat()) {
1051
+ this.on = (method, path11, ...handlers) => {
1052
+ for (const p of [path11].flat()) {
1053
1053
  this.#path = p;
1054
1054
  for (const m of [method].flat()) {
1055
1055
  const methodName = m.toUpperCase();
@@ -1103,8 +1103,8 @@ var Hono = class Hono2 {
1103
1103
  * app.route("/api", app2) // GET /api/user
1104
1104
  * ```
1105
1105
  */
1106
- route(path10, app2) {
1107
- const subApp = this.basePath(path10);
1106
+ route(path11, app2) {
1107
+ const subApp = this.basePath(path11);
1108
1108
  app2.routes.map((r) => {
1109
1109
  let handler;
1110
1110
  if (app2.errorHandler === errorHandler) handler = r.handler;
@@ -1129,9 +1129,9 @@ var Hono = class Hono2 {
1129
1129
  * const api = new Hono().basePath('/api')
1130
1130
  * ```
1131
1131
  */
1132
- basePath(path10) {
1132
+ basePath(path11) {
1133
1133
  const subApp = this.#clone();
1134
- subApp._basePath = mergePath(this._basePath, path10);
1134
+ subApp._basePath = mergePath(this._basePath, path11);
1135
1135
  return subApp;
1136
1136
  }
1137
1137
  /**
@@ -1207,7 +1207,7 @@ var Hono = class Hono2 {
1207
1207
  * })
1208
1208
  * ```
1209
1209
  */
1210
- mount(path10, applicationHandler, options) {
1210
+ mount(path11, applicationHandler, options) {
1211
1211
  let replaceRequest;
1212
1212
  let optionHandler;
1213
1213
  if (options) {
@@ -1230,7 +1230,7 @@ var Hono = class Hono2 {
1230
1230
  return [c.env, executionContext];
1231
1231
  };
1232
1232
  replaceRequest ||= (() => {
1233
- const mergedPath = mergePath(this._basePath, path10);
1233
+ const mergedPath = mergePath(this._basePath, path11);
1234
1234
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
1235
1235
  return (request) => {
1236
1236
  const url = new URL(request.url);
@@ -1243,18 +1243,18 @@ var Hono = class Hono2 {
1243
1243
  if (res) return res;
1244
1244
  await next();
1245
1245
  };
1246
- this.#addRoute("ALL", mergePath(path10, "*"), handler);
1246
+ this.#addRoute("ALL", mergePath(path11, "*"), handler);
1247
1247
  return this;
1248
1248
  }
1249
- #addRoute(method, path10, handler, baseRoutePath) {
1250
- path10 = mergePath(this._basePath, path10);
1249
+ #addRoute(method, path11, handler, baseRoutePath) {
1250
+ path11 = mergePath(this._basePath, path11);
1251
1251
  const r = {
1252
1252
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
1253
- path: path10,
1253
+ path: path11,
1254
1254
  method,
1255
1255
  handler
1256
1256
  };
1257
- this.router.add(method, path10, [handler, r]);
1257
+ this.router.add(method, path11, [handler, r]);
1258
1258
  this.routes.push(r);
1259
1259
  }
1260
1260
  #handleError(err, c) {
@@ -1263,10 +1263,10 @@ var Hono = class Hono2 {
1263
1263
  }
1264
1264
  #dispatch(request, executionCtx, env, method) {
1265
1265
  if (method === "HEAD") return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
1266
- const path10 = this.getPath(request, { env });
1267
- const matchResult = this.router.match(method, path10);
1266
+ const path11 = this.getPath(request, { env });
1267
+ const matchResult = this.router.match(method, path11);
1268
1268
  const c = new Context(request, {
1269
- path: path10,
1269
+ path: path11,
1270
1270
  matchResult,
1271
1271
  env,
1272
1272
  executionCtx,
@@ -1320,10 +1320,10 @@ var Hono = class Hono2 {
1320
1320
  * ```
1321
1321
  * @see https://hono.dev/docs/api/hono#request
1322
1322
  */
1323
- request = (input, requestInit, Env, executionCtx) => {
1324
- if (input instanceof Request) return this.fetch(requestInit ? new Request(input, requestInit) : input, Env, executionCtx);
1325
- input = input.toString();
1326
- return this.fetch(new Request(/^https?:\/\//.test(input) ? input : `http://localhost${mergePath("/", input)}`, requestInit), Env, executionCtx);
1323
+ request = (input2, requestInit, Env, executionCtx) => {
1324
+ if (input2 instanceof Request) return this.fetch(requestInit ? new Request(input2, requestInit) : input2, Env, executionCtx);
1325
+ input2 = input2.toString();
1326
+ return this.fetch(new Request(/^https?:\/\//.test(input2) ? input2 : `http://localhost${mergePath("/", input2)}`, requestInit), Env, executionCtx);
1327
1327
  };
1328
1328
  /**
1329
1329
  * `.fire()` automatically adds a global fetch event listener.
@@ -1354,19 +1354,19 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
1354
1354
 
1355
1355
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/matcher.js
1356
1356
  var emptyParam = [];
1357
- function match(method, path10) {
1357
+ function match(method, path11) {
1358
1358
  const matchers = this.buildAllMatchers();
1359
- const match2 = ((method2, path11) => {
1359
+ const match2 = ((method2, path12) => {
1360
1360
  const matcher = matchers[method2] || matchers["ALL"];
1361
- const staticMatch = matcher[2][path11];
1361
+ const staticMatch = matcher[2][path12];
1362
1362
  if (staticMatch) return staticMatch;
1363
- const match3 = path11.match(matcher[0]);
1363
+ const match3 = path12.match(matcher[0]);
1364
1364
  if (!match3) return [[], emptyParam];
1365
1365
  const index = match3.indexOf("", 1);
1366
1366
  return [matcher[1][index], match3];
1367
1367
  });
1368
1368
  this.match = match2;
1369
- return match2(method, path10);
1369
+ return match2(method, path11);
1370
1370
  }
1371
1371
 
1372
1372
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -1456,14 +1456,14 @@ var Trie = class {
1456
1456
  #root = new Node();
1457
1457
  #index = 0;
1458
1458
  paths = createNullObject();
1459
- insert(path10, isStatic) {
1459
+ insert(path11, isStatic) {
1460
1460
  if (isStatic) {
1461
- this.#root.insert(path10.split(""), 0, [], this.#context, true);
1461
+ this.#root.insert(path11.split(""), 0, [], this.#context, true);
1462
1462
  return;
1463
1463
  }
1464
1464
  const paramAssoc = [];
1465
1465
  const groups = [];
1466
- let markedPath = path10;
1466
+ let markedPath = path11;
1467
1467
  for (let i = 0; ; ) {
1468
1468
  let replaced = false;
1469
1469
  markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
@@ -1484,7 +1484,7 @@ var Trie = class {
1484
1484
  }
1485
1485
  }
1486
1486
  this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
1487
- this.paths[path10] = [this.#index++, paramAssoc];
1487
+ this.paths[path11] = [this.#index++, paramAssoc];
1488
1488
  }
1489
1489
  buildRegExp() {
1490
1490
  let regexp = this.#root.buildRegExpStr();
@@ -1517,11 +1517,11 @@ var Trie = class {
1517
1517
 
1518
1518
  // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/router/reg-exp-router/router.js
1519
1519
  var wildcardRegExpCache = createNullObject();
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}`)}$`);
1520
+ function buildWildcardRegExp(path11) {
1521
+ return wildcardRegExpCache[path11] ??= new RegExp(`^${path11.replace(/\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g, (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ".*" : `/:${LABEL_REG_EXP_STR}`)}$`);
1522
1522
  }
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]];
1523
+ function findMiddleware(middleware, path11) {
1524
+ for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(path11)) return [...middleware[k]];
1525
1525
  }
1526
1526
  var RegExpRouter = class {
1527
1527
  name = "RegExpRouter";
@@ -1533,14 +1533,14 @@ var RegExpRouter = class {
1533
1533
  this.#routes = { ["ALL"]: createNullObject() };
1534
1534
  this.#tries = { ["ALL"]: new Trie() };
1535
1535
  }
1536
- #insertPath(method, path10) {
1536
+ #insertPath(method, path11) {
1537
1537
  try {
1538
- this.#tries[method].insert(path10, !/\*|\/:/.test(path10));
1538
+ this.#tries[method].insert(path11, !/\*|\/:/.test(path11));
1539
1539
  } catch (e) {
1540
- throw e === PATH_ERROR ? new UnsupportedPathError(path10) : e;
1540
+ throw e === PATH_ERROR ? new UnsupportedPathError(path11) : e;
1541
1541
  }
1542
1542
  }
1543
- add(method, path10, handler) {
1543
+ add(method, path11, handler) {
1544
1544
  const middleware = this.#middleware;
1545
1545
  const routes = this.#routes;
1546
1546
  if (!middleware) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
@@ -1554,24 +1554,24 @@ var RegExpRouter = class {
1554
1554
  }
1555
1555
  }
1556
1556
  }
1557
- if (path10 === "/*") path10 = "*";
1557
+ if (path11 === "/*") path11 = "*";
1558
1558
  const methods = method === "ALL" ? Object.keys(middleware) : [method];
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) || [];
1559
+ if (/\*$/.test(path11)) {
1560
+ const re = buildWildcardRegExp(path11);
1561
+ for (const m of methods) if (!middleware[m][path11]) {
1562
+ this.#insertPath(m, path11);
1563
+ middleware[m][path11] = findMiddleware(middleware[m], path11) || findMiddleware(middleware["ALL"], path11) || [];
1564
1564
  }
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]);
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, path11]);
1566
1566
  return;
1567
1567
  }
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) || [];
1568
+ const paths = checkOptionalParameter(path11) || [path11];
1569
+ for (const path12 of paths) for (const m of methods) {
1570
+ if (!routes[m][path12]) {
1571
+ this.#insertPath(m, path12);
1572
+ routes[m][path12] = findMiddleware(middleware[m], path12) || findMiddleware(middleware["ALL"], path12) || [];
1573
1573
  }
1574
- routes[m][path11].push([handler, path11]);
1574
+ routes[m][path12].push([handler, path12]);
1575
1575
  }
1576
1576
  }
1577
1577
  match = match;
@@ -1589,11 +1589,11 @@ var RegExpRouter = class {
1589
1589
  const staticMap = createNullObject();
1590
1590
  const handlerData = [];
1591
1591
  const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
1592
- for (const r of [middleware, routes]) for (const path10 in r) {
1593
- const handlers = r[path10];
1594
- const pathData = trie.paths[path10];
1592
+ for (const r of [middleware, routes]) for (const path11 in r) {
1593
+ const handlers = r[path11];
1594
+ const pathData = trie.paths[path11];
1595
1595
  if (!pathData) {
1596
- staticMap[path10] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
1596
+ staticMap[path11] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
1597
1597
  continue;
1598
1598
  }
1599
1599
  handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [h, trie.paths[handlerPath][1].reduceRight((map, [key], i) => {
@@ -1617,15 +1617,15 @@ var SmartRouter = class {
1617
1617
  constructor(init) {
1618
1618
  this.#routers = init.routers;
1619
1619
  }
1620
- add(method, path10, handler) {
1620
+ add(method, path11, handler) {
1621
1621
  if (!this.#routes) throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
1622
1622
  this.#routes.push([
1623
1623
  method,
1624
- path10,
1624
+ path11,
1625
1625
  handler
1626
1626
  ]);
1627
1627
  }
1628
- match(method, path10) {
1628
+ match(method, path11) {
1629
1629
  if (!this.#routes) throw new Error("Fatal error");
1630
1630
  const routers = this.#routers;
1631
1631
  const routes = this.#routes;
@@ -1636,7 +1636,7 @@ var SmartRouter = class {
1636
1636
  const router = routers[i];
1637
1637
  try {
1638
1638
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) router.add(...routes[i2]);
1639
- res = router.match(method, path10);
1639
+ res = router.match(method, path11);
1640
1640
  } catch (e) {
1641
1641
  if (e instanceof UnsupportedPathError) continue;
1642
1642
  throw e;
@@ -1665,9 +1665,9 @@ var Node3 = class Node4 {
1665
1665
  #patterns = [];
1666
1666
  #pattern;
1667
1667
  #params = emptyParams;
1668
- insert(method, path10, handler) {
1668
+ insert(method, path11, handler) {
1669
1669
  let curNode = this;
1670
- const parts = splitRoutingPath(path10);
1670
+ const parts = splitRoutingPath(path11);
1671
1671
  const possibleKeys = /* @__PURE__ */ new Set();
1672
1672
  let i = 0;
1673
1673
  for (const p of parts) {
@@ -1703,11 +1703,11 @@ var Node3 = class Node4 {
1703
1703
  }
1704
1704
  }
1705
1705
  }
1706
- search(method, path10) {
1706
+ search(method, path11) {
1707
1707
  const handlerSets = [];
1708
1708
  this.#params = emptyParams;
1709
1709
  let curNodes = [this];
1710
- const parts = splitPath(path10);
1710
+ const parts = splitPath(path11);
1711
1711
  const curNodesQueue = [];
1712
1712
  const len = parts.length;
1713
1713
  let partOffsets = null;
@@ -1743,13 +1743,13 @@ var Node3 = class Node4 {
1743
1743
  if (matcher !== true) {
1744
1744
  if (!partOffsets) {
1745
1745
  partOffsets = [];
1746
- let offset = path10[0] === "/" ? 1 : 0;
1746
+ let offset = path11[0] === "/" ? 1 : 0;
1747
1747
  for (let p = 0; p < len; p++) {
1748
1748
  partOffsets[p] = offset;
1749
1749
  offset += parts[p].length + 1;
1750
1750
  }
1751
1751
  }
1752
- const restPathString = path10.slice(partOffsets[i]);
1752
+ const restPathString = path11.slice(partOffsets[i]);
1753
1753
  const m = matcher.exec(restPathString);
1754
1754
  if (m) {
1755
1755
  if (!Object.hasOwn(params, name)) params[name] = m[0];
@@ -1790,11 +1790,11 @@ var Node3 = class Node4 {
1790
1790
  var TrieRouter = class {
1791
1791
  name = "TrieRouter";
1792
1792
  #node = new Node3();
1793
- add(method, path10, handler) {
1794
- for (const result of checkOptionalParameter(path10) || [path10]) this.#node.insert(method, result, handler);
1793
+ add(method, path11, handler) {
1794
+ for (const result of checkOptionalParameter(path11) || [path11]) this.#node.insert(method, result, handler);
1795
1795
  }
1796
- match(method, path10) {
1797
- return this.#node.search(method, path10);
1796
+ match(method, path11) {
1797
+ return this.#node.search(method, path11);
1798
1798
  }
1799
1799
  };
1800
1800
 
@@ -1844,16 +1844,16 @@ var StreamingApi = class {
1844
1844
  }
1845
1845
  });
1846
1846
  }
1847
- async write(input) {
1847
+ async write(input2) {
1848
1848
  try {
1849
- if (typeof input === "string") input = this.encoder.encode(input);
1850
- await this.writer.write(input);
1849
+ if (typeof input2 === "string") input2 = this.encoder.encode(input2);
1850
+ await this.writer.write(input2);
1851
1851
  } catch {
1852
1852
  }
1853
1853
  return this;
1854
1854
  }
1855
- async writeln(input) {
1856
- await this.write(input + "\n");
1855
+ async writeln(input2) {
1856
+ await this.write(input2 + "\n");
1857
1857
  return this;
1858
1858
  }
1859
1859
  sleep(ms) {
@@ -1979,15 +1979,15 @@ var toRequestError = (e) => {
1979
1979
  };
1980
1980
  var GlobalRequest = global.Request;
1981
1981
  var Request2 = class extends GlobalRequest {
1982
- constructor(input, options) {
1983
- if (typeof input === "object" && getRequestCache in input) {
1984
- input = input[getRequestCache]();
1982
+ constructor(input2, options) {
1983
+ if (typeof input2 === "object" && getRequestCache in input2) {
1984
+ input2 = input2[getRequestCache]();
1985
1985
  }
1986
1986
  if (typeof options?.body?.getReader !== "undefined") {
1987
1987
  ;
1988
1988
  options.duplex ??= "half";
1989
1989
  }
1990
- super(input, options);
1990
+ super(input2, options);
1991
1991
  }
1992
1992
  };
1993
1993
  var newHeadersFromIncoming = (incoming) => {
@@ -3166,6 +3166,26 @@ function buildRuns({ cwd, entries, live }) {
3166
3166
  }
3167
3167
 
3168
3168
  // server/chats.ts
3169
+ var SessionActionError = class extends Error {
3170
+ status;
3171
+ uncertain;
3172
+ constructor(message, status = 409, uncertain = false) {
3173
+ super(message);
3174
+ this.status = status;
3175
+ this.uncertain = uncertain;
3176
+ }
3177
+ };
3178
+ var CLIENT_COMMANDS = /* @__PURE__ */ new Set(["login", "model", "compact", "new"]);
3179
+ async function boundedAction(operation, ms, name) {
3180
+ let timer;
3181
+ try {
3182
+ return await Promise.race([operation, new Promise((_, reject) => {
3183
+ timer = setTimeout(() => reject(new SessionActionError(`${name} is still awaiting Pi. Delivery/completion is uncertain; do not retry. Read the chat's latest state.`, 504, true)), ms);
3184
+ })]);
3185
+ } finally {
3186
+ clearTimeout(timer);
3187
+ }
3188
+ }
3169
3189
  var toolLine = (name, args) => {
3170
3190
  const a = args ?? {};
3171
3191
  if (name === "bash") return `bash \xB7 ${String(a.command ?? "").split("\n")[0].slice(0, 90)}`;
@@ -3238,10 +3258,15 @@ var Chat = class {
3238
3258
  file = null;
3239
3259
  sessionName = null;
3240
3260
  autoNamed = false;
3261
+ sessionReady = false;
3262
+ action = null;
3263
+ compaction = { status: "idle" };
3264
+ currentModel;
3265
+ modelRevision = 0;
3266
+ modelsRequireRestart = false;
3241
3267
  constructor(id, options) {
3242
3268
  this.id = id;
3243
3269
  this.options = options;
3244
- this.model = options.model ?? null;
3245
3270
  this.pi = new PiRpc({
3246
3271
  command: options.piCommand,
3247
3272
  args: ["--session-dir", options.sessionDir, ...options.sessionFile ? ["--session", options.sessionFile] : ["--session-id", id], ...options.args ?? [], ...options.model ? ["--model", options.model] : []],
@@ -3281,6 +3306,12 @@ var Chat = class {
3281
3306
  get busy() {
3282
3307
  return !!this.live?.busy;
3283
3308
  }
3309
+ get setupBlocked() {
3310
+ return !this.sessionReady && this.dialogs.length > 0;
3311
+ }
3312
+ get sessionActions() {
3313
+ return { setupBlocked: this.setupBlocked, busy: this.busy || this.runs.some((r) => r.alive) || this.liveLoops.size > 0 || !!this.action, compaction: { ...this.compaction } };
3314
+ }
3284
3315
  touch() {
3285
3316
  try {
3286
3317
  this.options.onUpdate?.(this);
@@ -3325,22 +3356,25 @@ var Chat = class {
3325
3356
  }
3326
3357
  /** Re-read the session's entries (cursor-based) and messages, then rebuild the runs. */
3327
3358
  async refresh(full = false) {
3359
+ if (this.stopping) return;
3328
3360
  if (this.refreshing) return this.refreshing;
3329
3361
  this.refreshing = (async () => {
3330
3362
  try {
3331
3363
  if (full) this.cursor = void 0;
3332
- const entries = await this.pi.send({ type: "get_entries", ...this.cursor ? { since: this.cursor } : {} });
3364
+ const entries = await this.pi.send({ type: "get_entries", ...this.cursor ? { since: this.cursor } : {} }, 1e4);
3333
3365
  const got = entries.entries ?? [];
3334
3366
  if (this.cursor) this.entries = got.length ? [...this.entries, ...got] : this.entries;
3335
3367
  else this.entries = got;
3336
3368
  this.cursor = got.at(-1)?.id ?? this.cursor;
3337
- const messages = await this.pi.send({ type: "get_messages" });
3369
+ const messages = await this.pi.send({ type: "get_messages" }, 1e4);
3338
3370
  if (process.env.LOOPS_UI_DEBUG) console.error(`[${this.id}] refresh: ${got.length} new entries, ${(messages.messages ?? []).map((m) => `${m.role}[${Array.isArray(m.content) ? m.content.map((c) => c.type).join("+") : typeof m.content}]`).join(" ")}`);
3339
3371
  this.items = itemsFromMessages(messages.messages ?? []);
3340
- const state = await this.pi.send({ type: "get_state" }).catch(() => ({}));
3372
+ const modelRevision = this.modelRevision;
3373
+ const state = await this.pi.send({ type: "get_state" }, 1e4).catch(() => null);
3341
3374
  const st = state;
3342
3375
  if (st?.sessionName) this.sessionName = String(st.sessionName);
3343
- if (st?.model?.id) this.model = `${st.model.provider ?? ""}/${st.model.id}`.replace(/^\//, "");
3376
+ if (st) this.readModel(st.model, modelRevision);
3377
+ this.sessionReady = true;
3344
3378
  this.runs = buildRuns({ cwd: this.options.cwd, entries: this.entries, live: this.liveLoops });
3345
3379
  const auto = autoNameFor(this.runs, this.sessionName, this.autoNamed);
3346
3380
  if (auto) {
@@ -3350,8 +3384,10 @@ var Chat = class {
3350
3384
  }
3351
3385
  this.refreshError = "";
3352
3386
  } catch (e) {
3353
- console.error(`[${this.id}] refresh failed:`, e.message ?? e);
3354
- this.refreshError = String(e.message ?? e);
3387
+ if (!this.stopping) {
3388
+ console.error(`[${this.id}] refresh failed:`, e.message ?? e);
3389
+ this.refreshError = String(e.message ?? e);
3390
+ }
3355
3391
  } finally {
3356
3392
  this.refreshing = null;
3357
3393
  this.broadcast(true);
@@ -3426,6 +3462,22 @@ var Chat = class {
3426
3462
  this.touch();
3427
3463
  }
3428
3464
  async prompt(text, behavior) {
3465
+ const candidate = text.trimStart();
3466
+ if (candidate.startsWith("/")) {
3467
+ const slash = /^\/([^\s]+)([\s\S]*)$/.exec(candidate);
3468
+ if (!slash) throw new SessionActionError("A discovered slash command name is required.", 400);
3469
+ await this.validateCommand(slash[1]);
3470
+ text = `/${slash[1]}${slash[2].replace(/^\s/, " ")}`;
3471
+ }
3472
+ if (this.action && !/^\s*\/loop\s+stop(?:\s|$)/.test(text)) throw new SessionActionError("A session action is in progress. Wait for Pi to finish.");
3473
+ return this.dispatchPrompt(text, behavior);
3474
+ }
3475
+ /** A server-owned safety control, not an arbitrary slash/RPC bypass. Run ownership is checked by the route. */
3476
+ async stopLoop(run3) {
3477
+ if (!/^[\w.-]+$/.test(run3)) throw new SessionActionError("Exact run ID required.", 400);
3478
+ return this.dispatchPrompt(`/loop stop ${run3}`);
3479
+ }
3480
+ async dispatchPrompt(text, behavior) {
3429
3481
  const previous = this.live;
3430
3482
  if (!text.startsWith("/") && !this.live) this.live = { busy: true, chat: this.id, startedAt: Date.now(), user: text, text: "", tools: [] };
3431
3483
  this.broadcast(true);
@@ -3508,19 +3560,148 @@ var Chat = class {
3508
3560
  stateFor(run3) {
3509
3561
  return stateFromEntries(this.entries, run3);
3510
3562
  }
3511
- /** The models this chat could run, and what each one can take (text only, or images too). */
3563
+ readModel(model, revision = this.modelRevision) {
3564
+ if (revision !== this.modelRevision) return;
3565
+ if (typeof model?.id === "string" && typeof model?.provider === "string") {
3566
+ this.currentModel = { provider: model.provider, id: model.id };
3567
+ this.model = `${model.provider}/${model.id}`;
3568
+ } else if (model == null) {
3569
+ this.currentModel = void 0;
3570
+ this.model = null;
3571
+ }
3572
+ }
3573
+ /** Only Pi's dynamic prompt-invocable resources, never terminal-only builtins. */
3574
+ async commands() {
3575
+ if (this.setupBlocked) return { commands: [], supported: false, setupBlocked: true, error: "Complete Pi's startup decision before loading commands." };
3576
+ try {
3577
+ const result = await this.pi.send({ type: "get_commands" }, 1e4);
3578
+ if (!Array.isArray(result?.commands)) return { commands: [], supported: false, setupBlocked: false, error: "This Pi executable does not support command discovery." };
3579
+ return {
3580
+ commands: result.commands.filter((c) => c && typeof c.name === "string" && !!c.name && !/[\s/]/.test(c.name) && ["extension", "prompt", "skill"].includes(c.source)).map((c) => ({ name: c.name, source: c.source, ...typeof c.description === "string" ? { description: c.description } : {} })),
3581
+ supported: true,
3582
+ setupBlocked: false
3583
+ };
3584
+ } catch (error) {
3585
+ return { commands: [], supported: false, setupBlocked: false, error: String(error.message ?? error) };
3586
+ }
3587
+ }
3588
+ async validateCommand(name) {
3589
+ const catalog = await this.commands();
3590
+ if (catalog.commands.some((command) => command.name === name)) return;
3591
+ if (CLIENT_COMMANDS.has(name)) throw new SessionActionError(`/${name} is a browser action. Use ${name === "new" ? "New chat" : name === "login" ? "provider settings" : `the ${name} control`} instead.`, 400);
3592
+ if (!catalog.supported) throw new SessionActionError(catalog.error ?? "Pi command discovery is unavailable; slash commands cannot be safely sent.", 409);
3593
+ throw new SessionActionError(`Unknown or unsupported slash command: /${name}. It was not sent to the model.`, 400);
3594
+ }
3595
+ /** Public RPC returns Pi's available snapshot; never manufacture provider/model spellings. */
3596
+ async models() {
3597
+ if (this.setupBlocked) return { models: [], current: this.currentModel, setupBlocked: true, error: "Complete Pi's startup decision before loading models." };
3598
+ const modelRevision = this.modelRevision;
3599
+ const [available2, state] = await Promise.all([
3600
+ this.pi.send({ type: "get_available_models" }, 1e4),
3601
+ this.pi.send({ type: "get_state" }, 1e4)
3602
+ ]);
3603
+ this.readModel(state?.model, modelRevision);
3604
+ return {
3605
+ // Custom SDK models may carry API headers or credential-bearing base URLs. Only this display DTO crosses HTTP.
3606
+ models: (available2?.models ?? []).filter((m) => m && typeof m.id === "string" && typeof m.provider === "string").map((m) => ({ id: m.id, provider: m.provider, ...typeof m.name === "string" ? { name: m.name } : {}, ...Array.isArray(m.input) ? { input: m.input.filter((v) => typeof v === "string") } : {} })),
3607
+ current: this.currentModel,
3608
+ setupBlocked: false,
3609
+ ...this.modelsRequireRestart ? { requiresRestart: true, warning: "This Pi executable cannot refresh model credentials over RPC. Restart the agent explicitly after work finishes to see newly configured models." } : {}
3610
+ };
3611
+ }
3612
+ /** The bundled compatibility runner refreshes through Pi's public ModelRuntime API, without network calls. */
3613
+ async refreshModels() {
3614
+ if (this.setupBlocked || this.sessionActions.busy) return this.models();
3615
+ this.action = "metadata";
3616
+ this.broadcast(true);
3617
+ const operation = (async () => {
3618
+ try {
3619
+ await this.pi.send({ type: "refresh_available_models" });
3620
+ this.modelsRequireRestart = false;
3621
+ } catch (error) {
3622
+ if (!/unknown command|unsupported|not supported/i.test(String(error.message ?? error))) throw error;
3623
+ this.modelsRequireRestart = true;
3624
+ } finally {
3625
+ this.action = null;
3626
+ this.broadcast(true);
3627
+ }
3628
+ })();
3629
+ await boundedAction(operation, 1e4, "Model metadata refresh");
3630
+ return this.models();
3631
+ }
3632
+ /** Compatibility for the existing config editor's composite model IDs. */
3512
3633
  async availableModels() {
3513
3634
  try {
3514
- const list = await this.pi.send({ type: "get_available_models" });
3635
+ const list = await this.models();
3515
3636
  return (list.models ?? []).map((m) => ({ id: `${m.provider}/${m.id}`, provider: m.provider, model: m.id, images: Array.isArray(m.input) ? m.input.includes("image") : null }));
3516
3637
  } catch {
3517
3638
  return [];
3518
3639
  }
3519
3640
  }
3520
- async setModel(provider, model) {
3521
- await this.pi.send({ type: "set_model", provider, modelId: model });
3522
- this.model = `${provider}/${model}`;
3641
+ async beginAction(action) {
3642
+ if (this.setupBlocked) throw new SessionActionError("Complete Pi's startup decision first.");
3643
+ if (this.action) throw new SessionActionError("A session action is already in progress. Do not retry until Pi finishes.");
3644
+ this.action = action;
3523
3645
  this.broadcast(true);
3646
+ try {
3647
+ await this.refresh();
3648
+ const state = await this.pi.send({ type: "get_state" }, 1e4);
3649
+ if (this.busy || this.runs.some((r) => r.alive) || this.liveLoops.size || state.isStreaming || state.isCompacting || state.pendingMessageCount) {
3650
+ throw new SessionActionError("Stop or finish the parent agent and background loops before changing the model or compacting.");
3651
+ }
3652
+ } catch (error) {
3653
+ this.action = null;
3654
+ this.broadcast(true);
3655
+ throw error;
3656
+ }
3657
+ }
3658
+ async setModel(provider, modelId) {
3659
+ if (!provider || !modelId) throw new SessionActionError("Exact provider and modelId are required.", 400);
3660
+ await this.beginAction("model");
3661
+ const operation = (async () => {
3662
+ try {
3663
+ const models = await this.pi.send({ type: "get_available_models" }, 1e4);
3664
+ if (!models.models?.some((m) => m.provider === provider && m.id === modelId)) throw new SessionActionError("The exact model is not in Pi's available catalog.", 400);
3665
+ const result = await this.pi.send({ type: "set_model", provider, modelId });
3666
+ this.modelRevision++;
3667
+ this.readModel(result?.provider && result?.id ? result : { provider, id: modelId });
3668
+ return this.currentModel;
3669
+ } finally {
3670
+ this.action = null;
3671
+ this.broadcast(true);
3672
+ }
3673
+ })();
3674
+ return boundedAction(operation, 2e4, "Model change");
3675
+ }
3676
+ async compact(customInstructions) {
3677
+ if (customInstructions !== void 0 && typeof customInstructions !== "string") throw new SessionActionError("customInstructions must be text.", 400);
3678
+ await this.beginAction("compact");
3679
+ this.compaction = { status: "running", startedAt: Date.now() };
3680
+ this.broadcast(true);
3681
+ const operation = (async () => {
3682
+ try {
3683
+ const result = await this.pi.send({ type: "compact", ...customInstructions !== void 0 ? { customInstructions } : {} });
3684
+ this.compaction = { ...this.compaction, status: "succeeded", finishedAt: Date.now(), result };
3685
+ if (this.refreshing) await this.refreshing;
3686
+ await this.refresh(true);
3687
+ return result;
3688
+ } catch (error) {
3689
+ this.compaction = { ...this.compaction, status: "error", finishedAt: Date.now(), error: String(error.message ?? error) };
3690
+ throw error;
3691
+ } finally {
3692
+ this.action = null;
3693
+ this.broadcast(true);
3694
+ }
3695
+ })();
3696
+ try {
3697
+ return await boundedAction(operation, 12e4, "Compaction");
3698
+ } catch (error) {
3699
+ if (error instanceof SessionActionError && error.uncertain && this.compaction.status === "running") {
3700
+ this.compaction = { ...this.compaction, status: "uncertain", error: error.message };
3701
+ this.broadcast(true);
3702
+ }
3703
+ throw error;
3704
+ }
3524
3705
  }
3525
3706
  /** An attempt's transcript, straight from the session file the attempt forked into. */
3526
3707
  sessionItems(file) {
@@ -3557,6 +3738,7 @@ var Chat = class {
3557
3738
  live: this.live,
3558
3739
  runs,
3559
3740
  model: this.model,
3741
+ sessionActions: this.sessionActions,
3560
3742
  error: this.pi.running ? this.refreshError || void 0 : this.exitError || this.refreshError || void 0,
3561
3743
  questions: runs.flatMap((r) => r.questions),
3562
3744
  dialogs: this.dialogs,
@@ -3933,9 +4115,9 @@ async function connect(wsUrl) {
3933
4115
  sock.addEventListener("open", () => res());
3934
4116
  sock.addEventListener("error", () => rej(new Error("could not connect to the browser's DevTools endpoint")));
3935
4117
  });
3936
- const send = (method, params = {}, sessionId) => new Promise((ok, fail) => {
4118
+ const send = (method, params = {}, sessionId) => new Promise((ok, fail2) => {
3937
4119
  const n = ++id;
3938
- pending.set(n, { ok, fail });
4120
+ pending.set(n, { ok, fail: fail2 });
3939
4121
  sock.send(JSON.stringify({ id: n, method, params, ...sessionId ? { sessionId } : {} }));
3940
4122
  });
3941
4123
  return {
@@ -4733,9 +4915,377 @@ function originalBaseline(state) {
4733
4915
  return state.rounds?.find((round) => round.n === 1)?.from || state.baseCommit;
4734
4916
  }
4735
4917
 
4918
+ // server/session-actions.ts
4919
+ function createSessionActions(options) {
4920
+ const routes = new Hono3();
4921
+ const chat = async (id) => {
4922
+ if (typeof id !== "string" || !id.trim()) throw new SessionActionError("chat required", 400);
4923
+ return options.requireChat(id);
4924
+ };
4925
+ routes.onError((error, c) => {
4926
+ if (error instanceof HTTPException) return c.json({ error: error.message }, error.status);
4927
+ if (error instanceof SessionActionError) return c.json({ error: error.message, ...error.uncertain ? { uncertain: true } : {} }, error.status);
4928
+ return c.json({ error: error.message }, 502);
4929
+ });
4930
+ routes.get("/commands", async (c) => c.json(await (await chat(c.req.query("chat"))).commands()));
4931
+ routes.get("/models", async (c) => {
4932
+ const target = await chat(c.req.query("chat"));
4933
+ return c.json(await (target.sessionActions.busy ? target.models() : target.refreshModels()));
4934
+ });
4935
+ routes.post("/models", async (c) => {
4936
+ const body = await c.req.json().catch(() => null);
4937
+ if (typeof body?.provider !== "string" || !body.provider || typeof body?.modelId !== "string" || !body.modelId) throw new SessionActionError("Exact provider and modelId are required.", 400);
4938
+ const current = await (await chat(body.chat)).setModel(body.provider, body.modelId);
4939
+ return c.json({ ok: true, current });
4940
+ });
4941
+ routes.post("/compact", async (c) => {
4942
+ const body = await c.req.json().catch(() => null);
4943
+ if (body?.customInstructions !== void 0 && typeof body.customInstructions !== "string") throw new SessionActionError("customInstructions must be text.", 400);
4944
+ const result = await (await chat(body?.chat)).compact(body?.customInstructions);
4945
+ return c.json({ ok: true, result });
4946
+ });
4947
+ routes.post("/commands/run", async (c) => {
4948
+ const body = await c.req.json().catch(() => null);
4949
+ if (typeof body?.name !== "string" || !body.name || /[\s/]/.test(body.name)) throw new SessionActionError("An exact discovered command name (without /) is required.", 400);
4950
+ if (body.args !== void 0 && typeof body.args !== "string") throw new SessionActionError("Command args must be text.", 400);
4951
+ const target = await chat(body.chat);
4952
+ const disposition = await target.prompt(`/${body.name}${body.args ? ` ${body.args}` : ""}`);
4953
+ return c.json({ disposition });
4954
+ });
4955
+ return routes;
4956
+ }
4957
+
4958
+ // server/provider-auth.ts
4959
+ import { randomUUID } from "node:crypto";
4960
+ import path9 from "node:path";
4961
+
4962
+ // ../../node_modules/.pnpm/hono@4.13.14/node_modules/hono/dist/middleware/body-limit/index.js
4963
+ var ERROR_MESSAGE = "Payload Too Large";
4964
+ var bodyLimit = (options) => {
4965
+ const onError = options.onError || (() => {
4966
+ const res = new Response(ERROR_MESSAGE, { status: 413 });
4967
+ throw new HTTPException(413, { res });
4968
+ });
4969
+ const maxSize = options.maxSize;
4970
+ return async function bodyLimit2(c, next) {
4971
+ if (!c.req.raw.body) return next();
4972
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
4973
+ if (c.req.raw.headers.has("content-length") && !hasTransferEncoding) return parseInt(c.req.raw.headers.get("content-length") || "0", 10) > maxSize ? onError(c) : next();
4974
+ let size = 0;
4975
+ const chunks = [];
4976
+ const rawReader = c.req.raw.body.getReader();
4977
+ for (; ; ) {
4978
+ const { done, value } = await rawReader.read();
4979
+ if (done) break;
4980
+ size += value.length;
4981
+ if (size > maxSize) return onError(c);
4982
+ chunks.push(value);
4983
+ }
4984
+ const requestInit = {
4985
+ body: new ReadableStream({ start(controller) {
4986
+ for (const chunk of chunks) controller.enqueue(chunk);
4987
+ controller.close();
4988
+ } }),
4989
+ duplex: "half"
4990
+ };
4991
+ c.req.raw = new Request(c.req.raw, requestInit);
4992
+ return next();
4993
+ };
4994
+ };
4995
+
4996
+ // server/provider-auth.ts
4997
+ var AuthRequestError = class extends Error {
4998
+ status;
4999
+ constructor(status, message) {
5000
+ super(message);
5001
+ this.status = status;
5002
+ }
5003
+ };
5004
+ var fail = (status, message) => {
5005
+ throw new AuthRequestError(status, message);
5006
+ };
5007
+ var MAX_FLOW_MS = 15 * 6e4;
5008
+ var TERMINAL_MS = 6e4;
5009
+ var MAX_INPUT = 16384;
5010
+ var copy = (value) => structuredClone(value);
5011
+ function httpsUrl(value) {
5012
+ try {
5013
+ const url = new URL(value);
5014
+ if (url.protocol === "https:" && !url.username && !url.password && value.length <= MAX_INPUT) return url.href;
5015
+ } catch {
5016
+ }
5017
+ return void 0;
5018
+ }
5019
+ function input(value, label, max = MAX_INPUT) {
5020
+ if (typeof value !== "string" || !value.trim() || value.length > max || value.includes("\0")) fail(400, `Invalid ${label}`);
5021
+ return value;
5022
+ }
5023
+ function keyInput(value) {
5024
+ const key = input(value, "API key", 8192).trim();
5025
+ if (key.startsWith("!") || /[\r\n]/.test(key)) fail(400, "A literal, single-line API key is required");
5026
+ return key;
5027
+ }
5028
+ var ProviderAuth = class {
5029
+ runtime;
5030
+ options;
5031
+ flow;
5032
+ active;
5033
+ waiting;
5034
+ terminalAt = 0;
5035
+ closed = false;
5036
+ constructor(runtime, options = {}) {
5037
+ this.runtime = runtime;
5038
+ this.options = options;
5039
+ }
5040
+ async snapshot() {
5041
+ const stored = new Set((await this.runtime.listCredentials()).map((c) => c.providerId));
5042
+ const providers = this.runtime.getProviders().map((p) => ({
5043
+ id: p.id,
5044
+ name: p.name,
5045
+ oauth: Boolean(p.auth.oauth),
5046
+ apiKey: Boolean(p.auth.apiKey?.login),
5047
+ connected: stored.has(p.id)
5048
+ })).sort((a, b) => a.name.localeCompare(b.name));
5049
+ if (!this.active && this.flow && Date.now() - this.terminalAt > TERMINAL_MS) this.flow = void 0;
5050
+ return { providers, ...this.flow ? { flow: copy(this.flow) } : {} };
5051
+ }
5052
+ start(provider, key) {
5053
+ if (this.closed) fail(503, "Provider login is unavailable");
5054
+ if (this.active) fail(409, "A provider connection is already in progress");
5055
+ const id = input(provider, "provider", 128);
5056
+ const selected = this.runtime.getProviders().find((p) => p.id === id);
5057
+ if (!selected) fail(400, "Unknown provider");
5058
+ const type = key === void 0 ? "oauth" : "api_key";
5059
+ if (type === "oauth" ? !selected.auth.oauth : !selected.auth.apiKey?.login) fail(400, "Login method is not supported");
5060
+ let suppliedKey = type === "api_key" ? keyInput(key) : void 0;
5061
+ const controller = new AbortController();
5062
+ const flow = { id: randomUUID(), provider: id, status: "pending" };
5063
+ this.flow = flow;
5064
+ const interaction = {
5065
+ signal: controller.signal,
5066
+ notify: (event) => {
5067
+ if (this.flow !== flow || flow.status !== "pending" || controller.signal.aborted) return;
5068
+ this.notify(flow, event);
5069
+ },
5070
+ prompt: (prompt) => {
5071
+ controller.signal.throwIfAborted();
5072
+ prompt.signal?.throwIfAborted();
5073
+ if (suppliedKey !== void 0 && prompt.type === "secret") {
5074
+ const value = suppliedKey;
5075
+ suppliedKey = void 0;
5076
+ return Promise.resolve(value);
5077
+ }
5078
+ return this.prompt(flow, prompt, controller.signal, type === "api_key");
5079
+ }
5080
+ };
5081
+ const timeout = Math.min(MAX_FLOW_MS, Math.max(1, this.options.timeoutMs ?? MAX_FLOW_MS));
5082
+ const timer = setTimeout(() => this.stop(flow.id, "expired"), timeout);
5083
+ timer.unref();
5084
+ const active = { controller, timer, done: Promise.resolve(), deadline: Date.now() + timeout };
5085
+ this.active = active;
5086
+ active.done = Promise.resolve().then(async () => {
5087
+ try {
5088
+ await this.runtime.login(id, type, interaction, this.options.loginOptions);
5089
+ if (flow.status === "pending") this.finish(flow, "succeeded");
5090
+ this.changed(id);
5091
+ } catch (error) {
5092
+ if (error instanceof Error && error.name === "CredentialSynchronizationError") {
5093
+ if (flow.status === "pending") this.finish(flow, "succeeded");
5094
+ this.changed(id);
5095
+ } else if (flow.status === "pending") {
5096
+ this.finish(flow, "failed", "Connection failed. Check the provider sign-in requirements and try again.");
5097
+ }
5098
+ } finally {
5099
+ suppliedKey = void 0;
5100
+ clearTimeout(active.timer);
5101
+ this.waiting?.reject(new Error("Login finished"));
5102
+ if (this.active === active) this.active = void 0;
5103
+ }
5104
+ });
5105
+ return copy(flow);
5106
+ }
5107
+ answer(id, promptId, value) {
5108
+ const flow = this.current(id);
5109
+ if (!this.waiting || this.waiting.id !== promptId || flow.prompt?.id !== promptId) fail(409, "This login prompt is no longer active");
5110
+ if (typeof value !== "string" || value.length > MAX_INPUT || value.includes("\0")) fail(400, "Invalid login answer");
5111
+ if (flow.prompt?.type === "select" && !flow.prompt.options?.some((o) => o.id === value)) fail(400, "Invalid login option");
5112
+ this.waiting.resolve(value);
5113
+ return copy(flow);
5114
+ }
5115
+ cancel(id) {
5116
+ const flow = this.current(id);
5117
+ this.stop(flow.id, "cancelled");
5118
+ return copy(flow);
5119
+ }
5120
+ /** Abort pending SDK interactions during server shutdown (including loopback listeners). */
5121
+ async close() {
5122
+ this.closed = true;
5123
+ const active = this.active;
5124
+ if (active && this.flow?.status === "pending") this.stop(this.flow.id, "cancelled");
5125
+ await active?.done;
5126
+ }
5127
+ current(id) {
5128
+ if (typeof id !== "string" || id !== this.flow?.id) return fail(404, "Unknown or stale login flow");
5129
+ if (!this.active || this.flow.status !== "pending") return fail(409, "This login flow is no longer active");
5130
+ return this.flow;
5131
+ }
5132
+ changed(provider) {
5133
+ void Promise.resolve().then(() => this.options.onCredentialsChanged?.(provider)).catch(() => {
5134
+ });
5135
+ }
5136
+ stop(id, status) {
5137
+ if (this.flow?.id !== id || this.flow.status !== "pending") return;
5138
+ this.finish(this.flow, status, status === "expired" ? "Sign-in expired. Start a new connection." : void 0);
5139
+ this.active?.controller.abort(new Error("Login cancelled"));
5140
+ this.waiting?.reject(new Error("Login cancelled"));
5141
+ }
5142
+ finish(flow, status, error) {
5143
+ for (const key of ["authorizationUrl", "instructions", "prompt", "deviceCode"]) delete flow[key];
5144
+ flow.status = status;
5145
+ if (error) flow.error = error;
5146
+ this.terminalAt = Date.now();
5147
+ }
5148
+ notify(flow, event) {
5149
+ if (event.type === "auth_url") {
5150
+ const url = httpsUrl(event.url);
5151
+ if (!url) throw new Error("Unsafe authorization URL");
5152
+ flow.authorizationUrl = url;
5153
+ flow.instructions = event.instructions?.slice(0, 4096);
5154
+ } else if (event.type === "device_code") {
5155
+ const url = httpsUrl(event.verificationUri);
5156
+ if (!url) throw new Error("Unsafe verification URL");
5157
+ const expires = event.expiresInSeconds;
5158
+ const lifetime = typeof expires === "number" && Number.isFinite(expires) && expires > 0 ? expires : void 0;
5159
+ flow.authorizationUrl = url;
5160
+ flow.deviceCode = {
5161
+ userCode: event.userCode.slice(0, 256),
5162
+ verificationUri: url,
5163
+ ...lifetime === void 0 ? {} : { expiresInSeconds: lifetime }
5164
+ };
5165
+ if (lifetime !== void 0 && this.active) {
5166
+ const remaining = Math.min(this.active.deadline - Date.now(), lifetime * 1e3);
5167
+ clearTimeout(this.active.timer);
5168
+ this.active.deadline = Date.now() + remaining;
5169
+ this.active.timer = setTimeout(() => this.stop(flow.id, "expired"), Math.max(1, remaining));
5170
+ this.active.timer.unref();
5171
+ }
5172
+ } else if (event.type === "info") {
5173
+ flow.instructions = event.message.slice(0, 4096);
5174
+ }
5175
+ }
5176
+ prompt(flow, prompt, signal, apiKey) {
5177
+ if (this.waiting) return Promise.reject(new Error("Concurrent login prompts are not supported"));
5178
+ const id = randomUUID();
5179
+ flow.prompt = {
5180
+ id,
5181
+ type: prompt.type,
5182
+ message: prompt.message.slice(0, 4096),
5183
+ // Pi text prompts include "blank for github.com" and "press Enter to
5184
+ // continue". The public prompt API has no required flag; let the provider
5185
+ // validate text input instead of inventing a browser-only restriction.
5186
+ ...prompt.type === "text" ? { allowEmpty: true } : {},
5187
+ ..."placeholder" in prompt ? { placeholder: prompt.placeholder?.slice(0, 1024) } : {},
5188
+ ...prompt.type === "select" ? { options: prompt.options.map((o) => ({ ...o })) } : {}
5189
+ };
5190
+ return new Promise((resolve, reject) => {
5191
+ const signals = [signal, ...prompt.signal ? [prompt.signal] : []];
5192
+ const cleanup = () => {
5193
+ signals.forEach((s) => s.removeEventListener("abort", abort));
5194
+ if (this.waiting?.id === id) this.waiting = void 0;
5195
+ if (flow.prompt?.id === id) delete flow.prompt;
5196
+ };
5197
+ const abort = () => {
5198
+ cleanup();
5199
+ reject(new Error("Login prompt cancelled"));
5200
+ };
5201
+ this.waiting = {
5202
+ id,
5203
+ resolve: (value) => {
5204
+ if (apiKey && value.trim().startsWith("!")) {
5205
+ cleanup();
5206
+ reject(new Error("Literal input required"));
5207
+ return;
5208
+ }
5209
+ cleanup();
5210
+ resolve(value);
5211
+ },
5212
+ reject: (error) => {
5213
+ cleanup();
5214
+ reject(error);
5215
+ }
5216
+ };
5217
+ signals.forEach((s) => s.addEventListener("abort", abort, { once: true }));
5218
+ if (signals.some((s) => s.aborted)) abort();
5219
+ });
5220
+ }
5221
+ };
5222
+ async function createProviderAuth(options = {}) {
5223
+ if (options.runtime) return new ProviderAuth(options.runtime, options);
5224
+ const { ModelRuntime, SettingsManager, getAgentDir } = await import("@earendil-works/pi-coding-agent");
5225
+ const runtime = await ModelRuntime.create({
5226
+ authPath: options.authPath,
5227
+ allowModelNetwork: false,
5228
+ refreshOnCreate: false
5229
+ });
5230
+ let settings;
5231
+ const agentDir3 = options.authPath ? path9.dirname(options.authPath) : getAgentDir();
5232
+ return new ProviderAuth(runtime, {
5233
+ ...options,
5234
+ loginOptions: options.loginOptions ?? {
5235
+ getDeviceId: () => {
5236
+ settings ??= SettingsManager.create(agentDir3, agentDir3, { projectTrusted: false });
5237
+ return settings.getOrCreateDeviceId();
5238
+ }
5239
+ }
5240
+ });
5241
+ }
5242
+ function mountProviderAuthRoutes(api2, auth) {
5243
+ api2.use("/providers", async (c, next) => {
5244
+ c.header("Cache-Control", "no-store");
5245
+ c.header("Referrer-Policy", "no-referrer");
5246
+ await next();
5247
+ });
5248
+ api2.use("/providers/*", async (c, next) => {
5249
+ c.header("Cache-Control", "no-store");
5250
+ c.header("Referrer-Policy", "no-referrer");
5251
+ await next();
5252
+ });
5253
+ api2.use("/providers/*", bodyLimit({ maxSize: 32768, onError: (c) => c.json({ error: "Login request is too large" }, 413) }));
5254
+ const safe = async (fn, c) => {
5255
+ try {
5256
+ return c.json(await fn());
5257
+ } catch (error) {
5258
+ if (error instanceof AuthRequestError) return c.json({ error: error.message }, error.status);
5259
+ return c.json({ error: "Provider connection is unavailable" }, 503);
5260
+ }
5261
+ };
5262
+ const body = async (c, fields) => {
5263
+ const value = await c.req.json().catch(() => null);
5264
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((k) => !fields.includes(k))) fail(400, "Invalid login request");
5265
+ return value;
5266
+ };
5267
+ api2.get("/providers", (c) => safe(() => auth.snapshot(), c));
5268
+ api2.post("/providers/login", (c) => safe(async () => {
5269
+ const b = await body(c, ["provider"]);
5270
+ return { flow: auth.start(b.provider) };
5271
+ }, c));
5272
+ api2.post("/providers/key", (c) => safe(async () => {
5273
+ const b = await body(c, ["provider", "key"]);
5274
+ return { flow: auth.start(b.provider, keyInput(b.key)) };
5275
+ }, c));
5276
+ api2.post("/providers/answer", (c) => safe(async () => {
5277
+ const b = await body(c, ["id", "promptId", "value"]);
5278
+ return { flow: auth.answer(b.id, b.promptId, b.value) };
5279
+ }, c));
5280
+ api2.post("/providers/cancel", (c) => safe(async () => {
5281
+ const b = await body(c, ["id"]);
5282
+ return { flow: auth.cancel(b.id) };
5283
+ }, c));
5284
+ }
5285
+
4736
5286
  // server/index.ts
4737
- var HERE = path9.dirname(fileURLToPath(import.meta.url));
4738
- var APP_ROOT = path9.resolve(HERE, "..");
5287
+ var HERE = path10.dirname(fileURLToPath(import.meta.url));
5288
+ var APP_ROOT = path10.resolve(HERE, "..");
4739
5289
  var arg = (name, fallback) => {
4740
5290
  const i = process.argv.indexOf(`--${name}`);
4741
5291
  return i >= 0 ? process.argv[i + 1] : process.env[`LOOPS_UI_${name.toUpperCase().replace(/-/g, "_")}`] ?? fallback;
@@ -4751,13 +5301,13 @@ var PUBLIC_ORIGINS = [...new Set((arg("public-origin", "") ?? "").split(",").map
4751
5301
  return url.origin;
4752
5302
  }))];
4753
5303
  var SEED = arg("cwd");
4754
- var CWD = SEED ? path9.resolve(SEED) : null;
5304
+ var CWD = SEED ? path10.resolve(SEED) : null;
4755
5305
  var DEV = process.argv.includes("--dev") || process.env.NODE_ENV === "development";
4756
5306
  var VITE = arg("vite", "http://localhost:5173");
4757
- var PI = arg("pi", path9.join(APP_ROOT, "bin", "pi-rpc.mjs"));
4758
- var DATA = path9.resolve(arg("data", process.env.LOOPS_UI_DATA ?? path9.join(process.env.HOME ?? process.cwd(), ".pi", "loops-ui")));
5307
+ var PI = arg("pi", path10.join(APP_ROOT, "bin", "pi-rpc.mjs"));
5308
+ var DATA = path10.resolve(arg("data", process.env.LOOPS_UI_DATA ?? path10.join(process.env.HOME ?? process.cwd(), ".pi", "loops-ui")));
4759
5309
  var MODEL = arg("model", void 0) || null;
4760
- var EXTENSIONS = (arg("extensions", path9.resolve(APP_ROOT, "../pi-loops/extensions/loop.ts")) ?? "").split(",").filter(Boolean);
5310
+ var EXTENSIONS = (arg("extensions", path10.resolve(APP_ROOT, "../pi-loops/extensions/loop.ts")) ?? "").split(",").filter(Boolean);
4761
5311
  function prerequisites() {
4762
5312
  const out = [];
4763
5313
  const onPath = PI.includes("/") || /^[A-Za-z]:\\/.test(PI) ? fs9.existsSync(PI) : (() => {
@@ -4778,7 +5328,7 @@ var PROBLEMS = prerequisites();
4778
5328
  fs9.mkdirSync(DATA, { recursive: true });
4779
5329
  var TOKEN = (() => {
4780
5330
  const explicit = arg("token", void 0);
4781
- const file = path9.join(DATA, "token");
5331
+ const file = path10.join(DATA, "token");
4782
5332
  const stored = (() => {
4783
5333
  try {
4784
5334
  return fs9.readFileSync(file, "utf8").trim() || null;
@@ -4834,7 +5384,7 @@ var manager = new ChatManager({
4834
5384
  });
4835
5385
  var wsOf = (chat) => chat ? requireChatWorkspace(chat) : requireWorkspace();
4836
5386
  var wsOfRun = (run3) => {
4837
- const byIndex = workspaces.list().find((w) => fs9.existsSync(path9.join(w.path, STATE_DIR, `${run3}.json`)));
5387
+ const byIndex = workspaces.list().find((w) => fs9.existsSync(path10.join(w.path, STATE_DIR, `${run3}.json`)));
4838
5388
  if (byIndex) return byIndex;
4839
5389
  const chat = manager.findRun(run3);
4840
5390
  if (chat) return requireChatWorkspace(chat.id);
@@ -4868,7 +5418,7 @@ var watchOf = (wsId) => {
4868
5418
  if (!w) {
4869
5419
  const read = () => {
4870
5420
  try {
4871
- return JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
5421
+ return JSON.parse(fs9.readFileSync(path10.join(ws.path, ".pi", "loops.json"), "utf8"));
4872
5422
  } catch {
4873
5423
  return {};
4874
5424
  }
@@ -4876,14 +5426,14 @@ var watchOf = (wsId) => {
4876
5426
  w = { cwd: ws.path, accepted: read() };
4877
5427
  watches.set(ws.id, w);
4878
5428
  try {
4879
- fs9.watch(path9.join(ws.path, ".pi"), (_e, name) => {
5429
+ fs9.watch(path10.join(ws.path, ".pi"), (_e, name) => {
4880
5430
  if (name && name !== "loops.json") return;
4881
5431
  clearTimeout(watchTimers.get(ws.id));
4882
5432
  watchTimers.set(ws.id, setTimeout(() => {
4883
5433
  try {
4884
5434
  checkConfig(ws.id);
4885
5435
  } catch (e) {
4886
- configErrors.set(ws.id, { ws: ws.id, file: path9.join(ws.path, ".pi/loops.json"), error: `Could not validate settings: ${e.message}. Fix the file or save settings again.` });
5436
+ configErrors.set(ws.id, { ws: ws.id, file: path10.join(ws.path, ".pi/loops.json"), error: `Could not validate settings: ${e.message}. Fix the file or save settings again.` });
4887
5437
  notifyProposals();
4888
5438
  }
4889
5439
  }, 120));
@@ -4911,7 +5461,7 @@ function notifyProposals() {
4911
5461
  }
4912
5462
  function checkConfig(wsId) {
4913
5463
  const w = watchOf(wsId);
4914
- const CONFIG = path9.join(w.cwd, ".pi", "loops.json");
5464
+ const CONFIG = path10.join(w.cwd, ".pi", "loops.json");
4915
5465
  const raw2 = (() => {
4916
5466
  try {
4917
5467
  return fs9.readFileSync(CONFIG, "utf8");
@@ -4954,8 +5504,8 @@ function checkConfig(wsId) {
4954
5504
  notifyProposals();
4955
5505
  }
4956
5506
  var inside = (root, p) => {
4957
- const abs = path9.resolve(root, p);
4958
- return abs === root || abs.startsWith(root + path9.sep) ? abs : null;
5507
+ const abs = path10.resolve(root, p);
5508
+ return abs === root || abs.startsWith(root + path10.sep) ? abs : null;
4959
5509
  };
4960
5510
  var stateOf = (run3) => manager.findRun(run3)?.stateFor(run3) ?? readState(wsOfRun(run3).path, run3);
4961
5511
  var app = new Hono3();
@@ -5000,10 +5550,21 @@ app.use("/api/v1/*", async (c, next) => {
5000
5550
  var api = new Hono3();
5001
5551
  api.onError((error, c) => {
5002
5552
  if (error instanceof HTTPException) return c.json({ error: error.message }, error.status);
5553
+ if (error instanceof SessionActionError) return c.json({ error: error.message, ...error.uncertain ? { uncertain: true } : {} }, error.status);
5003
5554
  console.error(error);
5004
5555
  return c.json({ error: error.message }, 500);
5005
5556
  });
5006
5557
  api.get("/health", (c) => c.json({ ok: PROBLEMS.length === 0, problems: PROBLEMS, cwd: CWD, data: DATA, pi: PI, extensions: EXTENSIONS, workspaces: workspaces.list() }));
5558
+ var providerAuth = await createProviderAuth({
5559
+ onCredentialsChanged: async () => {
5560
+ await Promise.allSettled(manager.openChats().filter((chat) => !chat.sessionActions.busy).map((chat) => chat.refreshModels()));
5561
+ }
5562
+ });
5563
+ mountProviderAuthRoutes(api, providerAuth);
5564
+ api.route("/", createSessionActions({ requireChat: async (id) => {
5565
+ requireChatWorkspace(id);
5566
+ return manager.open(id);
5567
+ } }));
5007
5568
  api.get("/credentials", (c) => c.json({ dir: agentDir2(), providers: credentialStatus() }));
5008
5569
  api.post("/credentials", async (c) => {
5009
5570
  const body = await c.req.json().catch(() => ({}));
@@ -5073,7 +5634,7 @@ api.post("/chat", async (c) => {
5073
5634
  if (typeof body.ws !== "string" || !body.ws) return c.json({ error: "workspace required for a new chat" }, 400);
5074
5635
  ws = requireWorkspace(body.ws);
5075
5636
  if (!ws.open) return c.json({ error: "open the workspace before creating a chat" }, 409);
5076
- id = `chat-${randomUUID()}`;
5637
+ id = `chat-${randomUUID2()}`;
5077
5638
  workspaces.bindChat(id, ws.id);
5078
5639
  }
5079
5640
  watchOf(ws.id);
@@ -5201,7 +5762,7 @@ api.post("/steer", async (c) => {
5201
5762
  // Pi executes extension commands before its streaming queue. Do not send this as a queued follow-up:
5202
5763
  // a background round must remain independently stoppable while the parent is in a long-running tool.
5203
5764
  case "stop":
5204
- return c.json({ ok: true, requested: true, sent: await slash(`/loop stop ${run3}`) });
5765
+ return c.json({ ok: true, requested: true, sent: await chat.stopLoop(run3) });
5205
5766
  case "clean":
5206
5767
  return c.json({ ok: true, sent: await slash(`/loop clean ${run3}`) });
5207
5768
  case "rule":
@@ -5255,13 +5816,13 @@ api.get("/shot", (c) => {
5255
5816
  }
5256
5817
  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;
5257
5818
  if (!dir) return c.notFound();
5258
- const abs = inside(repo, path9.join(dir, file));
5819
+ const abs = inside(repo, path10.join(dir, file));
5259
5820
  if (!abs || !fs9.existsSync(abs)) return c.notFound();
5260
5821
  return new Response(fs9.readFileSync(abs), { headers: { "content-type": abs.endsWith(".jpg") || abs.endsWith(".jpeg") ? "image/jpeg" : "image/png", "cache-control": "no-cache" } });
5261
5822
  });
5262
5823
  api.get("/sheet", (c) => {
5263
5824
  const file = c.req.query("f") ?? "";
5264
- const ws = c.req.query("run") ? wsOfRun(c.req.query("run")) : workspaces.list().find((w) => fs9.existsSync(path9.join(w.path, file)));
5825
+ const ws = c.req.query("run") ? wsOfRun(c.req.query("run")) : workspaces.list().find((w) => fs9.existsSync(path10.join(w.path, file)));
5265
5826
  if (!ws) return c.notFound();
5266
5827
  const abs = inside(ws.path, file);
5267
5828
  if (!abs || !abs.includes(STATE_DIR) || !fs9.existsSync(abs)) return c.notFound();
@@ -5330,7 +5891,7 @@ api.post("/config", async (c) => {
5330
5891
  configErrors.delete(ws.id);
5331
5892
  const w = watchOf(ws.id);
5332
5893
  try {
5333
- w.accepted = JSON.parse(fs9.readFileSync(path9.join(ws.path, ".pi", "loops.json"), "utf8"));
5894
+ w.accepted = JSON.parse(fs9.readFileSync(path10.join(ws.path, ".pi", "loops.json"), "utf8"));
5334
5895
  } catch {
5335
5896
  }
5336
5897
  notifyProposals();
@@ -5381,7 +5942,7 @@ api.get("/meta", (c) => c.json({
5381
5942
  app.route("/api", api);
5382
5943
  app.route("/api/v1", api);
5383
5944
  app.all("/api/*", (c) => c.json({ error: `unknown API route: ${c.req.path}` }, 404));
5384
- var DIST = path9.join(APP_ROOT, "dist");
5945
+ var DIST = path10.join(APP_ROOT, "dist");
5385
5946
  app.get("*", async (c) => {
5386
5947
  const url = c.req.path;
5387
5948
  if (DEV) {
@@ -5391,11 +5952,11 @@ app.get("*", async (c) => {
5391
5952
  }
5392
5953
  const abs = inside(DIST, url === "/" ? "index.html" : url.replace(/^\//, ""));
5393
5954
  if (abs && fs9.existsSync(abs) && fs9.statSync(abs).isFile()) {
5394
- const ext = path9.extname(abs);
5955
+ const ext = path10.extname(abs);
5395
5956
  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";
5396
5957
  return new Response(fs9.readFileSync(abs), { headers: { "content-type": type } });
5397
5958
  }
5398
- const index = path9.join(DIST, "index.html");
5959
+ const index = path10.join(DIST, "index.html");
5399
5960
  if (!fs9.existsSync(index)) return c.text("run `npm run build` first", 500);
5400
5961
  return new Response(fs9.readFileSync(index), { headers: { "content-type": "text/html" } });
5401
5962
  });
@@ -5438,6 +5999,7 @@ server.listen(PORT, HOST, () => {
5438
5999
  });
5439
6000
  var shutdown = async () => {
5440
6001
  stopAllLive();
6002
+ await providerAuth.close();
5441
6003
  await manager.closeAll();
5442
6004
  server.close(() => process.exit(0));
5443
6005
  setTimeout(() => process.exit(0), 1500);