@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.
- package/API.md +35 -5
- package/README.md +67 -21
- package/bin/loops.mjs +13 -4
- package/bin/pi-rpc-startup.mjs +38 -0
- package/bin/pi-rpc.mjs +27 -0
- package/client.ts +16 -1
- package/deploy/loops.service.example +24 -0
- package/dist/assets/{abnfDiagram-VCTEODGH-CqEf5Tyi.js → abnfDiagram-VCTEODGH-BjWORKhB.js} +1 -1
- package/dist/assets/{arc-CrYrleHr.js → arc-iaXuj2E3.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-D5K4RlB6.js → architectureDiagram-5GKGNRK7-BGYlRe5d.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-Co-w8wX9.js → blockDiagram-I7D4REHJ-DbRX-Eko.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-DlcBV2hX.js → c4Diagram-7LVT6UL2-DFNI3QWM.js} +1 -1
- package/dist/assets/channel-mVgxi6SM.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dswi4LXs.js → chunk-2Q5K7J3B-DqhjvffG.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-CtLKoCf3.js → chunk-5VM5RSS4-pskYIVgB.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-Bbck4p8J.js → chunk-F27PBJKO-CQcyYiby.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-CvTEJd26.js → chunk-IMKFNOWR-B0MpoITM.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-AIHpJEyX.js → chunk-JWPE2WC7-BsNxH0py.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-C_B3I7hU.js → chunk-POPQ4Y6H-VnAuAhI3.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-au1NQO2y.js → chunk-SVP7TREG-fiQQ7YON.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-DOQXr0QZ.js → chunk-TICWLB2K-agM_rWnc.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-U36QglrB.js → chunk-XXDRQBXY-BrnsKK60.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BNg4EoCj.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BNg4EoCj.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-D7GIzP1z.js → cose-bilkent-JH36ORCC-skVRi8C0.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CjokfIiy.js → cynefin-OW5HDTMX-sugAAQ5D.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-1aEkMuPh.js → cynefinDiagram-5FMLGOSQ-BTjQ5HD4.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-BiIp4Lpg.js → dagre-GXQ25YYZ-wS0CVEvh.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-BZSI7zSx.js → diagram-S7CK7UJ4-0P_U489Q.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BRdBU2w8.js → diagram-UQ7AKVKN-D85SHcZz.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-DQ-h_ycl.js → diagram-VSXAHHWV-CozaqKLI.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-DmLQWSdL.js → diagram-VX7I27RA-kzpgzRzP.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-m8FGF4_n.js → diagram-Z3DM3KII-DeIAvld9.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-DtQ-Rw5F.js → ebnfDiagram-PWID7BFC-Cvo7CZD4.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-DIf-60L3.js → erDiagram-RLTQ6QDP-BILowZs8.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-COvzkFo5.js → flowDiagram-HODETNUW-D58_1bOZ.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-Dhz_v-Sb.js → ganttDiagram-EL5Y4UJY-Wc2egLDH.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-BwkibubM.js → gitGraphDiagram-WWUBYQGX-prvx2Pvu.js} +1 -1
- package/dist/assets/{highlighted-body-JLMTFACH-CbkekRih.js → highlighted-body-JLMTFACH-D3B11uXh.js} +1 -1
- package/dist/assets/index-C68b-bL6.css +1 -0
- package/dist/assets/index-CoHgGrA6.js +900 -0
- package/dist/assets/{infoDiagram-27XIBGKW-BltzH8hk.js → infoDiagram-27XIBGKW-S1_RXMDl.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-CKXLbULh.js → ishikawaDiagram-5VMMS53U-DPVVwIcA.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-Co-OVAQM.js → journeyDiagram-3NMN7TZE-Dmlf7Ej8.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-QEXNvAHJ.js → kanban-definition-UXKFOSKX-D7SJAgUZ.js} +1 -1
- package/dist/assets/{layout-DlW7eXy-.js → layout-DWrir7bj.js} +1 -1
- package/dist/assets/{linear-DmNmIGNl.js → linear-B7B0s6Fb.js} +1 -1
- package/dist/assets/{mindmap-definition-YA3MSWOX-CfrGbvrI.js → mindmap-definition-YA3MSWOX-CvytaBvR.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-BRPtQX-b.js → pegDiagram-XKGWAZYB-unjus9we.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-cyRAi108.js → pieDiagram-E7YTZNPT-Bc0mRNN1.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-BBwMoijR.js → quadrantDiagram-AXDQQJYC-DCMBYddi.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-DicocbzE.js → railroadDiagram-O6MQD6OU-Bbb5CPfd.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-DDiXCN4F.js → requirementDiagram-BXWQKSXE-C8qmEESJ.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-DnDGP5-W.js → sankeyDiagram-P5KCCOFB-ChzXGPH8.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-C1NFZjf0.js → sequenceDiagram-WJ2MYXX4-BeKUAzAe.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CEF6O58z.js → sizeCapture-INFHLROL-YJiYQ0Mn.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-Ibszr2iY.js → stateDiagram-D77RDMKH-D6NvtAkq.js} +1 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-C9eBMjpA.js +1 -0
- package/dist/assets/{swimlanes-42K2YHIH-DbvtObGJ.js → swimlanes-42K2YHIH-Cjb-j5cm.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-tvNn9kix.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-C6qVzSzu.js → timeline-definition-24CTP7MA-CaBJ4VuK.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-B3ZZFoCj.js → vennDiagram-4TSXK5OY-D8MLav5x.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-C9pFlzx9.js → wardleyDiagram-VM6X3IG4-CSy7rn-Q.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-mUkoBUkH.js → xychartDiagram-S5SC5T6Z-BtRqCsaE.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/server.mjs +646 -311
- package/package.json +5 -3
- package/src/shared/types.ts +1 -1
- package/dist/assets/channel-DoVi6ksL.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-Cyi65D9L.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-Cyi65D9L.js +0 -1
- package/dist/assets/index-DhZYU086.js +0 -900
- package/dist/assets/index-NzHlk7dF.css +0 -1
- package/dist/assets/stateDiagram-v2-MP3YSRHH-CCH6TZvE.js +0 -1
- 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
|
|
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
|
|
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 = (
|
|
83
|
-
const paths =
|
|
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:
|
|
89
|
-
const paths = splitPath(
|
|
116
|
+
const { groups, path: path10 } = extractGroupsFromPath(routePath);
|
|
117
|
+
const paths = splitPath(path10);
|
|
90
118
|
return replaceGroupMarks(paths, groups);
|
|
91
119
|
};
|
|
92
|
-
var extractGroupsFromPath = (
|
|
120
|
+
var extractGroupsFromPath = (path10) => {
|
|
93
121
|
const groups = [];
|
|
94
|
-
|
|
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:
|
|
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
|
|
165
|
-
return tryDecodeURI(
|
|
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 = (
|
|
179
|
-
if (
|
|
180
|
-
const segments =
|
|
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,
|
|
319
|
+
constructor(request, path10 = "/", matchResult = [[]]) {
|
|
292
320
|
this.raw = request;
|
|
293
|
-
this.path =
|
|
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,
|
|
1024
|
-
for (const p of [
|
|
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(
|
|
1079
|
-
const subApp = this.basePath(
|
|
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(
|
|
1132
|
+
basePath(path10) {
|
|
1105
1133
|
const subApp = this.#clone();
|
|
1106
|
-
subApp._basePath = mergePath(this._basePath,
|
|
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(
|
|
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,
|
|
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(
|
|
1246
|
+
this.#addRoute("ALL", mergePath(path10, "*"), handler);
|
|
1219
1247
|
return this;
|
|
1220
1248
|
}
|
|
1221
|
-
#addRoute(method,
|
|
1222
|
-
|
|
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:
|
|
1253
|
+
path: path10,
|
|
1226
1254
|
method,
|
|
1227
1255
|
handler
|
|
1228
1256
|
};
|
|
1229
|
-
this.router.add(method,
|
|
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
|
|
1239
|
-
const matchResult = this.router.match(method,
|
|
1266
|
+
const path10 = this.getPath(request, { env });
|
|
1267
|
+
const matchResult = this.router.match(method, path10);
|
|
1240
1268
|
const c = new Context(request, {
|
|
1241
|
-
path:
|
|
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,
|
|
1357
|
+
function match(method, path10) {
|
|
1330
1358
|
const matchers = this.buildAllMatchers();
|
|
1331
|
-
const match2 = ((method2,
|
|
1359
|
+
const match2 = ((method2, path11) => {
|
|
1332
1360
|
const matcher = matchers[method2] || matchers["ALL"];
|
|
1333
|
-
const staticMatch = matcher[2][
|
|
1361
|
+
const staticMatch = matcher[2][path11];
|
|
1334
1362
|
if (staticMatch) return staticMatch;
|
|
1335
|
-
const match3 =
|
|
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,
|
|
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(
|
|
1459
|
+
insert(path10, isStatic) {
|
|
1432
1460
|
if (isStatic) {
|
|
1433
|
-
this.#root.insert(
|
|
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 =
|
|
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[
|
|
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(
|
|
1493
|
-
return wildcardRegExpCache[
|
|
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,
|
|
1496
|
-
for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) if (buildWildcardRegExp(k).test(
|
|
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,
|
|
1536
|
+
#insertPath(method, path10) {
|
|
1509
1537
|
try {
|
|
1510
|
-
this.#tries[method].insert(
|
|
1538
|
+
this.#tries[method].insert(path10, !/\*|\/:/.test(path10));
|
|
1511
1539
|
} catch (e) {
|
|
1512
|
-
throw e === PATH_ERROR ? new UnsupportedPathError(
|
|
1540
|
+
throw e === PATH_ERROR ? new UnsupportedPathError(path10) : e;
|
|
1513
1541
|
}
|
|
1514
1542
|
}
|
|
1515
|
-
add(method,
|
|
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 (
|
|
1557
|
+
if (path10 === "/*") path10 = "*";
|
|
1530
1558
|
const methods = method === "ALL" ? Object.keys(middleware) : [method];
|
|
1531
|
-
if (/\*$/.test(
|
|
1532
|
-
const re = buildWildcardRegExp(
|
|
1533
|
-
for (const m of methods) if (!middleware[m][
|
|
1534
|
-
this.#insertPath(m,
|
|
1535
|
-
middleware[m][
|
|
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,
|
|
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(
|
|
1541
|
-
for (const
|
|
1542
|
-
if (!routes[m][
|
|
1543
|
-
this.#insertPath(m,
|
|
1544
|
-
routes[m][
|
|
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][
|
|
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
|
|
1565
|
-
const handlers = r[
|
|
1566
|
-
const pathData = trie.paths[
|
|
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[
|
|
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,
|
|
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
|
-
|
|
1624
|
+
path10,
|
|
1597
1625
|
handler
|
|
1598
1626
|
]);
|
|
1599
1627
|
}
|
|
1600
|
-
match(method,
|
|
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,
|
|
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,
|
|
1668
|
+
insert(method, path10, handler) {
|
|
1641
1669
|
let curNode = this;
|
|
1642
|
-
const parts = splitRoutingPath(
|
|
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,
|
|
1706
|
+
search(method, path10) {
|
|
1679
1707
|
const handlerSets = [];
|
|
1680
1708
|
this.#params = emptyParams;
|
|
1681
1709
|
let curNodes = [this];
|
|
1682
|
-
const parts = splitPath(
|
|
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 =
|
|
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 =
|
|
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,
|
|
1766
|
-
for (const result of checkOptionalParameter(
|
|
1793
|
+
add(method, path10, handler) {
|
|
1794
|
+
for (const result of checkOptionalParameter(path10) || [path10]) this.#node.insert(method, result, handler);
|
|
1767
1795
|
}
|
|
1768
|
-
match(method,
|
|
1769
|
-
return this.#node.search(method,
|
|
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,
|
|
2378
|
+
let [status, body, header2] = res[cacheKey];
|
|
2351
2379
|
let hasContentLength = false;
|
|
2352
|
-
if (!
|
|
2353
|
-
|
|
2354
|
-
} else if (
|
|
2355
|
-
hasContentLength =
|
|
2356
|
-
|
|
2357
|
-
} else if (Array.isArray(
|
|
2358
|
-
const headerObj = new Headers(
|
|
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
|
-
|
|
2388
|
+
header2 = buildOutgoingHttpHeaders(headerObj);
|
|
2361
2389
|
} else {
|
|
2362
|
-
for (const key in
|
|
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
|
-
|
|
2399
|
+
header2["Content-Length"] = Buffer.byteLength(body);
|
|
2372
2400
|
} else if (body instanceof Uint8Array) {
|
|
2373
|
-
|
|
2401
|
+
header2["Content-Length"] = body.byteLength;
|
|
2374
2402
|
} else if (body instanceof Blob) {
|
|
2375
|
-
|
|
2403
|
+
header2["Content-Length"] = body.size;
|
|
2376
2404
|
}
|
|
2377
2405
|
}
|
|
2378
|
-
outgoing.writeHead(status,
|
|
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
|
|
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("
|
|
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)
|
|
2712
|
-
const record = { type: "extension_ui_response", id
|
|
2713
|
-
|
|
2714
|
-
this.proc.stdin.write(JSON.stringify(record) + "\n");
|
|
2715
|
-
}
|
|
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("
|
|
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 =
|
|
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
|
-
|
|
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))
|
|
3139
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
3408
|
-
const
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
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
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
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
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
4211
|
-
this.chatMap = new Map(Object.entries(
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
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
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4236
|
-
|
|
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
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
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
|
-
|
|
4461
|
+
names = fs7.readdirSync(dir).filter((n) => n.endsWith(".jsonl"));
|
|
4284
4462
|
} catch {
|
|
4285
4463
|
continue;
|
|
4286
4464
|
}
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
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
|
-
|
|
4302
|
-
if (
|
|
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
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
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
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
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
|
-
|
|
4493
|
+
hasChat(chat) {
|
|
4494
|
+
return this.chatMap.has(chat) && this.byId.has(this.chatMap.get(chat));
|
|
4495
|
+
}
|
|
4321
4496
|
ofChat(chat) {
|
|
4322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4331
|
-
|
|
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 =
|
|
4346
|
-
var APP_ROOT =
|
|
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
|
|
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 =
|
|
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",
|
|
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) ?
|
|
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 (!
|
|
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
|
-
|
|
4653
|
+
fs9.mkdirSync(DATA, { recursive: true });
|
|
4379
4654
|
var TOKEN = (() => {
|
|
4380
4655
|
const explicit = arg("token", void 0);
|
|
4381
|
-
const file =
|
|
4656
|
+
const file = path9.join(DATA, "token");
|
|
4382
4657
|
const stored = (() => {
|
|
4383
4658
|
try {
|
|
4384
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 ?
|
|
4710
|
+
var wsOf = (chat) => chat ? requireChatWorkspace(chat) : requireWorkspace();
|
|
4420
4711
|
var wsOfRun = (run3) => {
|
|
4421
|
-
const byIndex = workspaces.list().find((w) =>
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
4775
|
+
const CONFIG = path9.join(w.cwd, ".pi", "loops.json");
|
|
4480
4776
|
const raw2 = (() => {
|
|
4481
4777
|
try {
|
|
4482
|
-
return
|
|
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 =
|
|
4504
|
-
return abs === root || abs.startsWith(root +
|
|
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([
|
|
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
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
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
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
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
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
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 =
|
|
4628
|
-
if (!dir || dir
|
|
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
|
-
|
|
4668
|
-
|
|
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 || !
|
|
4714
|
-
|
|
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 || !
|
|
4733
|
-
return new Response(
|
|
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,
|
|
4738
|
-
if (!abs || !
|
|
4739
|
-
return new Response(
|
|
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) =>
|
|
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) || !
|
|
4746
|
-
return new Response(
|
|
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
|
-
|
|
4760
|
-
|
|
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 =
|
|
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(
|
|
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) =>
|
|
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
|
-
|
|
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 &&
|
|
4867
|
-
const ext =
|
|
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(
|
|
5204
|
+
return new Response(fs9.readFileSync(abs), { headers: { "content-type": type } });
|
|
4870
5205
|
}
|
|
4871
|
-
const index =
|
|
4872
|
-
if (!
|
|
4873
|
-
return new Response(
|
|
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}`);
|