offsite 0.0.7 → 0.0.10
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/apps/server/dist/main.mjs +563 -431
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261017041447_init/migration.sql +32 -0
- package/apps/server/drizzle/20261017041447_init/snapshot.json +265 -0
- package/apps/web/dist/assets/{angular-html-CxKqLP98.js → angular-html-BEUtPJ31.js} +1 -1
- package/apps/web/dist/assets/{angular-ts-ChaIimso.js → angular-ts-Bnr4rzFr.js} +1 -1
- package/apps/web/dist/assets/{apl-C79wKZ96.js → apl-B-JNBTdP.js} +1 -1
- package/apps/web/dist/assets/{astro-Bjd0-rUa.js → astro-DLjK95IW.js} +1 -1
- package/apps/web/dist/assets/{blade-CwnKAXvb.js → blade-PLMCMBeb.js} +1 -1
- package/apps/web/dist/assets/{c-Z516m-0_.js → c-D5z8xNe6.js} +1 -1
- package/apps/web/dist/assets/{chapel-CjZ9_INP.js → chapel-B9y2HxI_.js} +1 -1
- package/apps/web/dist/assets/{cobol-loY0YFmk.js → cobol-BSwRu2X7.js} +1 -1
- package/apps/web/dist/assets/{coffee-UH3lFVdK.js → coffee-BAWFR6iF.js} +1 -1
- package/apps/web/dist/assets/{cpp-DglnXsh6.js → cpp-CO-vZI4m.js} +1 -1
- package/apps/web/dist/assets/{crystal-8bvlXru4.js → crystal-BSfyPt6R.js} +1 -1
- package/apps/web/dist/assets/{css-BR1q7k6_.js → css-BOzKs1dt.js} +1 -1
- package/apps/web/dist/assets/{edge-BxokJ4MN.js → edge-OBqkt95y.js} +1 -1
- package/apps/web/dist/assets/{elixir-BUaqVZQr.js → elixir-B14D2jMM.js} +1 -1
- package/apps/web/dist/assets/{elm-D_9smeEe.js → elm-DgfIayFq.js} +1 -1
- package/apps/web/dist/assets/{erb-DOZIqXQH.js → erb-w-PUv7ic.js} +1 -1
- package/apps/web/dist/assets/{git-rebase-DN-oOLvo.js → git-rebase-DnzmyV_3.js} +1 -1
- package/apps/web/dist/assets/{glimmer-js-dSW-hX4N.js → glimmer-js-CLephpyX.js} +1 -1
- package/apps/web/dist/assets/{glimmer-ts-BgJbFWVH.js → glimmer-ts-CvphZOog.js} +1 -1
- package/apps/web/dist/assets/{glsl-FkhRNxnG.js → glsl-Ddj2GHKO.js} +1 -1
- package/apps/web/dist/assets/{graphql-BKHVf0BT.js → graphql-CeGXMmoK.js} +1 -1
- package/apps/web/dist/assets/{hack-obS7Vybj.js → hack-CtBKZ5VW.js} +1 -1
- package/apps/web/dist/assets/{haml-8M07SKZy.js → haml-DVvVd-Mp.js} +1 -1
- package/apps/web/dist/assets/{handlebars-Cyb9PeHi.js → handlebars-Blx6Nluh.js} +1 -1
- package/apps/web/dist/assets/{html-Dwl7QR96.js → html-O-gZ5M1-.js} +1 -1
- package/apps/web/dist/assets/{html-derivative-Dx3lESlT.js → html-derivative-BsDe9c3P.js} +1 -1
- package/apps/web/dist/assets/{http-CKSR21d-.js → http-CAUZeWmG.js} +1 -1
- package/apps/web/dist/assets/{hurl-DGaDIWma.js → hurl-CYnRsJrb.js} +1 -1
- package/apps/web/dist/assets/index-CGSSO_em.js +1152 -0
- package/apps/web/dist/assets/{java-BmaAOMbe.js → java-DjjpMT1c.js} +1 -1
- package/apps/web/dist/assets/{javascript-Dh-l17xw.js → javascript-BtiswSWS.js} +1 -1
- package/apps/web/dist/assets/{jinja-C14B9X7r.js → jinja-ZQGJY58d.js} +1 -1
- package/apps/web/dist/assets/{jison-DOLQZnVk.js → jison-VPkdeMFr.js} +1 -1
- package/apps/web/dist/assets/{json-CKQXRUCT.js → json-D5BAJIHo.js} +1 -1
- package/apps/web/dist/assets/{jsx-D3LXQmFh.js → jsx-DMEKVcfT.js} +1 -1
- package/apps/web/dist/assets/{julia-BONGpLSW.js → julia-BmOLQFch.js} +1 -1
- package/apps/web/dist/assets/{just-1ImCRd19.js → just-YZGutsxo.js} +1 -1
- package/apps/web/dist/assets/{latex-C6nf8qng.js → latex-CvrF79xl.js} +1 -1
- package/apps/web/dist/assets/{liquid-OyHcMLT8.js → liquid-B--WbaSh.js} +1 -1
- package/apps/web/dist/assets/{lua-CFyXSn73.js → lua-BNEqLlv-.js} +1 -1
- package/apps/web/dist/assets/{marko-w4Xaqrn8.js → marko-jEzbzXvj.js} +1 -1
- package/apps/web/dist/assets/{mdc-CipE4q24.js → mdc-yfiYVuXh.js} +1 -1
- package/apps/web/dist/assets/{nginx-CidGWCuA.js → nginx-j7vnr3lB.js} +1 -1
- package/apps/web/dist/assets/{nim-DqvuZ6-V.js → nim-CV2OP2RC.js} +1 -1
- package/apps/web/dist/assets/{org-C8k2emY1.js → org-DTTS5Tv_.js} +1 -1
- package/apps/web/dist/assets/{perl-D06UK6ou.js → perl-DuF7yMdF.js} +1 -1
- package/apps/web/dist/assets/{php-POxwc-vY.js → php-8ufnwSAo.js} +1 -1
- package/apps/web/dist/assets/{pug-BzSRzYD8.js → pug-D-_t9dIn.js} +1 -1
- package/apps/web/dist/assets/{qml-DZF7i2J0.js → qml-DFxD5uAC.js} +1 -1
- package/apps/web/dist/assets/{r-Cem3rJmc.js → r-D8-Yi2KS.js} +1 -1
- package/apps/web/dist/assets/{razor-DG8dmRN8.js → razor-DhBq7-pb.js} +1 -1
- package/apps/web/dist/assets/{regexp-B4kXooBf.js → regexp-C20CUsFr.js} +1 -1
- package/apps/web/dist/assets/{rst-DKLtDtVi.js → rst-DsIaukoD.js} +1 -1
- package/apps/web/dist/assets/{ruby-CamS7fke.js → ruby-C__EgK1w.js} +1 -1
- package/apps/web/dist/assets/{sas-B0QV8BKG.js → sas-BdDDRZUF.js} +1 -1
- package/apps/web/dist/assets/{scss-PDWZBsRp.js → scss-nPFPh8nF.js} +1 -1
- package/apps/web/dist/assets/{shellscript-CdzFXVZ3.js → shellscript-Cfn0DgiQ.js} +1 -1
- package/apps/web/dist/assets/{shellsession-BTvaJuzj.js → shellsession-DFGAcvZ4.js} +1 -1
- package/apps/web/dist/assets/{soy-CKjkt_DB.js → soy-8DrmW46F.js} +1 -1
- package/apps/web/dist/assets/{sql-CvyoBOik.js → sql-BeU26hky.js} +1 -1
- package/apps/web/dist/assets/{stata-DKSZtLec.js → stata-OFfE-Kh9.js} +1 -1
- package/apps/web/dist/assets/{surrealql-DLiWChn8.js → surrealql-Bwt4eVgP.js} +1 -1
- package/apps/web/dist/assets/{svelte-BEKUDT0w.js → svelte-DF7N9uBp.js} +1 -1
- package/apps/web/dist/assets/{templ-Cwpj8Ert.js → templ-CEirsWl9.js} +1 -1
- package/apps/web/dist/assets/{tex-eE29FuOr.js → tex-BvBhDkV4.js} +1 -1
- package/apps/web/dist/assets/{ts-tags-CS_oR42m.js → ts-tags-Dx8bn0ts.js} +1 -1
- package/apps/web/dist/assets/{tsx-CXh89N3W.js → tsx-vXk5Q55O.js} +1 -1
- package/apps/web/dist/assets/{twig-DE2-3Vmd.js → twig-CIgLDHIW.js} +1 -1
- package/apps/web/dist/assets/{typescript-xOdvp9de.js → typescript-CCW6p4Fc.js} +1 -1
- package/apps/web/dist/assets/{typst-B_JeYRUR.js → typst-CQL-qDz3.js} +1 -1
- package/apps/web/dist/assets/{vue-dnoj_d3g.js → vue-gFAUYOUp.js} +1 -1
- package/apps/web/dist/assets/{vue-html-UHMeN4EH.js → vue-html-BHuiCeAo.js} +1 -1
- package/apps/web/dist/assets/{vue-vine-Cl2xkrIS.js → vue-vine-DxeD8nHa.js} +1 -1
- package/apps/web/dist/assets/{xml-BgkAAaUc.js → xml-B43ZEHE5.js} +1 -1
- package/apps/web/dist/assets/{xsl-C4H21Kbc.js → xsl-icvYLDXv.js} +1 -1
- package/apps/web/dist/assets/{yaml-Ce1Tbjry.js → yaml-Bj0dlmrV.js} +1 -1
- package/apps/web/dist/index.html +1 -1
- package/apps/web/dist/sw.js +1 -1
- package/package.json +2 -1
- package/apps/web/dist/assets/index-BJCHT3_X.js +0 -1148
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { consola } from "consola";
|
|
@@ -6,17 +6,21 @@ import { renderUnicodeCompact } from "uqr";
|
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
9
|
-
import { randomBytes, randomUUID
|
|
9
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
11
11
|
import readline from "node:readline";
|
|
12
12
|
import { Command } from "@commander-js/extra-typings";
|
|
13
13
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
14
|
import { OpenCode } from "@opencode/client";
|
|
15
15
|
import { ensure, headers } from "@opencode/client/service";
|
|
16
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
16
17
|
import { DatabaseSync } from "node:sqlite";
|
|
18
|
+
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
19
|
+
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
20
|
+
import webPush from "web-push";
|
|
21
|
+
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
17
22
|
import { lstat, open, readFile, readdir, readlink, stat } from "node:fs/promises";
|
|
18
23
|
import { promisify } from "node:util";
|
|
19
|
-
import webPush from "web-push";
|
|
20
24
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
21
25
|
//#region ../core/src/protocol.ts
|
|
22
26
|
const HOSTED_APP = "https://offsite.sayad.dev";
|
|
@@ -178,27 +182,6 @@ const defaults = {
|
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
};
|
|
181
|
-
const retired = [
|
|
182
|
-
"theme",
|
|
183
|
-
"diff",
|
|
184
|
-
"terminal.font",
|
|
185
|
-
"terminal.cursor",
|
|
186
|
-
"terminal.padding",
|
|
187
|
-
"terminal.scrollback",
|
|
188
|
-
"terminal.boldIsBright",
|
|
189
|
-
"terminal.behavior.modernizeShiftEnter",
|
|
190
|
-
"terminal.behavior.pasteBrowserImages",
|
|
191
|
-
"terminal.behavior.captureShortcuts",
|
|
192
|
-
"terminal.behavior.bellFlash",
|
|
193
|
-
"terminal.behavior.bellSound",
|
|
194
|
-
"terminal.behavior.clickToMoveCursor",
|
|
195
|
-
"terminal.behavior.optionAsMeta",
|
|
196
|
-
"terminal.behavior.confirmCloseBusyTab",
|
|
197
|
-
"composer.accountId",
|
|
198
|
-
"composer.model",
|
|
199
|
-
"composer.effort",
|
|
200
|
-
"composer.permissionMode"
|
|
201
|
-
];
|
|
202
185
|
const choices = {
|
|
203
186
|
"composer.provider": [
|
|
204
187
|
"claude",
|
|
@@ -299,7 +282,6 @@ function saveConfig(value) {
|
|
|
299
282
|
return value;
|
|
300
283
|
}
|
|
301
284
|
function merge(config, keys, user) {
|
|
302
|
-
if (retired.includes(keys.join("."))) return config;
|
|
303
285
|
if (isObject(keys.reduce((value, key) => isObject(value) ? value[key] : void 0, config)) && isObject(user)) {
|
|
304
286
|
let next = config;
|
|
305
287
|
for (const [key, value] of Object.entries(user)) next = merge(next, [...keys, key], value);
|
|
@@ -344,6 +326,8 @@ function isObject(value) {
|
|
|
344
326
|
//#endregion
|
|
345
327
|
//#region src/lib/codex/app-server.ts
|
|
346
328
|
const IDLE_LIMIT$3 = 18e5;
|
|
329
|
+
const START_TIMEOUT = 15e3;
|
|
330
|
+
const REQUEST_TIMEOUT = 3e4;
|
|
347
331
|
const codexBaseDir = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
348
332
|
const servers = /* @__PURE__ */ new Map();
|
|
349
333
|
function appServer(home) {
|
|
@@ -401,13 +385,16 @@ var AppServer = class {
|
|
|
401
385
|
experimentalApi: true,
|
|
402
386
|
requestAttestation: false
|
|
403
387
|
}
|
|
404
|
-
}).then(() => this.write({ method: "initialized" }))
|
|
388
|
+
}, START_TIMEOUT).then(() => this.write({ method: "initialized" }), (error) => {
|
|
389
|
+
this.child.kill();
|
|
390
|
+
throw error;
|
|
391
|
+
});
|
|
405
392
|
this.armIdleTimer();
|
|
406
393
|
}
|
|
407
394
|
async request(method, params) {
|
|
408
395
|
await this.ready;
|
|
409
396
|
this.armIdleTimer();
|
|
410
|
-
return await this.call(method, params);
|
|
397
|
+
return await this.call(method, params, REQUEST_TIMEOUT);
|
|
411
398
|
}
|
|
412
399
|
reply(id, result) {
|
|
413
400
|
this.write({
|
|
@@ -429,14 +416,26 @@ var AppServer = class {
|
|
|
429
416
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
430
417
|
this.child.stdin.end();
|
|
431
418
|
}
|
|
432
|
-
call(method, params) {
|
|
419
|
+
call(method, params, timeout) {
|
|
433
420
|
if (this.exited !== null) return Promise.reject(this.exited);
|
|
434
421
|
this.next += 1;
|
|
435
422
|
const id = this.next;
|
|
436
|
-
const result = new Promise((resolve, reject) =>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
423
|
+
const result = new Promise((resolve, reject) => {
|
|
424
|
+
const timer = setTimeout(() => {
|
|
425
|
+
this.waiting.delete(id);
|
|
426
|
+
reject(/* @__PURE__ */ new Error(`Codex did not answer ${method} within ${timeout / 1e3} seconds`));
|
|
427
|
+
}, timeout);
|
|
428
|
+
this.waiting.set(id, {
|
|
429
|
+
resolve: (value) => {
|
|
430
|
+
clearTimeout(timer);
|
|
431
|
+
resolve(value);
|
|
432
|
+
},
|
|
433
|
+
reject: (error) => {
|
|
434
|
+
clearTimeout(timer);
|
|
435
|
+
reject(error);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
440
439
|
this.write({
|
|
441
440
|
id,
|
|
442
441
|
method,
|
|
@@ -618,7 +617,7 @@ function isMirror(dir) {
|
|
|
618
617
|
}
|
|
619
618
|
//#endregion
|
|
620
619
|
//#region src/lib/codex/items.ts
|
|
621
|
-
const statuses = {
|
|
620
|
+
const statuses$1 = {
|
|
622
621
|
inProgress: "running",
|
|
623
622
|
completed: "completed",
|
|
624
623
|
failed: "failed",
|
|
@@ -684,7 +683,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
684
683
|
command: item.command,
|
|
685
684
|
description: "",
|
|
686
685
|
output: item.aggregatedOutput ?? "",
|
|
687
|
-
status: statuses[item.status],
|
|
686
|
+
status: statuses$1[item.status],
|
|
688
687
|
background: null
|
|
689
688
|
}];
|
|
690
689
|
case "fileChange": return item.changes.map((change, index) => ({
|
|
@@ -700,7 +699,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
700
699
|
after: ""
|
|
701
700
|
}] : hunks(change.diff),
|
|
702
701
|
output: "",
|
|
703
|
-
status: statuses[item.status]
|
|
702
|
+
status: statuses$1[item.status]
|
|
704
703
|
}));
|
|
705
704
|
case "mcpToolCall": return [{
|
|
706
705
|
...base,
|
|
@@ -708,7 +707,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
708
707
|
name: `${item.server}.${item.tool}`,
|
|
709
708
|
input: record$1(item.arguments),
|
|
710
709
|
output: item.error?.message ?? (item.result === null ? "" : contentText$1(item.result.content)),
|
|
711
|
-
status: statuses[item.status]
|
|
710
|
+
status: statuses$1[item.status]
|
|
712
711
|
}];
|
|
713
712
|
case "dynamicToolCall": return [{
|
|
714
713
|
...base,
|
|
@@ -716,7 +715,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
716
715
|
name: item.tool,
|
|
717
716
|
input: record$1(item.arguments),
|
|
718
717
|
output: contentText$1(item.contentItems ?? []),
|
|
719
|
-
status: statuses[item.status]
|
|
718
|
+
status: statuses$1[item.status]
|
|
720
719
|
}];
|
|
721
720
|
case "collabAgentToolCall":
|
|
722
721
|
if (item.tool === "spawnAgent") return [{
|
|
@@ -725,14 +724,14 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
725
724
|
agent: item.model ?? "Agent",
|
|
726
725
|
description: item.prompt ?? "",
|
|
727
726
|
agentId: item.receiverThreadIds[0] ?? null,
|
|
728
|
-
status: statuses[item.status]
|
|
727
|
+
status: statuses$1[item.status]
|
|
729
728
|
}];
|
|
730
729
|
return [{
|
|
731
730
|
...base,
|
|
732
731
|
kind: "agent_message",
|
|
733
732
|
target: item.receiverThreadIds[0] ?? null,
|
|
734
733
|
summary: item.prompt ?? item.tool,
|
|
735
|
-
status: statuses[item.status]
|
|
734
|
+
status: statuses$1[item.status]
|
|
736
735
|
}];
|
|
737
736
|
case "webSearch": return [{
|
|
738
737
|
...base,
|
|
@@ -975,6 +974,7 @@ var CodexRun = class CodexRun {
|
|
|
975
974
|
prompted = false;
|
|
976
975
|
backgroundTasks = 0;
|
|
977
976
|
tasks = {};
|
|
977
|
+
activeAt = Date.now();
|
|
978
978
|
model;
|
|
979
979
|
close;
|
|
980
980
|
turnId = null;
|
|
@@ -1079,11 +1079,12 @@ var CodexRun = class CodexRun {
|
|
|
1079
1079
|
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => console.warn(`could not unload Codex thread: ${error}`)).finally(() => this.end());
|
|
1080
1080
|
}
|
|
1081
1081
|
exited() {
|
|
1082
|
-
this.put(errorItem(randomUUID(), "Codex stopped"));
|
|
1082
|
+
this.put(errorItem(randomUUID(), this.pending.size === 0 ? "Codex stopped" : "Codex stopped, its open requests expired"));
|
|
1083
1083
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1084
1084
|
this.end();
|
|
1085
1085
|
}
|
|
1086
1086
|
notify(method, params) {
|
|
1087
|
+
this.activeAt = Date.now();
|
|
1087
1088
|
switch (method) {
|
|
1088
1089
|
case "turn/started": {
|
|
1089
1090
|
const turn = params.turn;
|
|
@@ -1438,23 +1439,67 @@ async function readThread(threadId) {
|
|
|
1438
1439
|
})).thread;
|
|
1439
1440
|
}
|
|
1440
1441
|
//#endregion
|
|
1442
|
+
//#region src/storage/schema.ts
|
|
1443
|
+
const machine = snakeCase.table("machine", {
|
|
1444
|
+
id: text().primaryKey(),
|
|
1445
|
+
pushPublicKey: text().notNull(),
|
|
1446
|
+
pushPrivateKey: text().notNull()
|
|
1447
|
+
});
|
|
1448
|
+
const projects$2 = snakeCase.table("projects", {
|
|
1449
|
+
id: text().primaryKey(),
|
|
1450
|
+
path: text().notNull().unique(),
|
|
1451
|
+
name: text().notNull(),
|
|
1452
|
+
logo: text()
|
|
1453
|
+
});
|
|
1454
|
+
const sessions$1 = snakeCase.table("sessions", {
|
|
1455
|
+
sessionId: text().primaryKey(),
|
|
1456
|
+
interactedAt: integer(),
|
|
1457
|
+
status: text({ enum: [
|
|
1458
|
+
"active",
|
|
1459
|
+
"pinned",
|
|
1460
|
+
"minimized"
|
|
1461
|
+
] }).notNull()
|
|
1462
|
+
});
|
|
1463
|
+
const tokens = snakeCase.table("tokens", {
|
|
1464
|
+
hash: text().primaryKey(),
|
|
1465
|
+
name: text(),
|
|
1466
|
+
createdAt: integer().notNull(),
|
|
1467
|
+
lastUsedAt: integer()
|
|
1468
|
+
});
|
|
1469
|
+
const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
1470
|
+
endpoint: text().primaryKey(),
|
|
1471
|
+
p256dh: text().notNull(),
|
|
1472
|
+
auth: text().notNull(),
|
|
1473
|
+
server: text().notNull()
|
|
1474
|
+
});
|
|
1475
|
+
//#endregion
|
|
1441
1476
|
//#region src/storage/db.ts
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
db.exec(`
|
|
1452
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
1453
|
-
session_id TEXT PRIMARY KEY,
|
|
1454
|
-
interacted_at INTEGER,
|
|
1455
|
-
status TEXT NOT NULL CHECK (status IN ('active', 'pinned', 'minimized'))
|
|
1456
|
-
)
|
|
1477
|
+
const file = path.join(home, "offsite.db");
|
|
1478
|
+
closeSync(openSync(file, "a", 384));
|
|
1479
|
+
chmodSync(file, 384);
|
|
1480
|
+
const client = new DatabaseSync(file);
|
|
1481
|
+
client.exec(`
|
|
1482
|
+
PRAGMA journal_mode = WAL;
|
|
1483
|
+
PRAGMA busy_timeout = 5000;
|
|
1484
|
+
PRAGMA synchronous = NORMAL;
|
|
1485
|
+
PRAGMA foreign_keys = ON;
|
|
1457
1486
|
`);
|
|
1487
|
+
const db = drizzle({ client });
|
|
1488
|
+
migrate(db, { migrationsFolder: path.join(serverDir, "drizzle") });
|
|
1489
|
+
db.transaction((tx) => {
|
|
1490
|
+
if (tx.select().from(machine).get() !== void 0) return;
|
|
1491
|
+
const keys = webPush.generateVAPIDKeys();
|
|
1492
|
+
tx.insert(machine).values({
|
|
1493
|
+
id: randomUUID(),
|
|
1494
|
+
pushPublicKey: keys.publicKey,
|
|
1495
|
+
pushPrivateKey: keys.privateKey
|
|
1496
|
+
}).run();
|
|
1497
|
+
}, { behavior: "immediate" });
|
|
1498
|
+
function machineInfo() {
|
|
1499
|
+
const row = db.select().from(machine).get();
|
|
1500
|
+
if (row === void 0) throw new Error("the machine row is missing");
|
|
1501
|
+
return row;
|
|
1502
|
+
}
|
|
1458
1503
|
//#endregion
|
|
1459
1504
|
//#region src/services/projects.ts
|
|
1460
1505
|
const LOGO_TYPES = [
|
|
@@ -1468,32 +1513,42 @@ const LOGO_TYPES = [
|
|
|
1468
1513
|
];
|
|
1469
1514
|
const LOGO_LIMIT = 524288;
|
|
1470
1515
|
function projects$1() {
|
|
1471
|
-
return db.
|
|
1516
|
+
return db.select().from(projects$2).all();
|
|
1517
|
+
}
|
|
1518
|
+
function projectPath(id) {
|
|
1519
|
+
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
1520
|
+
if (project === void 0) throw new Error("That project was removed");
|
|
1521
|
+
return project.path;
|
|
1472
1522
|
}
|
|
1473
1523
|
async function addProject(input) {
|
|
1474
1524
|
const folder = path.resolve(expandHome(input.trim()));
|
|
1475
1525
|
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
1476
|
-
db.
|
|
1526
|
+
db.insert(projects$2).values({
|
|
1527
|
+
id: randomUUID(),
|
|
1528
|
+
path: folder,
|
|
1529
|
+
name: path.basename(folder) || folder,
|
|
1530
|
+
logo: null
|
|
1531
|
+
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
1477
1532
|
publish$1();
|
|
1478
1533
|
const project = projects$1().find((item) => item.path === folder);
|
|
1479
1534
|
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
1480
1535
|
return project;
|
|
1481
1536
|
}
|
|
1482
|
-
function updateProject(
|
|
1537
|
+
function updateProject(id, patch) {
|
|
1483
1538
|
if (patch.name !== void 0) {
|
|
1484
1539
|
const name = patch.name.trim();
|
|
1485
1540
|
if (name === "") throw new Error("Project name cannot be empty");
|
|
1486
|
-
db.
|
|
1541
|
+
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
1487
1542
|
}
|
|
1488
1543
|
if (patch.logo !== void 0) {
|
|
1489
1544
|
if (patch.logo !== null) checkLogo(patch.logo);
|
|
1490
|
-
db.
|
|
1545
|
+
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
1491
1546
|
}
|
|
1492
1547
|
publish$1();
|
|
1493
1548
|
return null;
|
|
1494
1549
|
}
|
|
1495
|
-
function removeProject(
|
|
1496
|
-
db.
|
|
1550
|
+
function removeProject(id) {
|
|
1551
|
+
db.delete(projects$2).where(eq(projects$2.id, id)).run();
|
|
1497
1552
|
publish$1();
|
|
1498
1553
|
}
|
|
1499
1554
|
async function directories(input) {
|
|
@@ -1515,14 +1570,6 @@ function publish$1() {
|
|
|
1515
1570
|
projects: projects$1()
|
|
1516
1571
|
});
|
|
1517
1572
|
}
|
|
1518
|
-
function toProject(row) {
|
|
1519
|
-
if (typeof row.path !== "string" || typeof row.name !== "string" || typeof row.logo !== "string" && row.logo !== null) throw new Error("The projects table has an unexpected row");
|
|
1520
|
-
return {
|
|
1521
|
-
path: row.path,
|
|
1522
|
-
name: row.name,
|
|
1523
|
-
logo: row.logo
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
1573
|
function checkLogo(logo) {
|
|
1527
1574
|
const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
|
|
1528
1575
|
if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
|
|
@@ -1536,54 +1583,43 @@ function expandHome(input) {
|
|
|
1536
1583
|
//#endregion
|
|
1537
1584
|
//#region src/services/sessions.ts
|
|
1538
1585
|
const day = 864e5;
|
|
1586
|
+
const LIST_TIMEOUT = 1e4;
|
|
1587
|
+
const REFRESH_DELAY = 1e3;
|
|
1588
|
+
const allProviders = [
|
|
1589
|
+
"claude",
|
|
1590
|
+
"codex",
|
|
1591
|
+
"opencode"
|
|
1592
|
+
];
|
|
1593
|
+
const listed = {
|
|
1594
|
+
claude: [],
|
|
1595
|
+
codex: [],
|
|
1596
|
+
opencode: []
|
|
1597
|
+
};
|
|
1598
|
+
const statuses = {
|
|
1599
|
+
claude: {
|
|
1600
|
+
provider: "claude",
|
|
1601
|
+
state: "loading",
|
|
1602
|
+
error: null
|
|
1603
|
+
},
|
|
1604
|
+
codex: {
|
|
1605
|
+
provider: "codex",
|
|
1606
|
+
state: "loading",
|
|
1607
|
+
error: null
|
|
1608
|
+
},
|
|
1609
|
+
opencode: {
|
|
1610
|
+
provider: "opencode",
|
|
1611
|
+
state: "loading",
|
|
1612
|
+
error: null
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
const refreshing = /* @__PURE__ */ new Map();
|
|
1616
|
+
const timers = /* @__PURE__ */ new Map();
|
|
1539
1617
|
const providers = /* @__PURE__ */ new Map();
|
|
1540
1618
|
let watchers = [];
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const
|
|
1544
|
-
|
|
1545
|
-
const [claude, codex, opencode] = await Promise.all([
|
|
1546
|
-
Promise.all(paths.map(async (cwd) => (await listSessions({
|
|
1547
|
-
dir: cwd,
|
|
1548
|
-
includeWorktrees: false,
|
|
1549
|
-
includeProgrammatic: false
|
|
1550
|
-
})).map((item) => ({
|
|
1551
|
-
id: item.sessionId,
|
|
1552
|
-
provider: "claude",
|
|
1553
|
-
title: item.summary,
|
|
1554
|
-
cwd,
|
|
1555
|
-
branch: item.gitBranch ?? null,
|
|
1556
|
-
updatedAt: item.lastModified
|
|
1557
|
-
})))),
|
|
1558
|
-
codexThreads(paths).catch((error) => {
|
|
1559
|
-
console.warn(`could not list Codex threads: ${error}`);
|
|
1560
|
-
return [];
|
|
1561
|
-
}),
|
|
1562
|
-
openCodeSessions(paths).catch((error) => {
|
|
1563
|
-
console.warn(`could not list OpenCode sessions: ${error}`);
|
|
1564
|
-
return [];
|
|
1565
|
-
})
|
|
1566
|
-
]);
|
|
1567
|
-
return [
|
|
1568
|
-
...claude.flat(),
|
|
1569
|
-
...codex.map((thread) => ({
|
|
1570
|
-
id: thread.id,
|
|
1571
|
-
provider: "codex",
|
|
1572
|
-
title: thread.name ?? thread.preview,
|
|
1573
|
-
cwd: thread.cwd,
|
|
1574
|
-
branch: thread.gitInfo?.branch ?? null,
|
|
1575
|
-
updatedAt: thread.updatedAt * 1e3
|
|
1576
|
-
})),
|
|
1577
|
-
...opencode.map((session) => ({
|
|
1578
|
-
id: session.id,
|
|
1579
|
-
provider: "opencode",
|
|
1580
|
-
title: session.title ?? "New session",
|
|
1581
|
-
cwd: session.location.directory,
|
|
1582
|
-
branch: null,
|
|
1583
|
-
updatedAt: session.time.updated
|
|
1584
|
-
}))
|
|
1585
|
-
].map((item) => {
|
|
1586
|
-
providers.set(item.id, item.provider);
|
|
1619
|
+
function sessions() {
|
|
1620
|
+
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
1621
|
+
const known = new Set(projects$1().map((project) => project.id));
|
|
1622
|
+
return allProviders.flatMap((provider) => listed[provider].filter((item) => known.has(item.projectId)).map((item) => {
|
|
1587
1623
|
const record = records.get(item.id);
|
|
1588
1624
|
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
1589
1625
|
return {
|
|
@@ -1591,7 +1627,13 @@ async function sessions() {
|
|
|
1591
1627
|
updatedAt,
|
|
1592
1628
|
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
1593
1629
|
};
|
|
1594
|
-
});
|
|
1630
|
+
}));
|
|
1631
|
+
}
|
|
1632
|
+
function providerStatuses() {
|
|
1633
|
+
return allProviders.map((provider) => statuses[provider]);
|
|
1634
|
+
}
|
|
1635
|
+
function listedUpdatedAt(provider, sessionId) {
|
|
1636
|
+
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
1595
1637
|
}
|
|
1596
1638
|
function providerOf(sessionId) {
|
|
1597
1639
|
const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
@@ -1599,23 +1641,138 @@ function providerOf(sessionId) {
|
|
|
1599
1641
|
return provider;
|
|
1600
1642
|
}
|
|
1601
1643
|
function recordInteraction(id) {
|
|
1602
|
-
db.
|
|
1644
|
+
db.insert(sessions$1).values({
|
|
1645
|
+
sessionId: id,
|
|
1646
|
+
interactedAt: Date.now(),
|
|
1647
|
+
status: "active"
|
|
1648
|
+
}).onConflictDoUpdate({
|
|
1649
|
+
target: sessions$1.sessionId,
|
|
1650
|
+
set: {
|
|
1651
|
+
interactedAt: Date.now(),
|
|
1652
|
+
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
1653
|
+
}
|
|
1654
|
+
}).run();
|
|
1603
1655
|
}
|
|
1604
|
-
|
|
1656
|
+
function publishSessions() {
|
|
1657
|
+
broadcast({
|
|
1658
|
+
type: "sessions",
|
|
1659
|
+
sessions: sessions()
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function refreshAll() {
|
|
1663
|
+
for (const provider of allProviders) refresh(provider);
|
|
1664
|
+
}
|
|
1665
|
+
function refreshSessions(provider) {
|
|
1666
|
+
if (timers.has(provider)) return;
|
|
1667
|
+
timers.set(provider, setTimeout(() => {
|
|
1668
|
+
timers.delete(provider);
|
|
1669
|
+
refresh(provider);
|
|
1670
|
+
}, REFRESH_DELAY));
|
|
1671
|
+
}
|
|
1672
|
+
async function refresh(provider) {
|
|
1673
|
+
const current = refreshing.get(provider);
|
|
1674
|
+
if (current !== void 0) {
|
|
1675
|
+
current.again = true;
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const entry = { again: true };
|
|
1679
|
+
refreshing.set(provider, entry);
|
|
1680
|
+
while (entry.again) {
|
|
1681
|
+
entry.again = false;
|
|
1682
|
+
await list$1(provider);
|
|
1683
|
+
}
|
|
1684
|
+
refreshing.delete(provider);
|
|
1685
|
+
}
|
|
1686
|
+
async function list$1(provider) {
|
|
1687
|
+
const paths = new Map(projects$1().map((project) => [project.path, project.id]));
|
|
1688
|
+
const before = statuses[provider];
|
|
1689
|
+
let timer;
|
|
1605
1690
|
try {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1691
|
+
const found = await Promise.race([find(provider, paths), new Promise((_, reject) => {
|
|
1692
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
|
|
1693
|
+
})]);
|
|
1694
|
+
if (found === null) {
|
|
1695
|
+
listed[provider] = [];
|
|
1696
|
+
statuses[provider] = {
|
|
1697
|
+
provider,
|
|
1698
|
+
state: "missing",
|
|
1699
|
+
error: null
|
|
1700
|
+
};
|
|
1701
|
+
} else {
|
|
1702
|
+
listed[provider] = found;
|
|
1703
|
+
statuses[provider] = {
|
|
1704
|
+
provider,
|
|
1705
|
+
state: "ready",
|
|
1706
|
+
error: null
|
|
1707
|
+
};
|
|
1708
|
+
for (const item of found) providers.set(item.id, provider);
|
|
1709
|
+
}
|
|
1610
1710
|
} catch (error) {
|
|
1611
|
-
|
|
1711
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1712
|
+
console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
1713
|
+
statuses[provider] = {
|
|
1714
|
+
provider,
|
|
1715
|
+
state: "error",
|
|
1716
|
+
error: message
|
|
1717
|
+
};
|
|
1718
|
+
} finally {
|
|
1719
|
+
clearTimeout(timer);
|
|
1720
|
+
}
|
|
1721
|
+
publishSessions();
|
|
1722
|
+
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
1723
|
+
type: "providers",
|
|
1724
|
+
providers: providerStatuses()
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
async function find(provider, paths) {
|
|
1728
|
+
switch (provider) {
|
|
1729
|
+
case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => (await listSessions({
|
|
1730
|
+
dir: cwd,
|
|
1731
|
+
includeWorktrees: false,
|
|
1732
|
+
includeProgrammatic: false
|
|
1733
|
+
})).map((item) => ({
|
|
1734
|
+
id: item.sessionId,
|
|
1735
|
+
provider: "claude",
|
|
1736
|
+
title: item.summary,
|
|
1737
|
+
projectId,
|
|
1738
|
+
branch: item.gitBranch ?? null,
|
|
1739
|
+
updatedAt: item.lastModified
|
|
1740
|
+
}))))).flat();
|
|
1741
|
+
case "codex":
|
|
1742
|
+
if (!codexInstalled()) return null;
|
|
1743
|
+
return (await codexThreads([...paths.keys()])).flatMap((thread) => {
|
|
1744
|
+
const projectId = paths.get(thread.cwd);
|
|
1745
|
+
if (projectId === void 0) return [];
|
|
1746
|
+
return [{
|
|
1747
|
+
id: thread.id,
|
|
1748
|
+
provider: "codex",
|
|
1749
|
+
title: thread.name ?? thread.preview,
|
|
1750
|
+
projectId,
|
|
1751
|
+
branch: thread.gitInfo?.branch ?? null,
|
|
1752
|
+
updatedAt: thread.updatedAt * 1e3
|
|
1753
|
+
}];
|
|
1754
|
+
});
|
|
1755
|
+
case "opencode":
|
|
1756
|
+
if (!openCodeInstalled()) return null;
|
|
1757
|
+
return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
|
|
1758
|
+
const projectId = paths.get(session.location.directory);
|
|
1759
|
+
if (projectId === void 0) return [];
|
|
1760
|
+
return [{
|
|
1761
|
+
id: session.id,
|
|
1762
|
+
provider: "opencode",
|
|
1763
|
+
title: session.title ?? "New session",
|
|
1764
|
+
projectId,
|
|
1765
|
+
branch: null,
|
|
1766
|
+
updatedAt: session.time.updated
|
|
1767
|
+
}];
|
|
1768
|
+
});
|
|
1612
1769
|
}
|
|
1613
1770
|
}
|
|
1614
1771
|
function watchSessions() {
|
|
1615
1772
|
for (const watcher of watchers) watcher.close();
|
|
1616
|
-
watchers = [path.join(readConfig().claudeBaseDir, "projects"), path.join(codexBaseDir, "sessions")].flatMap((dir) => {
|
|
1773
|
+
watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
1617
1774
|
try {
|
|
1618
|
-
const watcher = watch(dir, { recursive: true }, refreshSessions);
|
|
1775
|
+
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
1619
1776
|
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
1620
1777
|
return [watcher];
|
|
1621
1778
|
} catch (error) {
|
|
@@ -1624,12 +1781,6 @@ function watchSessions() {
|
|
|
1624
1781
|
}
|
|
1625
1782
|
});
|
|
1626
1783
|
}
|
|
1627
|
-
function refreshSessions() {
|
|
1628
|
-
timer ??= setTimeout(() => {
|
|
1629
|
-
timer = void 0;
|
|
1630
|
-
publishSessions();
|
|
1631
|
-
}, 1e3);
|
|
1632
|
-
}
|
|
1633
1784
|
async function updateSession(id, patch) {
|
|
1634
1785
|
if (patch.title !== void 0) switch (providerOf(id)) {
|
|
1635
1786
|
case "claude":
|
|
@@ -1640,8 +1791,15 @@ async function updateSession(id, patch) {
|
|
|
1640
1791
|
break;
|
|
1641
1792
|
case "opencode": await renameOpenCode(id, patch.title);
|
|
1642
1793
|
}
|
|
1643
|
-
if (patch.status === "inactive") db.
|
|
1644
|
-
else if (patch.status !== void 0) db.
|
|
1794
|
+
if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
1795
|
+
else if (patch.status !== void 0) db.insert(sessions$1).values({
|
|
1796
|
+
sessionId: id,
|
|
1797
|
+
status: patch.status
|
|
1798
|
+
}).onConflictDoUpdate({
|
|
1799
|
+
target: sessions$1.sessionId,
|
|
1800
|
+
set: { status: patch.status }
|
|
1801
|
+
}).run();
|
|
1802
|
+
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
1645
1803
|
}
|
|
1646
1804
|
async function removeSession(id) {
|
|
1647
1805
|
const provider = providerOf(id);
|
|
@@ -1659,13 +1817,15 @@ async function removeSession(id) {
|
|
|
1659
1817
|
break;
|
|
1660
1818
|
case "opencode": await removeOpenCode(id);
|
|
1661
1819
|
}
|
|
1662
|
-
db.
|
|
1820
|
+
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
1821
|
+
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
1663
1822
|
}
|
|
1664
|
-
async function fork(sessionId,
|
|
1823
|
+
async function fork(sessionId, projectId, messageId) {
|
|
1665
1824
|
const provider = providerOf(sessionId);
|
|
1666
|
-
const result = await forkWith(provider, sessionId,
|
|
1825
|
+
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
1667
1826
|
providers.set(result.sessionId, provider);
|
|
1668
1827
|
recordInteraction(result.sessionId);
|
|
1828
|
+
refreshSessions(provider);
|
|
1669
1829
|
return result;
|
|
1670
1830
|
}
|
|
1671
1831
|
async function forkWith(provider, sessionId, cwd, messageId) {
|
|
@@ -1677,17 +1837,6 @@ async function forkWith(provider, sessionId, cwd, messageId) {
|
|
|
1677
1837
|
upToMessageId: messageId
|
|
1678
1838
|
})).sessionId };
|
|
1679
1839
|
}
|
|
1680
|
-
function readRecords() {
|
|
1681
|
-
const records = /* @__PURE__ */ new Map();
|
|
1682
|
-
for (const row of db.prepare("SELECT session_id, interacted_at, status FROM sessions").all()) {
|
|
1683
|
-
if (typeof row.session_id !== "string" || typeof row.interacted_at !== "number" && row.interacted_at !== null || row.status !== "active" && row.status !== "pinned" && row.status !== "minimized") throw new Error("The sessions table has an unexpected row");
|
|
1684
|
-
records.set(row.session_id, {
|
|
1685
|
-
interactedAt: row.interacted_at,
|
|
1686
|
-
status: row.status
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
return records;
|
|
1690
|
-
}
|
|
1691
1840
|
//#endregion
|
|
1692
1841
|
//#region src/lib/opencode/client.ts
|
|
1693
1842
|
const listChanges = /* @__PURE__ */ new Set([
|
|
@@ -1700,6 +1849,9 @@ const listChanges = /* @__PURE__ */ new Set([
|
|
|
1700
1849
|
"session.execution.failed",
|
|
1701
1850
|
"session.execution.interrupted"
|
|
1702
1851
|
]);
|
|
1852
|
+
const SILENCE_LIMIT$1 = 45e3;
|
|
1853
|
+
const RECONNECT_ATTEMPTS = 5;
|
|
1854
|
+
const RECONNECT_DELAY = 2e3;
|
|
1703
1855
|
let connection;
|
|
1704
1856
|
function findOpenCode() {
|
|
1705
1857
|
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
@@ -1710,16 +1862,12 @@ function findOpenCode() {
|
|
|
1710
1862
|
return null;
|
|
1711
1863
|
}
|
|
1712
1864
|
function openCode() {
|
|
1713
|
-
connection ??=
|
|
1865
|
+
connection ??= start();
|
|
1714
1866
|
return connection;
|
|
1715
1867
|
}
|
|
1716
|
-
async function
|
|
1868
|
+
async function start() {
|
|
1717
1869
|
try {
|
|
1718
|
-
const
|
|
1719
|
-
const client = OpenCode.make({
|
|
1720
|
-
baseUrl: endpoint.url,
|
|
1721
|
-
headers: headers(endpoint)
|
|
1722
|
-
});
|
|
1870
|
+
const client = await connect();
|
|
1723
1871
|
listen(client);
|
|
1724
1872
|
return client;
|
|
1725
1873
|
} catch (error) {
|
|
@@ -1727,27 +1875,61 @@ async function connect() {
|
|
|
1727
1875
|
throw error;
|
|
1728
1876
|
}
|
|
1729
1877
|
}
|
|
1878
|
+
async function connect() {
|
|
1879
|
+
const endpoint = await ensure();
|
|
1880
|
+
return OpenCode.make({
|
|
1881
|
+
baseUrl: endpoint.url,
|
|
1882
|
+
headers: headers(endpoint)
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1730
1885
|
async function listen(client) {
|
|
1886
|
+
const controller = new AbortController();
|
|
1887
|
+
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
1731
1888
|
try {
|
|
1732
|
-
for await (const event of client.event.subscribe(
|
|
1889
|
+
for await (const event of client.event.subscribe({
|
|
1890
|
+
signal: controller.signal,
|
|
1891
|
+
onActivity: () => silence.refresh()
|
|
1892
|
+
})) route(event);
|
|
1733
1893
|
console.warn("the OpenCode event stream ended");
|
|
1734
1894
|
} catch (error) {
|
|
1735
|
-
console.warn(`lost the OpenCode event stream: ${error}`);
|
|
1895
|
+
console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
|
|
1896
|
+
} finally {
|
|
1897
|
+
clearTimeout(silence);
|
|
1736
1898
|
}
|
|
1899
|
+
connection = reconnect();
|
|
1900
|
+
connection.catch(() => void 0);
|
|
1901
|
+
}
|
|
1902
|
+
async function reconnect() {
|
|
1903
|
+
let failure;
|
|
1904
|
+
for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
|
|
1905
|
+
await setTimeout$1(RECONNECT_DELAY);
|
|
1906
|
+
let client;
|
|
1907
|
+
try {
|
|
1908
|
+
client = await connect();
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
failure = error;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
listen(client);
|
|
1914
|
+
await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
|
|
1915
|
+
return client;
|
|
1916
|
+
}
|
|
1917
|
+
console.warn(`could not reconnect to OpenCode: ${failure}`);
|
|
1737
1918
|
connection = void 0;
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1919
|
+
for (const run of openCodeRuns()) run.exited();
|
|
1920
|
+
refreshSessions("opencode");
|
|
1921
|
+
throw failure;
|
|
1922
|
+
}
|
|
1923
|
+
async function catchUp(client) {
|
|
1924
|
+
const active = await client.session.active();
|
|
1925
|
+
for (const run of openCodeRuns()) {
|
|
1926
|
+
const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
|
|
1927
|
+
run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
|
|
1747
1928
|
}
|
|
1929
|
+
refreshSessions("opencode");
|
|
1748
1930
|
}
|
|
1749
1931
|
function route(event) {
|
|
1750
|
-
if (listChanges.has(event.type)) refreshSessions();
|
|
1932
|
+
if (listChanges.has(event.type)) refreshSessions("opencode");
|
|
1751
1933
|
const sessionId = sessionOf(event);
|
|
1752
1934
|
if (sessionId === null) return;
|
|
1753
1935
|
const run = runs.get(sessionId);
|
|
@@ -1888,8 +2070,8 @@ function toolItems(base, tool) {
|
|
|
1888
2070
|
case "shell": return [{
|
|
1889
2071
|
...base,
|
|
1890
2072
|
kind: "command",
|
|
1891
|
-
command: text$
|
|
1892
|
-
description: text$
|
|
2073
|
+
command: text$2(input.command),
|
|
2074
|
+
description: text$2(input.description),
|
|
1893
2075
|
output,
|
|
1894
2076
|
status,
|
|
1895
2077
|
background: null
|
|
@@ -1897,10 +2079,10 @@ function toolItems(base, tool) {
|
|
|
1897
2079
|
case "edit": return [{
|
|
1898
2080
|
...base,
|
|
1899
2081
|
kind: "file_change",
|
|
1900
|
-
path: text$
|
|
2082
|
+
path: text$2(input.path ?? input.filePath),
|
|
1901
2083
|
diffs: [{
|
|
1902
|
-
before: text$
|
|
1903
|
-
after: text$
|
|
2084
|
+
before: text$2(input.oldString),
|
|
2085
|
+
after: text$2(input.newString)
|
|
1904
2086
|
}],
|
|
1905
2087
|
output,
|
|
1906
2088
|
status
|
|
@@ -1908,10 +2090,10 @@ function toolItems(base, tool) {
|
|
|
1908
2090
|
case "write": return [{
|
|
1909
2091
|
...base,
|
|
1910
2092
|
kind: "file_change",
|
|
1911
|
-
path: text$
|
|
2093
|
+
path: text$2(input.path ?? input.filePath),
|
|
1912
2094
|
diffs: [{
|
|
1913
2095
|
before: "",
|
|
1914
|
-
after: text$
|
|
2096
|
+
after: text$2(input.content)
|
|
1915
2097
|
}],
|
|
1916
2098
|
output,
|
|
1917
2099
|
status
|
|
@@ -1923,8 +2105,8 @@ function toolItems(base, tool) {
|
|
|
1923
2105
|
...base,
|
|
1924
2106
|
id: `${base.id}:${index}`,
|
|
1925
2107
|
kind: "file_change",
|
|
1926
|
-
path: text$
|
|
1927
|
-
diffs: hunks(text$
|
|
2108
|
+
path: text$2(record(file).file),
|
|
2109
|
+
diffs: hunks(text$2(record(file).patch)),
|
|
1928
2110
|
output,
|
|
1929
2111
|
status
|
|
1930
2112
|
}));
|
|
@@ -1932,16 +2114,16 @@ function toolItems(base, tool) {
|
|
|
1932
2114
|
...base,
|
|
1933
2115
|
kind: "todo_list",
|
|
1934
2116
|
steps: list(input.todos).map((todo) => ({
|
|
1935
|
-
text: text$
|
|
1936
|
-
status: todoStatuses$1[text$
|
|
2117
|
+
text: text$2(record(todo).content),
|
|
2118
|
+
status: todoStatuses$1[text$2(record(todo).status)] ?? "pending"
|
|
1937
2119
|
}))
|
|
1938
2120
|
}];
|
|
1939
2121
|
case "subagent":
|
|
1940
2122
|
case "task": return [{
|
|
1941
2123
|
...base,
|
|
1942
2124
|
kind: "subagent",
|
|
1943
|
-
agent: text$
|
|
1944
|
-
description: text$
|
|
2125
|
+
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
2126
|
+
description: text$2(input.description),
|
|
1945
2127
|
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
1946
2128
|
status
|
|
1947
2129
|
}];
|
|
@@ -1949,20 +2131,20 @@ function toolItems(base, tool) {
|
|
|
1949
2131
|
case "websearch": return [{
|
|
1950
2132
|
...base,
|
|
1951
2133
|
kind: "web_search",
|
|
1952
|
-
query: tool.name === "websearch" ? text$
|
|
1953
|
-
url: tool.name === "webfetch" ? text$
|
|
2134
|
+
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
2135
|
+
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
1954
2136
|
results: [],
|
|
1955
2137
|
output,
|
|
1956
2138
|
status
|
|
1957
2139
|
}];
|
|
1958
2140
|
case "question": {
|
|
1959
2141
|
const questions = list(input.questions).map((item) => ({
|
|
1960
|
-
question: text$
|
|
1961
|
-
header: text$
|
|
2142
|
+
question: text$2(record(item).question),
|
|
2143
|
+
header: text$2(record(item).header),
|
|
1962
2144
|
multiSelect: record(item).multiple === true,
|
|
1963
2145
|
options: list(record(item).options).map((option) => ({
|
|
1964
|
-
label: text$
|
|
1965
|
-
description: text$
|
|
2146
|
+
label: text$2(record(option).label),
|
|
2147
|
+
description: text$2(record(option).description)
|
|
1966
2148
|
}))
|
|
1967
2149
|
}));
|
|
1968
2150
|
const answers = metadata.answers;
|
|
@@ -1970,14 +2152,14 @@ function toolItems(base, tool) {
|
|
|
1970
2152
|
...base,
|
|
1971
2153
|
kind: "question",
|
|
1972
2154
|
questions,
|
|
1973
|
-
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list(answers[index]).map(text$
|
|
2155
|
+
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list(answers[index]).map(text$2).join(", ")])) : null,
|
|
1974
2156
|
status
|
|
1975
2157
|
}];
|
|
1976
2158
|
}
|
|
1977
2159
|
case "skill": return [{
|
|
1978
2160
|
...base,
|
|
1979
2161
|
kind: "skill",
|
|
1980
|
-
skill: text$
|
|
2162
|
+
skill: text$2(input.name),
|
|
1981
2163
|
output,
|
|
1982
2164
|
status
|
|
1983
2165
|
}];
|
|
@@ -2018,7 +2200,7 @@ function optionValue(options, label) {
|
|
|
2018
2200
|
function contentText(content) {
|
|
2019
2201
|
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
2020
2202
|
}
|
|
2021
|
-
function text$
|
|
2203
|
+
function text$2(value) {
|
|
2022
2204
|
return typeof value === "string" ? value : "";
|
|
2023
2205
|
}
|
|
2024
2206
|
function list(value) {
|
|
@@ -2256,28 +2438,24 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2256
2438
|
prompted = false;
|
|
2257
2439
|
backgroundTasks = 0;
|
|
2258
2440
|
tasks = {};
|
|
2259
|
-
client;
|
|
2260
2441
|
close;
|
|
2261
2442
|
applied;
|
|
2262
2443
|
loading = Promise.resolve();
|
|
2263
2444
|
refreshTimer;
|
|
2264
2445
|
idleTimer;
|
|
2265
2446
|
static async start(cwd, settings) {
|
|
2266
|
-
const
|
|
2267
|
-
const session = await client.session.create({
|
|
2447
|
+
const session = await (await openCode()).session.create({
|
|
2268
2448
|
location: { directory: cwd },
|
|
2269
2449
|
...settings.mode === null ? {} : { agent: settings.mode },
|
|
2270
2450
|
...settings.model === null ? {} : { model: modelRef(settings.model, settings.effort) }
|
|
2271
2451
|
});
|
|
2272
|
-
return new OpenCodeRun(
|
|
2452
|
+
return new OpenCodeRun(session.id, cwd, settings, settings);
|
|
2273
2453
|
}
|
|
2274
2454
|
static async resume(sessionId, cwd, settings) {
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
return new OpenCodeRun(client, sessionId, cwd, settings, sessionSettings(session, settings));
|
|
2455
|
+
const session = await (await openCode()).session.get({ sessionID: sessionId });
|
|
2456
|
+
return new OpenCodeRun(sessionId, cwd, settings, sessionSettings(session, settings));
|
|
2278
2457
|
}
|
|
2279
|
-
constructor(
|
|
2280
|
-
this.client = client;
|
|
2458
|
+
constructor(sessionId, cwd, settings, applied) {
|
|
2281
2459
|
this.sessionId = sessionId;
|
|
2282
2460
|
this.cwd = cwd;
|
|
2283
2461
|
this.settings = settings;
|
|
@@ -2295,7 +2473,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2295
2473
|
}
|
|
2296
2474
|
switch (delivery) {
|
|
2297
2475
|
case "steer":
|
|
2298
|
-
await
|
|
2476
|
+
await (await openCode()).session.prompt({
|
|
2299
2477
|
sessionID: this.sessionId,
|
|
2300
2478
|
text: prompt.text,
|
|
2301
2479
|
files: files(prompt),
|
|
@@ -2332,7 +2510,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2332
2510
|
});
|
|
2333
2511
|
}
|
|
2334
2512
|
async interrupt() {
|
|
2335
|
-
await
|
|
2513
|
+
await (await openCode()).session.interrupt({ sessionID: this.sessionId });
|
|
2336
2514
|
}
|
|
2337
2515
|
stopTask() {
|
|
2338
2516
|
throw new Error("OpenCode has no background tasks to stop");
|
|
@@ -2341,16 +2519,17 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2341
2519
|
const pending = this.pending.get(requestId);
|
|
2342
2520
|
if (pending === void 0) return;
|
|
2343
2521
|
const sessionID = this.sessionId;
|
|
2344
|
-
|
|
2522
|
+
const client = await openCode();
|
|
2523
|
+
if (pending.kind === "permission") await client.permission.reply({
|
|
2345
2524
|
sessionID,
|
|
2346
2525
|
requestID: requestId,
|
|
2347
2526
|
decision: replies[decision]
|
|
2348
2527
|
});
|
|
2349
|
-
else if (decision === "deny") await
|
|
2528
|
+
else if (decision === "deny") await client.session.form.cancel({
|
|
2350
2529
|
sessionID,
|
|
2351
2530
|
formID: requestId
|
|
2352
2531
|
});
|
|
2353
|
-
else await
|
|
2532
|
+
else await client.session.form.reply({
|
|
2354
2533
|
sessionID,
|
|
2355
2534
|
formID: requestId,
|
|
2356
2535
|
answer: formAnswer(pending.form, answers ?? {})
|
|
@@ -2362,11 +2541,15 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2362
2541
|
this.end();
|
|
2363
2542
|
}
|
|
2364
2543
|
exited() {
|
|
2365
|
-
this.put([errorItem(randomUUID(), "Lost the connection to OpenCode")]);
|
|
2544
|
+
this.put([errorItem(randomUUID(), this.pending.size === 0 ? "Lost the connection to OpenCode" : "Lost the connection to OpenCode, its open requests expired")]);
|
|
2366
2545
|
this.stop();
|
|
2367
2546
|
}
|
|
2368
|
-
|
|
2369
|
-
this.refresh();
|
|
2547
|
+
reconcile(client, active, permissions, forms) {
|
|
2548
|
+
this.refresh(client);
|
|
2549
|
+
const open = /* @__PURE__ */ new Set([...permissions.map((permission) => permission.id), ...forms.map((form) => form.id)]);
|
|
2550
|
+
for (const requestId of [...this.pending.keys()]) if (!open.has(requestId)) this.settle(requestId);
|
|
2551
|
+
for (const permission of permissions) if (!this.pending.has(permission.id)) this.ask(permission);
|
|
2552
|
+
for (const form of forms) if (!this.pending.has(form.id)) this.askForm(form);
|
|
2370
2553
|
if (this.running && !active) this.finishTurn();
|
|
2371
2554
|
}
|
|
2372
2555
|
receive(event) {
|
|
@@ -2402,15 +2585,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2402
2585
|
this.settle(event.data.requestID);
|
|
2403
2586
|
return;
|
|
2404
2587
|
case "form.created":
|
|
2405
|
-
this.
|
|
2406
|
-
kind: "form",
|
|
2407
|
-
request: {
|
|
2408
|
-
id: event.data.form.id,
|
|
2409
|
-
kind: "question",
|
|
2410
|
-
questions: formQuestions(event.data.form)
|
|
2411
|
-
},
|
|
2412
|
-
form: event.data.form
|
|
2413
|
-
});
|
|
2588
|
+
this.askForm(event.data.form);
|
|
2414
2589
|
return;
|
|
2415
2590
|
case "form.replied":
|
|
2416
2591
|
case "form.cancelled":
|
|
@@ -2421,18 +2596,18 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2421
2596
|
this.refreshTimer = setTimeout(() => this.refresh(), 100);
|
|
2422
2597
|
}
|
|
2423
2598
|
}
|
|
2424
|
-
refresh() {
|
|
2599
|
+
refresh(client) {
|
|
2425
2600
|
clearTimeout(this.refreshTimer);
|
|
2426
|
-
this.loading = this.loading.then(() => this.load()).catch((error) => console.warn(`could not load OpenCode messages: ${error}`));
|
|
2601
|
+
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => console.warn(`could not load OpenCode messages: ${error}`));
|
|
2427
2602
|
}
|
|
2428
|
-
async load() {
|
|
2429
|
-
const messages = (await
|
|
2603
|
+
async load(client) {
|
|
2604
|
+
const messages = (await client.message.list({
|
|
2430
2605
|
sessionID: this.sessionId,
|
|
2431
2606
|
limit: RECENT,
|
|
2432
2607
|
order: "desc"
|
|
2433
2608
|
})).data.toReversed();
|
|
2434
2609
|
this.put(messages.flatMap((message) => messageItems(message, null)).filter((item) => JSON.stringify(this.items.get(item.id)) !== JSON.stringify(item)));
|
|
2435
|
-
const context = await contextOf(
|
|
2610
|
+
const context = await contextOf(client, this.cwd, messages);
|
|
2436
2611
|
if (context === null || context.tokens === this.context?.tokens) return;
|
|
2437
2612
|
this.context = context;
|
|
2438
2613
|
publish$2(this.sessionId, {
|
|
@@ -2457,16 +2632,17 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2457
2632
|
}
|
|
2458
2633
|
}
|
|
2459
2634
|
async send(prompt) {
|
|
2635
|
+
const client = await openCode();
|
|
2460
2636
|
const sessionID = this.sessionId;
|
|
2461
2637
|
const location = { directory: this.cwd };
|
|
2462
2638
|
const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
|
|
2463
2639
|
if (match !== null) {
|
|
2464
2640
|
if (match[1] === "compact" && prompt.images.length === 0) {
|
|
2465
|
-
await
|
|
2641
|
+
await client.session.compact({ sessionID });
|
|
2466
2642
|
return;
|
|
2467
2643
|
}
|
|
2468
|
-
if ((await
|
|
2469
|
-
await
|
|
2644
|
+
if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
|
|
2645
|
+
await client.session.command({
|
|
2470
2646
|
sessionID,
|
|
2471
2647
|
name: match[1],
|
|
2472
2648
|
text: match[2],
|
|
@@ -2474,9 +2650,9 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2474
2650
|
});
|
|
2475
2651
|
return;
|
|
2476
2652
|
}
|
|
2477
|
-
const skill = (await
|
|
2653
|
+
const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
2478
2654
|
if (skill !== void 0) {
|
|
2479
|
-
await
|
|
2655
|
+
await client.session.prompt({
|
|
2480
2656
|
sessionID,
|
|
2481
2657
|
text: match[2],
|
|
2482
2658
|
files: files(prompt),
|
|
@@ -2485,20 +2661,21 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2485
2661
|
return;
|
|
2486
2662
|
}
|
|
2487
2663
|
}
|
|
2488
|
-
await
|
|
2664
|
+
await client.session.prompt({
|
|
2489
2665
|
sessionID,
|
|
2490
2666
|
text: prompt.text,
|
|
2491
2667
|
files: files(prompt)
|
|
2492
2668
|
});
|
|
2493
2669
|
}
|
|
2494
2670
|
async apply() {
|
|
2671
|
+
const client = await openCode();
|
|
2495
2672
|
const settings = this.settings;
|
|
2496
2673
|
const model = settings.model ?? this.applied.model;
|
|
2497
|
-
if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await
|
|
2674
|
+
if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await client.session.switchModel({
|
|
2498
2675
|
sessionID: this.sessionId,
|
|
2499
2676
|
model: modelRef(model, settings.effort)
|
|
2500
2677
|
});
|
|
2501
|
-
if (settings.mode !== null && settings.mode !== this.applied.mode) await
|
|
2678
|
+
if (settings.mode !== null && settings.mode !== this.applied.mode) await client.session.switchAgent({
|
|
2502
2679
|
sessionID: this.sessionId,
|
|
2503
2680
|
agent: settings.mode
|
|
2504
2681
|
});
|
|
@@ -2531,6 +2708,17 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2531
2708
|
}
|
|
2532
2709
|
});
|
|
2533
2710
|
}
|
|
2711
|
+
askForm(form) {
|
|
2712
|
+
this.addPending({
|
|
2713
|
+
kind: "form",
|
|
2714
|
+
request: {
|
|
2715
|
+
id: form.id,
|
|
2716
|
+
kind: "question",
|
|
2717
|
+
questions: formQuestions(form)
|
|
2718
|
+
},
|
|
2719
|
+
form
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2534
2722
|
addPending(pending) {
|
|
2535
2723
|
this.pending.set(pending.request.id, pending);
|
|
2536
2724
|
publish$2(this.sessionId, {
|
|
@@ -2621,10 +2809,7 @@ function providerName(provider) {
|
|
|
2621
2809
|
return providerNames[provider];
|
|
2622
2810
|
}
|
|
2623
2811
|
function storedAccounts() {
|
|
2624
|
-
return
|
|
2625
|
-
...item,
|
|
2626
|
-
provider: item.provider ?? "claude"
|
|
2627
|
-
}));
|
|
2812
|
+
return readJson("accounts.json") ?? [];
|
|
2628
2813
|
}
|
|
2629
2814
|
function storedAccount(provider, accountId) {
|
|
2630
2815
|
if (accountId === null) throw new Error(`Add a ${providerName(provider)} account in Settings → Accounts`);
|
|
@@ -3031,10 +3216,10 @@ function toItems(messages) {
|
|
|
3031
3216
|
break;
|
|
3032
3217
|
case "tool_use":
|
|
3033
3218
|
if (block.name === "SendMessage") {
|
|
3034
|
-
const owner = owners.get(text(block.input.to));
|
|
3219
|
+
const owner = owners.get(text$1(block.input.to));
|
|
3035
3220
|
if (owner !== void 0) change(owner, { status: "running" });
|
|
3036
3221
|
}
|
|
3037
|
-
if (block.name === "TaskStop") stops.set(block.id, text(block.input.task_id));
|
|
3222
|
+
if (block.name === "TaskStop") stops.set(block.id, text$1(block.input.task_id));
|
|
3038
3223
|
tools.set(block.id, items.length);
|
|
3039
3224
|
items.push(toolItem(block, parent, message.time, (to) => {
|
|
3040
3225
|
const owner = owners.get(to);
|
|
@@ -3125,7 +3310,7 @@ function toRequest(options) {
|
|
|
3125
3310
|
case "ExitPlanMode": return {
|
|
3126
3311
|
id: options.id,
|
|
3127
3312
|
kind: "plan",
|
|
3128
|
-
plan: text(options.input.plan)
|
|
3313
|
+
plan: text$1(options.input.plan)
|
|
3129
3314
|
};
|
|
3130
3315
|
default: return {
|
|
3131
3316
|
id: options.id,
|
|
@@ -3152,8 +3337,8 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3152
3337
|
case "Bash": return {
|
|
3153
3338
|
...base,
|
|
3154
3339
|
kind: "command",
|
|
3155
|
-
command: text(input.command),
|
|
3156
|
-
description: text(input.description),
|
|
3340
|
+
command: text$1(input.command),
|
|
3341
|
+
description: text$1(input.description),
|
|
3157
3342
|
output: "",
|
|
3158
3343
|
status: "running",
|
|
3159
3344
|
background: input.run_in_background === true ? {
|
|
@@ -3165,8 +3350,8 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3165
3350
|
case "Monitor": return {
|
|
3166
3351
|
...base,
|
|
3167
3352
|
kind: "command",
|
|
3168
|
-
command: text(input.command) || (isRecord(input.ws) ? text(input.ws.url) : ""),
|
|
3169
|
-
description: text(input.description),
|
|
3353
|
+
command: text$1(input.command) || (isRecord(input.ws) ? text$1(input.ws.url) : ""),
|
|
3354
|
+
description: text$1(input.description),
|
|
3170
3355
|
output: "",
|
|
3171
3356
|
status: "running",
|
|
3172
3357
|
background: {
|
|
@@ -3178,10 +3363,10 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3178
3363
|
case "Edit": return {
|
|
3179
3364
|
...base,
|
|
3180
3365
|
kind: "file_change",
|
|
3181
|
-
path: text(input.file_path),
|
|
3366
|
+
path: text$1(input.file_path),
|
|
3182
3367
|
diffs: [{
|
|
3183
|
-
before: text(input.old_string),
|
|
3184
|
-
after: text(input.new_string)
|
|
3368
|
+
before: text$1(input.old_string),
|
|
3369
|
+
after: text$1(input.new_string)
|
|
3185
3370
|
}],
|
|
3186
3371
|
output: "",
|
|
3187
3372
|
status: "running"
|
|
@@ -3189,7 +3374,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3189
3374
|
case "MultiEdit": return {
|
|
3190
3375
|
...base,
|
|
3191
3376
|
kind: "file_change",
|
|
3192
|
-
path: text(input.file_path),
|
|
3377
|
+
path: text$1(input.file_path),
|
|
3193
3378
|
diffs: input.edits.map((edit) => ({
|
|
3194
3379
|
before: edit.old_string,
|
|
3195
3380
|
after: edit.new_string
|
|
@@ -3200,10 +3385,10 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3200
3385
|
case "Write": return {
|
|
3201
3386
|
...base,
|
|
3202
3387
|
kind: "file_change",
|
|
3203
|
-
path: text(input.file_path),
|
|
3388
|
+
path: text$1(input.file_path),
|
|
3204
3389
|
diffs: [{
|
|
3205
3390
|
before: "",
|
|
3206
|
-
after: text(input.content)
|
|
3391
|
+
after: text$1(input.content)
|
|
3207
3392
|
}],
|
|
3208
3393
|
output: "",
|
|
3209
3394
|
status: "running"
|
|
@@ -3211,7 +3396,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3211
3396
|
case "NotebookEdit": return {
|
|
3212
3397
|
...base,
|
|
3213
3398
|
kind: "file_change",
|
|
3214
|
-
path: text(input.notebook_path),
|
|
3399
|
+
path: text$1(input.notebook_path),
|
|
3215
3400
|
diffs: [],
|
|
3216
3401
|
output: "",
|
|
3217
3402
|
status: "running"
|
|
@@ -3220,7 +3405,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3220
3405
|
...base,
|
|
3221
3406
|
kind: "web_search",
|
|
3222
3407
|
query: null,
|
|
3223
|
-
url: text(input.url),
|
|
3408
|
+
url: text$1(input.url),
|
|
3224
3409
|
results: [],
|
|
3225
3410
|
output: "",
|
|
3226
3411
|
status: "running"
|
|
@@ -3228,7 +3413,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3228
3413
|
case "WebSearch": return {
|
|
3229
3414
|
...base,
|
|
3230
3415
|
kind: "web_search",
|
|
3231
|
-
query: text(input.query),
|
|
3416
|
+
query: text$1(input.query),
|
|
3232
3417
|
url: null,
|
|
3233
3418
|
results: [],
|
|
3234
3419
|
output: "",
|
|
@@ -3237,7 +3422,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3237
3422
|
case "Skill": return {
|
|
3238
3423
|
...base,
|
|
3239
3424
|
kind: "skill",
|
|
3240
|
-
skill: text(input.skill),
|
|
3425
|
+
skill: text$1(input.skill),
|
|
3241
3426
|
output: "",
|
|
3242
3427
|
status: "running"
|
|
3243
3428
|
};
|
|
@@ -3252,7 +3437,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3252
3437
|
case "ExitPlanMode": return {
|
|
3253
3438
|
...base,
|
|
3254
3439
|
kind: "plan",
|
|
3255
|
-
plan: text(input.plan)
|
|
3440
|
+
plan: text$1(input.plan)
|
|
3256
3441
|
};
|
|
3257
3442
|
case "AskUserQuestion": return {
|
|
3258
3443
|
...base,
|
|
@@ -3263,23 +3448,23 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3263
3448
|
};
|
|
3264
3449
|
case "Agent":
|
|
3265
3450
|
case "Task": {
|
|
3266
|
-
const agent = text(input.subagent_type);
|
|
3451
|
+
const agent = text$1(input.subagent_type);
|
|
3267
3452
|
return {
|
|
3268
3453
|
...base,
|
|
3269
3454
|
kind: "subagent",
|
|
3270
3455
|
agent: agent === "" ? "general-purpose" : agent,
|
|
3271
|
-
description: text(input.description),
|
|
3456
|
+
description: text$1(input.description),
|
|
3272
3457
|
agentId: null,
|
|
3273
3458
|
status: "running"
|
|
3274
3459
|
};
|
|
3275
3460
|
}
|
|
3276
3461
|
case "SendMessage": {
|
|
3277
|
-
const summary = text(input.summary);
|
|
3462
|
+
const summary = text$1(input.summary);
|
|
3278
3463
|
return {
|
|
3279
3464
|
...base,
|
|
3280
3465
|
kind: "agent_message",
|
|
3281
|
-
target: ownerOf(text(input.to)),
|
|
3282
|
-
summary: summary === "" ? text(input.message).split("\n")[0] : summary,
|
|
3466
|
+
target: ownerOf(text$1(input.to)),
|
|
3467
|
+
summary: summary === "" ? text$1(input.message).split("\n")[0] : summary,
|
|
3283
3468
|
status: "running"
|
|
3284
3469
|
};
|
|
3285
3470
|
}
|
|
@@ -3369,7 +3554,7 @@ function userText(value) {
|
|
|
3369
3554
|
text: value
|
|
3370
3555
|
};
|
|
3371
3556
|
}
|
|
3372
|
-
function text(value) {
|
|
3557
|
+
function text$1(value) {
|
|
3373
3558
|
return typeof value === "string" ? value : "";
|
|
3374
3559
|
}
|
|
3375
3560
|
//#endregion
|
|
@@ -3498,11 +3683,13 @@ async function history(sessionId, cwd) {
|
|
|
3498
3683
|
const seen = new Set(messages.map((item) => item.uuid));
|
|
3499
3684
|
const last = kept.findLast((item) => item.type === "assistant" && item.parent_tool_use_id === null);
|
|
3500
3685
|
const tokens = last === void 0 ? null : contextTokens(last.message);
|
|
3686
|
+
const live = run?.messages ?? [];
|
|
3687
|
+
const compacted = live.findLastIndex((item) => item.type === "compaction");
|
|
3501
3688
|
return {
|
|
3502
3689
|
type: "history",
|
|
3503
3690
|
sessionId,
|
|
3504
3691
|
provider: "claude",
|
|
3505
|
-
items: toItems([...messages, ...
|
|
3692
|
+
items: toItems([...messages, ...live.slice(compacted + 1).filter((item) => !seen.has(item.uuid))]),
|
|
3506
3693
|
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
3507
3694
|
queue: run?.queue ?? [],
|
|
3508
3695
|
settings: run?.settings ?? null,
|
|
@@ -4254,60 +4441,43 @@ function walk(root) {
|
|
|
4254
4441
|
}
|
|
4255
4442
|
//#endregion
|
|
4256
4443
|
//#region src/services/push.ts
|
|
4257
|
-
const pushFile = path.join(home, "push.json");
|
|
4258
4444
|
const TITLE_LIMIT = 100;
|
|
4259
4445
|
const BODY_LIMIT = 500;
|
|
4260
4446
|
const TIME_TO_LIVE = 900;
|
|
4261
4447
|
const SEND_TIMEOUT = 1e4;
|
|
4262
4448
|
const SUBJECT = "https://github.com/NazmusSayad/offsite";
|
|
4263
4449
|
let server$1 = "";
|
|
4264
|
-
let active = false;
|
|
4265
4450
|
function startPush(address) {
|
|
4266
4451
|
server$1 = address;
|
|
4267
|
-
try {
|
|
4268
|
-
const store = readJson("push.json");
|
|
4269
|
-
if (store !== null) track(store);
|
|
4270
|
-
} catch (error) {
|
|
4271
|
-
console.warn(`push notifications are off until a browser turns them on again, could not read ${pushFile}: ${error}`);
|
|
4272
|
-
}
|
|
4273
4452
|
}
|
|
4274
4453
|
function pushActive() {
|
|
4275
|
-
return
|
|
4454
|
+
return ours().length > 0;
|
|
4276
4455
|
}
|
|
4277
4456
|
function pushKey() {
|
|
4278
|
-
return { publicKey:
|
|
4457
|
+
return { publicKey: machineInfo().pushPublicKey };
|
|
4279
4458
|
}
|
|
4280
4459
|
function subscribe(subscription) {
|
|
4281
4460
|
if (new URL(subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
});
|
|
4461
|
+
const values = {
|
|
4462
|
+
endpoint: subscription.endpoint,
|
|
4463
|
+
p256dh: subscription.keys.p256dh,
|
|
4464
|
+
auth: subscription.keys.auth,
|
|
4465
|
+
server: server$1
|
|
4466
|
+
};
|
|
4467
|
+
db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
|
|
4468
|
+
target: pushSubscriptions.endpoint,
|
|
4469
|
+
set: values
|
|
4470
|
+
}).run();
|
|
4293
4471
|
console.log("a browser subscribed to push notifications");
|
|
4294
4472
|
}
|
|
4295
4473
|
function unsubscribe(endpoint) {
|
|
4296
|
-
|
|
4297
|
-
store.subscriptions = store.subscriptions.filter((existing) => existing.endpoint !== endpoint);
|
|
4298
|
-
});
|
|
4474
|
+
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4299
4475
|
console.log("a browser unsubscribed from push notifications");
|
|
4300
4476
|
}
|
|
4301
4477
|
async function sendPush(message) {
|
|
4302
|
-
|
|
4303
|
-
try {
|
|
4304
|
-
store = read();
|
|
4305
|
-
} catch (error) {
|
|
4306
|
-
console.error(`could not read push subscriptions: ${error}`);
|
|
4307
|
-
return;
|
|
4308
|
-
}
|
|
4309
|
-
const subscriptions = store.subscriptions.filter(ours);
|
|
4478
|
+
const subscriptions = ours();
|
|
4310
4479
|
if (subscriptions.length === 0) return;
|
|
4480
|
+
const keys = machineInfo();
|
|
4311
4481
|
const payload = JSON.stringify({
|
|
4312
4482
|
...message,
|
|
4313
4483
|
title: [...message.title].slice(0, TITLE_LIMIT).join(""),
|
|
@@ -4316,11 +4486,17 @@ async function sendPush(message) {
|
|
|
4316
4486
|
const gone = [];
|
|
4317
4487
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4318
4488
|
try {
|
|
4319
|
-
await webPush.sendNotification(
|
|
4489
|
+
await webPush.sendNotification({
|
|
4490
|
+
endpoint: subscription.endpoint,
|
|
4491
|
+
keys: {
|
|
4492
|
+
p256dh: subscription.p256dh,
|
|
4493
|
+
auth: subscription.auth
|
|
4494
|
+
}
|
|
4495
|
+
}, payload, {
|
|
4320
4496
|
vapidDetails: {
|
|
4321
4497
|
subject: SUBJECT,
|
|
4322
|
-
publicKey:
|
|
4323
|
-
privateKey:
|
|
4498
|
+
publicKey: keys.pushPublicKey,
|
|
4499
|
+
privateKey: keys.pushPrivateKey
|
|
4324
4500
|
},
|
|
4325
4501
|
TTL: TIME_TO_LIVE,
|
|
4326
4502
|
urgency: "high",
|
|
@@ -4342,59 +4518,10 @@ async function sendPush(message) {
|
|
|
4342
4518
|
}
|
|
4343
4519
|
}));
|
|
4344
4520
|
if (gone.length === 0) return;
|
|
4345
|
-
|
|
4346
|
-
update((current) => {
|
|
4347
|
-
current.subscriptions = current.subscriptions.filter((subscription) => !gone.includes(subscription.endpoint));
|
|
4348
|
-
});
|
|
4349
|
-
} catch (error) {
|
|
4350
|
-
console.error(`could not remove gone push subscriptions: ${error}`);
|
|
4351
|
-
}
|
|
4521
|
+
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4352
4522
|
}
|
|
4353
|
-
function ours(
|
|
4354
|
-
return
|
|
4355
|
-
}
|
|
4356
|
-
function track(store) {
|
|
4357
|
-
active = store.subscriptions.some(ours);
|
|
4358
|
-
}
|
|
4359
|
-
function load() {
|
|
4360
|
-
let store;
|
|
4361
|
-
try {
|
|
4362
|
-
store = readJson("push.json");
|
|
4363
|
-
} catch (error) {
|
|
4364
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
4365
|
-
return replaceCorrupt(error);
|
|
4366
|
-
}
|
|
4367
|
-
if (store === null) return create();
|
|
4368
|
-
if (typeof store.publicKey !== "string" || typeof store.privateKey !== "string" || !Array.isArray(store.subscriptions)) return replaceCorrupt(/* @__PURE__ */ new Error("it is missing its keys or subscriptions"));
|
|
4369
|
-
return store;
|
|
4370
|
-
}
|
|
4371
|
-
function replaceCorrupt(error) {
|
|
4372
|
-
const aside = `${pushFile}.corrupt-${randomUUID()}`;
|
|
4373
|
-
renameSync(pushFile, aside);
|
|
4374
|
-
console.warn(`${pushFile} is corrupt, moved it to ${aside} and created a new one: ${error}`);
|
|
4375
|
-
return create();
|
|
4376
|
-
}
|
|
4377
|
-
function create() {
|
|
4378
|
-
const store = {
|
|
4379
|
-
...webPush.generateVAPIDKeys(),
|
|
4380
|
-
subscriptions: []
|
|
4381
|
-
};
|
|
4382
|
-
save(store);
|
|
4383
|
-
return store;
|
|
4384
|
-
}
|
|
4385
|
-
function save(store) {
|
|
4386
|
-
writeJson("push.json", store);
|
|
4387
|
-
}
|
|
4388
|
-
function read() {
|
|
4389
|
-
const store = load();
|
|
4390
|
-
track(store);
|
|
4391
|
-
return store;
|
|
4392
|
-
}
|
|
4393
|
-
function update(change) {
|
|
4394
|
-
const store = load();
|
|
4395
|
-
change(store);
|
|
4396
|
-
save(store);
|
|
4397
|
-
track(store);
|
|
4523
|
+
function ours() {
|
|
4524
|
+
return db.select().from(pushSubscriptions).where(eq(pushSubscriptions.server, server$1)).all();
|
|
4398
4525
|
}
|
|
4399
4526
|
//#endregion
|
|
4400
4527
|
//#region src/services/runs.ts
|
|
@@ -4415,9 +4542,10 @@ function rewind(request) {
|
|
|
4415
4542
|
}
|
|
4416
4543
|
async function rewindNow(request) {
|
|
4417
4544
|
const sessionId = request.sessionId;
|
|
4418
|
-
|
|
4545
|
+
const cwd = projectPath(request.projectId);
|
|
4546
|
+
if (runs.get(sessionId)?.running === true) throw new Error(`Stop ${providerName(providerOf(sessionId))} before rewinding`);
|
|
4419
4547
|
if ((runs.get(sessionId)?.backgroundTasks ?? 0) > 0) throw new Error("Wait for background tasks to finish before rewinding");
|
|
4420
|
-
const run = await runFor(sessionId,
|
|
4548
|
+
const run = await runFor(sessionId, cwd, request.settings);
|
|
4421
4549
|
if (run.provider === "codex") {
|
|
4422
4550
|
if (request.code) throw new Error("Codex cannot restore files");
|
|
4423
4551
|
if (request.dryRun || !request.conversation) return null;
|
|
@@ -4430,7 +4558,7 @@ async function rewindNow(request) {
|
|
|
4430
4558
|
const result = await rewindOpenCode(request);
|
|
4431
4559
|
if (request.dryRun) return result;
|
|
4432
4560
|
run.items.clear();
|
|
4433
|
-
publish$2(sessionId, await openCodeHistory(sessionId,
|
|
4561
|
+
publish$2(sessionId, await openCodeHistory(sessionId, cwd));
|
|
4434
4562
|
return result;
|
|
4435
4563
|
}
|
|
4436
4564
|
let result = null;
|
|
@@ -4444,58 +4572,63 @@ async function rewindNow(request) {
|
|
|
4444
4572
|
};
|
|
4445
4573
|
}
|
|
4446
4574
|
if (request.dryRun || !request.conversation) return result;
|
|
4447
|
-
const disk = await getSessionMessages(sessionId, { dir:
|
|
4575
|
+
const disk = await getSessionMessages(sessionId, { dir: cwd });
|
|
4448
4576
|
const index = disk.findIndex((item) => item.uuid === request.messageId);
|
|
4449
4577
|
if (index < 1) throw new Error("Cannot rewind the conversation to that point");
|
|
4450
4578
|
run.stop();
|
|
4451
4579
|
await run.closed;
|
|
4452
4580
|
new Run({
|
|
4453
4581
|
sessionId,
|
|
4454
|
-
cwd
|
|
4582
|
+
cwd,
|
|
4455
4583
|
resume: true,
|
|
4456
4584
|
resumeAt: disk[index - 1].uuid,
|
|
4457
4585
|
settings: request.settings
|
|
4458
4586
|
});
|
|
4459
|
-
publish$2(sessionId, await history(sessionId,
|
|
4587
|
+
publish$2(sessionId, await history(sessionId, cwd));
|
|
4460
4588
|
return result;
|
|
4461
4589
|
}
|
|
4462
|
-
async function open$1(client, sessionId,
|
|
4590
|
+
async function open$1(client, sessionId, projectId) {
|
|
4463
4591
|
client.opening = sessionId;
|
|
4464
|
-
const event = await sessionHistory(sessionId,
|
|
4592
|
+
const event = await sessionHistory(sessionId, projectPath(projectId));
|
|
4465
4593
|
if (client.opening !== sessionId) return;
|
|
4466
4594
|
client.sessionId = sessionId;
|
|
4467
4595
|
client.send(event);
|
|
4468
4596
|
}
|
|
4469
4597
|
async function send$1(client, request) {
|
|
4470
|
-
|
|
4598
|
+
const cwd = projectPath(request.projectId);
|
|
4599
|
+
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
|
|
4471
4600
|
if (request.sessionId === null) {
|
|
4472
|
-
const run = await startRun(
|
|
4601
|
+
const run = await startRun(cwd, request.settings);
|
|
4473
4602
|
const sessionId = run.sessionId;
|
|
4474
4603
|
client.opening = sessionId;
|
|
4475
4604
|
client.sessionId = sessionId;
|
|
4476
4605
|
client.send({
|
|
4477
4606
|
type: "started",
|
|
4478
4607
|
sessionId,
|
|
4479
|
-
|
|
4608
|
+
projectId: request.projectId
|
|
4480
4609
|
});
|
|
4481
4610
|
await run.submit(request.prompt, request.delivery);
|
|
4482
4611
|
recordInteraction(sessionId);
|
|
4483
4612
|
return;
|
|
4484
4613
|
}
|
|
4485
4614
|
const sessionId = request.sessionId;
|
|
4486
|
-
await locked(sessionId, () => sendExisting(client, sessionId, request));
|
|
4615
|
+
await locked(sessionId, () => sendExisting(client, sessionId, cwd, request));
|
|
4487
4616
|
}
|
|
4488
|
-
async function sendExisting(client, sessionId, request) {
|
|
4617
|
+
async function sendExisting(client, sessionId, cwd, request) {
|
|
4489
4618
|
const provider = providerOf(sessionId);
|
|
4490
4619
|
if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
|
|
4491
4620
|
const existing = runs.get(sessionId);
|
|
4492
|
-
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing,
|
|
4621
|
+
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing, cwd)) {
|
|
4493
4622
|
existing.stop();
|
|
4494
4623
|
await existing.closed;
|
|
4495
4624
|
}
|
|
4496
|
-
|
|
4625
|
+
if (existing?.provider === "codex" && !existing.running && existing.pending.size === 0 && (listedUpdatedAt("codex", sessionId) ?? 0) > existing.activeAt) {
|
|
4626
|
+
existing.stop();
|
|
4627
|
+
await existing.closed;
|
|
4628
|
+
}
|
|
4629
|
+
const run = await runFor(sessionId, cwd, request.settings);
|
|
4497
4630
|
if (run === existing) await run.configure(request.settings);
|
|
4498
|
-
else await open$1(client, sessionId, request.
|
|
4631
|
+
else await open$1(client, sessionId, request.projectId);
|
|
4499
4632
|
await run.submit(request.prompt, request.delivery);
|
|
4500
4633
|
recordInteraction(sessionId);
|
|
4501
4634
|
}
|
|
@@ -5139,7 +5272,7 @@ async function request(connection, message) {
|
|
|
5139
5272
|
return;
|
|
5140
5273
|
}
|
|
5141
5274
|
case "createTab":
|
|
5142
|
-
await createTab(message.
|
|
5275
|
+
await createTab(message.projectId);
|
|
5143
5276
|
break;
|
|
5144
5277
|
case "closeTab":
|
|
5145
5278
|
closeTab(message.id);
|
|
@@ -5163,17 +5296,17 @@ async function request(connection, message) {
|
|
|
5163
5296
|
break;
|
|
5164
5297
|
}
|
|
5165
5298
|
case "select": {
|
|
5166
|
-
const project = projects.get(message.
|
|
5299
|
+
const project = projects.get(message.projectId) ?? {
|
|
5167
5300
|
tabs: [],
|
|
5168
5301
|
activeTab: null
|
|
5169
5302
|
};
|
|
5170
|
-
projects.set(message.
|
|
5303
|
+
projects.set(message.projectId, project);
|
|
5171
5304
|
const tab = project.tabs.find((item) => item.id === message.tabId);
|
|
5172
5305
|
if (tab !== void 0) {
|
|
5173
5306
|
tab.minimized = false;
|
|
5174
5307
|
project.activeTab = tab.id;
|
|
5175
5308
|
}
|
|
5176
|
-
if (project.tabs.length === 0) await createTab(message.
|
|
5309
|
+
if (project.tabs.length === 0) await createTab(message.projectId);
|
|
5177
5310
|
break;
|
|
5178
5311
|
}
|
|
5179
5312
|
}
|
|
@@ -5206,16 +5339,17 @@ function input(connection, bytes) {
|
|
|
5206
5339
|
}
|
|
5207
5340
|
terminal.pty.write(Buffer.from(frame.data));
|
|
5208
5341
|
}
|
|
5209
|
-
async function createTab(
|
|
5342
|
+
async function createTab(projectId) {
|
|
5343
|
+
const cwd = projectPath(projectId);
|
|
5210
5344
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
5211
5345
|
console.error(`${cwd} is not a folder, terminal not created`);
|
|
5212
5346
|
return;
|
|
5213
5347
|
}
|
|
5214
|
-
const project = projects.get(
|
|
5348
|
+
const project = projects.get(projectId) ?? {
|
|
5215
5349
|
tabs: [],
|
|
5216
5350
|
activeTab: null
|
|
5217
5351
|
};
|
|
5218
|
-
projects.set(
|
|
5352
|
+
projects.set(projectId, project);
|
|
5219
5353
|
const active = project.activeTab === null ? void 0 : terminals.get(project.activeTab);
|
|
5220
5354
|
let directory = cwd;
|
|
5221
5355
|
if (active !== void 0) try {
|
|
@@ -5348,7 +5482,8 @@ function push(id, alert, windowTitle) {
|
|
|
5348
5482
|
sendPush({
|
|
5349
5483
|
title,
|
|
5350
5484
|
body,
|
|
5351
|
-
|
|
5485
|
+
machineId: machineInfo().id,
|
|
5486
|
+
projectId: entry[0],
|
|
5352
5487
|
tabId: id
|
|
5353
5488
|
});
|
|
5354
5489
|
}
|
|
@@ -5388,33 +5523,38 @@ function send(connection, event) {
|
|
|
5388
5523
|
//#region src/api/socket.ts
|
|
5389
5524
|
const handlers = {
|
|
5390
5525
|
ping: () => null,
|
|
5391
|
-
sync:
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5526
|
+
sync: () => {
|
|
5527
|
+
refreshAll();
|
|
5528
|
+
return {
|
|
5529
|
+
...configInfo(),
|
|
5530
|
+
protocol: 3,
|
|
5531
|
+
machineId: machineInfo().id,
|
|
5532
|
+
hostname: os.hostname(),
|
|
5533
|
+
projects: projects$1(),
|
|
5534
|
+
sessions: sessions(),
|
|
5535
|
+
providers: providerStatuses(),
|
|
5536
|
+
activity: activity()
|
|
5537
|
+
};
|
|
5538
|
+
},
|
|
5399
5539
|
"config.get": () => configInfo(),
|
|
5400
5540
|
"config.save": (params) => {
|
|
5401
5541
|
const before = readConfig().claudeBaseDir;
|
|
5402
5542
|
const saved = saveConfig(params);
|
|
5403
5543
|
if (saved.claudeBaseDir === before) return saved;
|
|
5404
5544
|
watchSessions();
|
|
5405
|
-
|
|
5545
|
+
refreshAll();
|
|
5406
5546
|
publishAccounts();
|
|
5407
5547
|
return saved;
|
|
5408
5548
|
},
|
|
5409
5549
|
"projects.add": async (params) => {
|
|
5410
5550
|
const project = await addProject(params.path);
|
|
5411
|
-
|
|
5551
|
+
refreshAll();
|
|
5412
5552
|
return project;
|
|
5413
5553
|
},
|
|
5414
|
-
"projects.update": (params) => updateProject(params.
|
|
5554
|
+
"projects.update": (params) => updateProject(params.id, params.patch),
|
|
5415
5555
|
"projects.remove": (params) => {
|
|
5416
|
-
removeProject(params.
|
|
5417
|
-
|
|
5556
|
+
removeProject(params.id);
|
|
5557
|
+
refreshAll();
|
|
5418
5558
|
return null;
|
|
5419
5559
|
},
|
|
5420
5560
|
"directories.list": (params) => directories(params.path),
|
|
@@ -5435,14 +5575,14 @@ const handlers = {
|
|
|
5435
5575
|
return null;
|
|
5436
5576
|
},
|
|
5437
5577
|
"sessions.open": async (params, client) => {
|
|
5438
|
-
await open$1(client, params.sessionId, params.
|
|
5578
|
+
await open$1(client, params.sessionId, params.projectId);
|
|
5439
5579
|
return null;
|
|
5440
5580
|
},
|
|
5441
5581
|
"sessions.earlier": (params) => earlier(params.sessionId, params.before),
|
|
5442
5582
|
"sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
|
|
5443
5583
|
"sessions.agent": (params) => {
|
|
5444
5584
|
switch (providerOf(params.sessionId)) {
|
|
5445
|
-
case "claude": return agentMessages(params.sessionId, params.
|
|
5585
|
+
case "claude": return agentMessages(params.sessionId, projectPath(params.projectId), params.agentId, params.parent);
|
|
5446
5586
|
case "codex": return codexAgent(params.agentId, params.parent);
|
|
5447
5587
|
case "opencode": return openCodeAgent(params.agentId, params.parent);
|
|
5448
5588
|
}
|
|
@@ -5480,7 +5620,7 @@ const handlers = {
|
|
|
5480
5620
|
},
|
|
5481
5621
|
"sessions.rewind": (params) => rewind(params),
|
|
5482
5622
|
"sessions.fork": async (params) => {
|
|
5483
|
-
const result = await fork(params.sessionId, params.
|
|
5623
|
+
const result = await fork(params.sessionId, params.projectId, params.messageId);
|
|
5484
5624
|
publishSessions();
|
|
5485
5625
|
return result;
|
|
5486
5626
|
},
|
|
@@ -5500,17 +5640,17 @@ const handlers = {
|
|
|
5500
5640
|
publishAccounts();
|
|
5501
5641
|
return null;
|
|
5502
5642
|
},
|
|
5503
|
-
capabilities: (params) => capabilities(params.accountId, params.
|
|
5504
|
-
"files.list": (params) => listFiles(params.
|
|
5505
|
-
"files.read": (params) => readProjectFile(params.
|
|
5643
|
+
capabilities: (params) => capabilities(params.accountId, projectPath(params.projectId)),
|
|
5644
|
+
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
5645
|
+
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
5506
5646
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
5507
5647
|
"tasks.output": (params) => taskOutput(params.sessionId, params.path),
|
|
5508
|
-
"git.status": (params) => gitStatus(params.
|
|
5509
|
-
"git.diff": (params) => projectDiff(params.
|
|
5510
|
-
"git.commit": (params) => commit(params.
|
|
5511
|
-
"git.push": (params) => push$1(params.
|
|
5512
|
-
"git.pullRequest": (params) => createPullRequest(params.
|
|
5513
|
-
"git.message": (params) => suggestCommitMessage(params.provider, params.accountId, params.
|
|
5648
|
+
"git.status": (params) => gitStatus(projectPath(params.projectId)),
|
|
5649
|
+
"git.diff": (params) => projectDiff(projectPath(params.projectId)),
|
|
5650
|
+
"git.commit": (params) => commit(projectPath(params.projectId), params.message),
|
|
5651
|
+
"git.push": (params) => push$1(projectPath(params.projectId)),
|
|
5652
|
+
"git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
|
|
5653
|
+
"git.message": (params) => suggestCommitMessage(params.provider, params.accountId, projectPath(params.projectId)),
|
|
5514
5654
|
"push.key": () => pushKey(),
|
|
5515
5655
|
"push.subscribe": (params) => {
|
|
5516
5656
|
subscribe(params);
|
|
@@ -5579,40 +5719,32 @@ async function answer(client, text) {
|
|
|
5579
5719
|
//#endregion
|
|
5580
5720
|
//#region src/storage/token.ts
|
|
5581
5721
|
function readTokens() {
|
|
5582
|
-
|
|
5583
|
-
return stored === null ? [] : stored.tokens;
|
|
5722
|
+
return db.select().from(tokens).orderBy(tokens.createdAt).all();
|
|
5584
5723
|
}
|
|
5585
5724
|
function addToken(name) {
|
|
5586
5725
|
const token = randomBytes(32).toString("base64url");
|
|
5587
|
-
|
|
5588
|
-
token,
|
|
5726
|
+
db.insert(tokens).values({
|
|
5727
|
+
hash: hashToken(token),
|
|
5589
5728
|
name,
|
|
5590
5729
|
createdAt: Date.now()
|
|
5591
|
-
}
|
|
5730
|
+
}).run();
|
|
5592
5731
|
return token;
|
|
5593
5732
|
}
|
|
5594
|
-
function removeTokens(
|
|
5595
|
-
|
|
5733
|
+
function removeTokens(hashes) {
|
|
5734
|
+
db.delete(tokens).where(inArray(tokens.hash, hashes)).run();
|
|
5596
5735
|
}
|
|
5597
|
-
function
|
|
5598
|
-
return
|
|
5736
|
+
function hasTokens() {
|
|
5737
|
+
return db.select().from(tokens).limit(1).get() !== void 0;
|
|
5599
5738
|
}
|
|
5600
5739
|
function matchesToken(given) {
|
|
5601
5740
|
if (given === null) return false;
|
|
5602
|
-
|
|
5603
|
-
return readTokens().some((item) => {
|
|
5604
|
-
const expected = Buffer.from(item.token);
|
|
5605
|
-
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
5606
|
-
});
|
|
5607
|
-
}
|
|
5608
|
-
function readUsage() {
|
|
5609
|
-
return readJson("token-usage.json") ?? {};
|
|
5741
|
+
return db.select().from(tokens).where(eq(tokens.hash, hashToken(given))).get() !== void 0;
|
|
5610
5742
|
}
|
|
5611
5743
|
function recordUse(token) {
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5744
|
+
db.update(tokens).set({ lastUsedAt: Date.now() }).where(eq(tokens.hash, hashToken(token))).run();
|
|
5745
|
+
}
|
|
5746
|
+
function hashToken(token) {
|
|
5747
|
+
return createHash("sha256").update(token).digest("hex");
|
|
5616
5748
|
}
|
|
5617
5749
|
//#endregion
|
|
5618
5750
|
//#region src/main.ts
|
|
@@ -5654,12 +5786,13 @@ if (args.command === "unpair") {
|
|
|
5654
5786
|
lockHome();
|
|
5655
5787
|
process.env.CLAUDE_CONFIG_DIR = readConfig().claudeBaseDir;
|
|
5656
5788
|
watchSessions();
|
|
5789
|
+
refreshAll();
|
|
5657
5790
|
if (findClaude() === null) {
|
|
5658
5791
|
console.error("could not find claude on PATH; install Claude Code first");
|
|
5659
5792
|
process.exit(1);
|
|
5660
5793
|
}
|
|
5661
5794
|
const publicDir = path.join(import.meta.dirname, "..", "..", "web", "dist");
|
|
5662
|
-
const token = args.auth && args.pair ?
|
|
5795
|
+
const token = args.auth && args.pair && !hasTokens() ? addToken(null) : null;
|
|
5663
5796
|
const server = createServer((request, response) => {
|
|
5664
5797
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5665
5798
|
if (url.pathname === "/version") {
|
|
@@ -5667,7 +5800,8 @@ const server = createServer((request, response) => {
|
|
|
5667
5800
|
response.setHeader("content-type", "application/json");
|
|
5668
5801
|
response.end(JSON.stringify({
|
|
5669
5802
|
version,
|
|
5670
|
-
protocol:
|
|
5803
|
+
protocol: 3,
|
|
5804
|
+
machineId: machineInfo().id,
|
|
5671
5805
|
authorized: authorized(url)
|
|
5672
5806
|
}));
|
|
5673
5807
|
return;
|
|
@@ -5684,7 +5818,7 @@ const paired = /* @__PURE__ */ new Map();
|
|
|
5684
5818
|
server.on("upgrade", (request, socket, head) => {
|
|
5685
5819
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5686
5820
|
const given = url.searchParams.get("token");
|
|
5687
|
-
if (url.pathname !== "/ws" || url.searchParams.get("protocol") !== String(
|
|
5821
|
+
if (url.pathname !== "/ws" || url.searchParams.get("protocol") !== String(3) || !authorized(url)) {
|
|
5688
5822
|
socket.destroy();
|
|
5689
5823
|
return;
|
|
5690
5824
|
}
|
|
@@ -5701,10 +5835,13 @@ if (args.auth) setInterval(() => {
|
|
|
5701
5835
|
}, UNPAIR_CHECK);
|
|
5702
5836
|
server.listen(port, args.host, () => {
|
|
5703
5837
|
startPush(`${args.host}:${port}`);
|
|
5704
|
-
if (args.pair) {
|
|
5838
|
+
if (args.pair && (token !== null || !args.auth)) {
|
|
5705
5839
|
const links = pairingLinks(token);
|
|
5706
5840
|
if (args.command === "web") console.log(`offsite is running at ${links.local}`);
|
|
5707
5841
|
console.log(`Open it on ${HOSTED_APP}: ${links.hosted}`);
|
|
5842
|
+
} else if (args.pair) {
|
|
5843
|
+
console.log(`offsite is running at ${address}`);
|
|
5844
|
+
console.log("Run offsite pair to pair another browser");
|
|
5708
5845
|
} else {
|
|
5709
5846
|
console.log(`offsite is running at ${address}`);
|
|
5710
5847
|
console.log(`Open it on ${HOSTED_APP}`);
|
|
@@ -5730,23 +5867,18 @@ async function unpair() {
|
|
|
5730
5867
|
console.error("offsite unpair needs an interactive terminal");
|
|
5731
5868
|
process.exit(1);
|
|
5732
5869
|
}
|
|
5733
|
-
const usage = readUsage();
|
|
5734
5870
|
const picked = await consola.prompt("Which browsers should be unpaired?", {
|
|
5735
5871
|
type: "multiselect",
|
|
5736
5872
|
required: false,
|
|
5737
5873
|
cancel: "null",
|
|
5738
|
-
options: tokens.map((item) => {
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
used === void 0 ? "never used" : `last used ${new Date(used).toLocaleString()}`
|
|
5747
|
-
].join(" · ")
|
|
5748
|
-
};
|
|
5749
|
-
})
|
|
5874
|
+
options: tokens.map((item) => ({
|
|
5875
|
+
value: item.hash,
|
|
5876
|
+
label: [
|
|
5877
|
+
item.name ?? "Unnamed",
|
|
5878
|
+
`paired ${new Date(item.createdAt).toLocaleString()}`,
|
|
5879
|
+
item.lastUsedAt === null ? "never used" : `last used ${new Date(item.lastUsedAt).toLocaleString()}`
|
|
5880
|
+
].join(" · ")
|
|
5881
|
+
}))
|
|
5750
5882
|
});
|
|
5751
5883
|
if (picked === null || picked.length === 0) {
|
|
5752
5884
|
console.log("Nothing unpaired");
|