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