offsite 0.0.10 → 0.0.11
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 +7 -7
- package/apps/server/dist/main.mjs +1829 -1750
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261017103559_browser_push_keys/migration.sql +11 -0
- package/apps/server/drizzle/20261017103559_browser_push_keys/snapshot.json +265 -0
- package/apps/server/drizzle/20261017110336_browser_machine_ids/migration.sql +3 -0
- package/apps/server/drizzle/20261017110336_browser_machine_ids/snapshot.json +252 -0
- package/apps/web/dist/assets/{angular-html-BEUtPJ31.js → angular-html-DTNmgvth.js} +1 -1
- package/apps/web/dist/assets/{angular-ts-Bnr4rzFr.js → angular-ts-DOOLa99A.js} +1 -1
- package/apps/web/dist/assets/{apl-B-JNBTdP.js → apl-CSWmNqJI.js} +1 -1
- package/apps/web/dist/assets/{astro-DLjK95IW.js → astro-CK7We5bo.js} +1 -1
- package/apps/web/dist/assets/{blade-PLMCMBeb.js → blade-B9Q94PMc.js} +1 -1
- package/apps/web/dist/assets/{c-D5z8xNe6.js → c-D05zRIsd.js} +1 -1
- package/apps/web/dist/assets/{chapel-B9y2HxI_.js → chapel-CJbjuVPR.js} +1 -1
- package/apps/web/dist/assets/{cobol-BSwRu2X7.js → cobol-CE0VhU2R.js} +1 -1
- package/apps/web/dist/assets/{coffee-BAWFR6iF.js → coffee-C0i-RL39.js} +1 -1
- package/apps/web/dist/assets/{cpp-CO-vZI4m.js → cpp-CBJhfD_V.js} +1 -1
- package/apps/web/dist/assets/{crystal-BSfyPt6R.js → crystal-DRdgVTW1.js} +1 -1
- package/apps/web/dist/assets/{css-BOzKs1dt.js → css-DgMip4qW.js} +1 -1
- package/apps/web/dist/assets/{edge-OBqkt95y.js → edge-BCmeW_3H.js} +1 -1
- package/apps/web/dist/assets/{elixir-B14D2jMM.js → elixir-BiRSxKnc.js} +1 -1
- package/apps/web/dist/assets/{elm-DgfIayFq.js → elm-zBiqCp9W.js} +1 -1
- package/apps/web/dist/assets/{erb-w-PUv7ic.js → erb-1QviMyT9.js} +1 -1
- package/apps/web/dist/assets/{git-rebase-DnzmyV_3.js → git-rebase-TjFhp_Gn.js} +1 -1
- package/apps/web/dist/assets/{glimmer-js-CLephpyX.js → glimmer-js-dThwC_hZ.js} +1 -1
- package/apps/web/dist/assets/{glimmer-ts-CvphZOog.js → glimmer-ts-cCThNW17.js} +1 -1
- package/apps/web/dist/assets/{glsl-Ddj2GHKO.js → glsl-BxmyIQYG.js} +1 -1
- package/apps/web/dist/assets/{graphql-CeGXMmoK.js → graphql-DygyczJj.js} +1 -1
- package/apps/web/dist/assets/{hack-CtBKZ5VW.js → hack-Bl1_oIjY.js} +1 -1
- package/apps/web/dist/assets/{haml-DVvVd-Mp.js → haml-CeOBocB0.js} +1 -1
- package/apps/web/dist/assets/{handlebars-Blx6Nluh.js → handlebars-Bw1KMTCv.js} +1 -1
- package/apps/web/dist/assets/{html-O-gZ5M1-.js → html-BaNo1IFo.js} +1 -1
- package/apps/web/dist/assets/{html-derivative-BsDe9c3P.js → html-derivative-f4LhlsLO.js} +1 -1
- package/apps/web/dist/assets/{http-CAUZeWmG.js → http-Be2FOF-U.js} +1 -1
- package/apps/web/dist/assets/{hurl-CYnRsJrb.js → hurl-BrtPQQhS.js} +1 -1
- package/apps/web/dist/assets/index-6ofGnhX4.css +2 -0
- package/apps/web/dist/assets/index-NWUHLKte.js +1203 -0
- package/apps/web/dist/assets/{java-DjjpMT1c.js → java-Bum5bcia.js} +1 -1
- package/apps/web/dist/assets/{javascript-BtiswSWS.js → javascript-C5oYhZKk.js} +1 -1
- package/apps/web/dist/assets/{jinja-ZQGJY58d.js → jinja-CNXoe8pn.js} +1 -1
- package/apps/web/dist/assets/{jison-VPkdeMFr.js → jison-g1hzxo4W.js} +1 -1
- package/apps/web/dist/assets/{json-D5BAJIHo.js → json-CiAAuc5j.js} +1 -1
- package/apps/web/dist/assets/{jsx-DMEKVcfT.js → jsx-Z0awY4ip.js} +1 -1
- package/apps/web/dist/assets/{julia-BmOLQFch.js → julia-DX3JjwcM.js} +1 -1
- package/apps/web/dist/assets/{just-YZGutsxo.js → just-DS6eCX0g.js} +1 -1
- package/apps/web/dist/assets/{latex-CvrF79xl.js → latex-CaBic_pJ.js} +1 -1
- package/apps/web/dist/assets/{liquid-B--WbaSh.js → liquid-DMaOBf3v.js} +1 -1
- package/apps/web/dist/assets/{lua-BNEqLlv-.js → lua-DeLz5pfA.js} +1 -1
- package/apps/web/dist/assets/{marko-jEzbzXvj.js → marko-DqlzpgnR.js} +1 -1
- package/apps/web/dist/assets/{mdc-yfiYVuXh.js → mdc-CRcSVpSn.js} +1 -1
- package/apps/web/dist/assets/{nginx-j7vnr3lB.js → nginx-BvszHPa-.js} +1 -1
- package/apps/web/dist/assets/{nim-CV2OP2RC.js → nim-B19-Hebo.js} +1 -1
- package/apps/web/dist/assets/{org-DTTS5Tv_.js → org-Dx7yMTgG.js} +1 -1
- package/apps/web/dist/assets/{perl-DuF7yMdF.js → perl-C-dISCJW.js} +1 -1
- package/apps/web/dist/assets/{php-8ufnwSAo.js → php-BHBxrwsl.js} +1 -1
- package/apps/web/dist/assets/{pug-D-_t9dIn.js → pug-4a3x4vRs.js} +1 -1
- package/apps/web/dist/assets/{qml-DFxD5uAC.js → qml-CGOhzFqi.js} +1 -1
- package/apps/web/dist/assets/{r-D8-Yi2KS.js → r-8jr0rtQL.js} +1 -1
- package/apps/web/dist/assets/{razor-DhBq7-pb.js → razor-BDrURPIA.js} +1 -1
- package/apps/web/dist/assets/{regexp-C20CUsFr.js → regexp-Cg-MfP18.js} +1 -1
- package/apps/web/dist/assets/{rst-DsIaukoD.js → rst-0OrP55QZ.js} +1 -1
- package/apps/web/dist/assets/{ruby-C__EgK1w.js → ruby-dAApWLhj.js} +1 -1
- package/apps/web/dist/assets/{sas-BdDDRZUF.js → sas-3_hKTjL9.js} +1 -1
- package/apps/web/dist/assets/{scss-nPFPh8nF.js → scss-D6ollEQv.js} +1 -1
- package/apps/web/dist/assets/{shellscript-Cfn0DgiQ.js → shellscript-V3H5Rwnn.js} +1 -1
- package/apps/web/dist/assets/{shellsession-DFGAcvZ4.js → shellsession-TxydKEoU.js} +1 -1
- package/apps/web/dist/assets/{soy-8DrmW46F.js → soy-t3rHc1tL.js} +1 -1
- package/apps/web/dist/assets/{sql-BeU26hky.js → sql-B-NkeM8R.js} +1 -1
- package/apps/web/dist/assets/{stata-OFfE-Kh9.js → stata-BW6neeVL.js} +1 -1
- package/apps/web/dist/assets/{surrealql-Bwt4eVgP.js → surrealql-CRBKdx6K.js} +1 -1
- package/apps/web/dist/assets/{svelte-DF7N9uBp.js → svelte-DTTmtyn-.js} +1 -1
- package/apps/web/dist/assets/{templ-CEirsWl9.js → templ-CCBYQttY.js} +1 -1
- package/apps/web/dist/assets/{tex-BvBhDkV4.js → tex-D9XxqR5N.js} +1 -1
- package/apps/web/dist/assets/{ts-tags-Dx8bn0ts.js → ts-tags-CI4pWHnF.js} +1 -1
- package/apps/web/dist/assets/{tsx-vXk5Q55O.js → tsx-DlfK3Vuf.js} +1 -1
- package/apps/web/dist/assets/{twig-CIgLDHIW.js → twig-D-C2s1f2.js} +1 -1
- package/apps/web/dist/assets/{typescript-CCW6p4Fc.js → typescript-BnqYTwqM.js} +1 -1
- package/apps/web/dist/assets/{typst-CQL-qDz3.js → typst-DdTyavvE.js} +1 -1
- package/apps/web/dist/assets/{vue-html-BHuiCeAo.js → vue-html-CIXNcZez.js} +1 -1
- package/apps/web/dist/assets/{vue-vine-DxeD8nHa.js → vue-vine-Czo_W6u3.js} +1 -1
- package/apps/web/dist/assets/{vue-gFAUYOUp.js → vue-zbXTTw4n.js} +1 -1
- package/apps/web/dist/assets/{xml-B43ZEHE5.js → xml-DWv3jkXo.js} +1 -1
- package/apps/web/dist/assets/{xsl-icvYLDXv.js → xsl-BiggF1J3.js} +1 -1
- package/apps/web/dist/assets/{yaml-Bj0dlmrV.js → yaml-D9oC3m1d.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/sw.js +1 -1
- package/package.json +4 -3
- package/apps/web/dist/assets/index-5vPpgW8A.css +0 -2
- package/apps/web/dist/assets/index-CGSSO_em.js +0 -1152
|
@@ -2,28 +2,28 @@ import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSyn
|
|
|
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";
|
|
6
5
|
import { WebSocketServer } from "ws";
|
|
7
6
|
import os from "node:os";
|
|
8
7
|
import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
9
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
9
|
import { execFile, spawn } from "node:child_process";
|
|
11
10
|
import readline from "node:readline";
|
|
11
|
+
import { z } from "zod";
|
|
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
16
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
17
|
+
import { sumBy } from "es-toolkit";
|
|
17
18
|
import { DatabaseSync } from "node:sqlite";
|
|
18
19
|
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
19
20
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
20
|
-
import webPush from "web-push";
|
|
21
21
|
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
22
22
|
import { lstat, open, readFile, readdir, readlink, stat } from "node:fs/promises";
|
|
23
23
|
import { promisify } from "node:util";
|
|
24
|
+
import webPush from "web-push";
|
|
24
25
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
25
26
|
//#region ../core/src/protocol.ts
|
|
26
|
-
const HOSTED_APP = "https://offsite.sayad.dev";
|
|
27
27
|
function encodeFrame(kind, id, data) {
|
|
28
28
|
const frame = new Uint8Array(5 + data.length);
|
|
29
29
|
frame[0] = kind;
|
|
@@ -40,6 +40,27 @@ function decodeFrame(frame) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/constants.ts
|
|
44
|
+
const deprecatedModels = {
|
|
45
|
+
claude: [
|
|
46
|
+
"claude-sonnet-5",
|
|
47
|
+
"claude-opus-5",
|
|
48
|
+
"claude-fable-5",
|
|
49
|
+
"claude-haiku-4-5-20251001",
|
|
50
|
+
"claude-opus-4-8",
|
|
51
|
+
"claude-opus-4-7",
|
|
52
|
+
"claude-opus-4-6",
|
|
53
|
+
"claude-sonnet-4-6"
|
|
54
|
+
],
|
|
55
|
+
codex: [],
|
|
56
|
+
opencode: []
|
|
57
|
+
};
|
|
58
|
+
const defaultModels = {
|
|
59
|
+
claude: "claude-opus-5-5",
|
|
60
|
+
codex: null,
|
|
61
|
+
opencode: null
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
43
64
|
//#region src/lib/hub.ts
|
|
44
65
|
const clients = /* @__PURE__ */ new Set();
|
|
45
66
|
function publish$2(sessionId, event) {
|
|
@@ -49,281 +70,6 @@ function broadcast(event) {
|
|
|
49
70
|
for (const client of clients) client.send(event);
|
|
50
71
|
}
|
|
51
72
|
//#endregion
|
|
52
|
-
//#region src/args.ts
|
|
53
|
-
let command = "web";
|
|
54
|
-
let name = null;
|
|
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");
|
|
56
|
-
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
57
|
-
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
58
|
-
command = "serve";
|
|
59
|
-
});
|
|
60
|
-
program.command("pair").description("Creates a pairing token for another browser and prints its links").option("--name <name>", "Name to recognize this browser by").action((options) => {
|
|
61
|
-
command = "pair";
|
|
62
|
-
name = options.name ?? null;
|
|
63
|
-
});
|
|
64
|
-
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
65
|
-
command = "unpair";
|
|
66
|
-
});
|
|
67
|
-
program.parse();
|
|
68
|
-
const options = program.opts();
|
|
69
|
-
const args = {
|
|
70
|
-
...options,
|
|
71
|
-
dir: path.resolve(options.dir),
|
|
72
|
-
command,
|
|
73
|
-
name
|
|
74
|
-
};
|
|
75
|
-
const serverDir = path.join(import.meta.dirname, "..");
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/storage/storage.ts
|
|
78
|
-
const home = args.dir;
|
|
79
|
-
const lockFile = path.join(home, "server.pid");
|
|
80
|
-
mkdirSync(home, { recursive: true });
|
|
81
|
-
function lockHome() {
|
|
82
|
-
lock();
|
|
83
|
-
process.on("exit", () => unlinkSync(lockFile));
|
|
84
|
-
}
|
|
85
|
-
function lock() {
|
|
86
|
-
try {
|
|
87
|
-
writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
|
|
88
|
-
return;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (!hasCode(error, "EEXIST")) throw error;
|
|
91
|
-
}
|
|
92
|
-
let content;
|
|
93
|
-
let modified;
|
|
94
|
-
try {
|
|
95
|
-
content = readFileSync(lockFile, "utf8");
|
|
96
|
-
modified = statSync(lockFile).mtimeMs;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (hasCode(error, "ENOENT")) return lock();
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
const pid = Number(content);
|
|
102
|
-
const validPid = Number.isInteger(pid) && pid > 0;
|
|
103
|
-
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
104
|
-
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
105
|
-
consola.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
unlinkSync(lockFile);
|
|
109
|
-
lock();
|
|
110
|
-
}
|
|
111
|
-
function isRunning(pid) {
|
|
112
|
-
try {
|
|
113
|
-
process.kill(pid, 0);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (hasCode(error, "ESRCH")) return false;
|
|
117
|
-
if (hasCode(error, "EPERM")) return true;
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function hasCode(error, code) {
|
|
122
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
123
|
-
}
|
|
124
|
-
function readJson(name) {
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(readFileSync(path.join(home, name), "utf8"));
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error.code === "ENOENT") return null;
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function writeJson(name, value) {
|
|
133
|
-
const file = path.join(home, name);
|
|
134
|
-
mkdirSync(home, { recursive: true });
|
|
135
|
-
writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
136
|
-
renameSync(`${file}.tmp`, file);
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/storage/config.ts
|
|
140
|
-
const version = "0.0.0";
|
|
141
|
-
const configFile = path.join(home, "config.json");
|
|
142
|
-
const defaults = {
|
|
143
|
-
claudeBaseDir: path.join(os.homedir(), ".claude"),
|
|
144
|
-
composer: {
|
|
145
|
-
provider: "claude",
|
|
146
|
-
claude: {
|
|
147
|
-
accountId: null,
|
|
148
|
-
model: "haiku",
|
|
149
|
-
effort: "low",
|
|
150
|
-
mode: "bypassPermissions",
|
|
151
|
-
plan: false
|
|
152
|
-
},
|
|
153
|
-
codex: {
|
|
154
|
-
accountId: null,
|
|
155
|
-
model: null,
|
|
156
|
-
effort: null,
|
|
157
|
-
mode: "auto",
|
|
158
|
-
plan: false
|
|
159
|
-
},
|
|
160
|
-
opencode: {
|
|
161
|
-
accountId: "opencode",
|
|
162
|
-
model: null,
|
|
163
|
-
effort: null,
|
|
164
|
-
mode: null,
|
|
165
|
-
plan: false
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
commitModels: {
|
|
169
|
-
claude: "haiku",
|
|
170
|
-
codex: "gpt-6-luna",
|
|
171
|
-
opencode: null
|
|
172
|
-
},
|
|
173
|
-
terminal: {
|
|
174
|
-
shell: {
|
|
175
|
-
macos: null,
|
|
176
|
-
linux: null,
|
|
177
|
-
windows: null
|
|
178
|
-
},
|
|
179
|
-
behavior: {
|
|
180
|
-
terminalIdentity: "offsite",
|
|
181
|
-
shellIntegration: true
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const choices = {
|
|
186
|
-
"composer.provider": [
|
|
187
|
-
"claude",
|
|
188
|
-
"codex",
|
|
189
|
-
"opencode"
|
|
190
|
-
],
|
|
191
|
-
"composer.claude.effort": [
|
|
192
|
-
"low",
|
|
193
|
-
"medium",
|
|
194
|
-
"high",
|
|
195
|
-
"xhigh",
|
|
196
|
-
"max"
|
|
197
|
-
],
|
|
198
|
-
"composer.claude.mode": [
|
|
199
|
-
"default",
|
|
200
|
-
"acceptEdits",
|
|
201
|
-
"bypassPermissions",
|
|
202
|
-
"plan",
|
|
203
|
-
"dontAsk",
|
|
204
|
-
"auto"
|
|
205
|
-
],
|
|
206
|
-
"composer.codex.mode": [
|
|
207
|
-
"read-only",
|
|
208
|
-
"auto",
|
|
209
|
-
"full-access"
|
|
210
|
-
],
|
|
211
|
-
"terminal.behavior.terminalIdentity": [
|
|
212
|
-
"offsite",
|
|
213
|
-
"ghostty",
|
|
214
|
-
"iterm2"
|
|
215
|
-
]
|
|
216
|
-
};
|
|
217
|
-
const nullable = [
|
|
218
|
-
"composer.claude.accountId",
|
|
219
|
-
"composer.claude.model",
|
|
220
|
-
"composer.claude.effort",
|
|
221
|
-
"composer.claude.mode",
|
|
222
|
-
"composer.codex.accountId",
|
|
223
|
-
"composer.codex.model",
|
|
224
|
-
"composer.codex.effort",
|
|
225
|
-
"composer.codex.mode",
|
|
226
|
-
"composer.opencode.accountId",
|
|
227
|
-
"composer.opencode.model",
|
|
228
|
-
"composer.opencode.effort",
|
|
229
|
-
"composer.opencode.mode",
|
|
230
|
-
"commitModels.claude",
|
|
231
|
-
"commitModels.codex",
|
|
232
|
-
"commitModels.opencode",
|
|
233
|
-
"terminal.shell.macos",
|
|
234
|
-
"terminal.shell.linux",
|
|
235
|
-
"terminal.shell.windows"
|
|
236
|
-
];
|
|
237
|
-
function platform() {
|
|
238
|
-
if (process.platform === "darwin") return "macos";
|
|
239
|
-
if (process.platform === "linux") return "linux";
|
|
240
|
-
if (process.platform === "win32") return "windows";
|
|
241
|
-
throw new Error(`unsupported platform ${process.platform}`);
|
|
242
|
-
}
|
|
243
|
-
function defaultShell() {
|
|
244
|
-
if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
|
|
245
|
-
return os.userInfo().shell ?? "/bin/sh";
|
|
246
|
-
}
|
|
247
|
-
function configInfo() {
|
|
248
|
-
return {
|
|
249
|
-
config: readConfig(),
|
|
250
|
-
defaults,
|
|
251
|
-
platform: platform(),
|
|
252
|
-
version,
|
|
253
|
-
defaultShell: defaultShell()
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function readConfig() {
|
|
257
|
-
let user;
|
|
258
|
-
try {
|
|
259
|
-
user = readJson("config.json");
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.warn(`invalid ${configFile}, using the default config: ${error}`);
|
|
262
|
-
return structuredClone(defaults);
|
|
263
|
-
}
|
|
264
|
-
if (user === null) return structuredClone(defaults);
|
|
265
|
-
if (!isObject(user)) {
|
|
266
|
-
console.warn(`${configFile} is not an object, using the default config`);
|
|
267
|
-
return structuredClone(defaults);
|
|
268
|
-
}
|
|
269
|
-
let config = structuredClone(defaults);
|
|
270
|
-
for (const [key, value] of Object.entries(user)) config = merge(config, [key], value);
|
|
271
|
-
return config;
|
|
272
|
-
}
|
|
273
|
-
function saveConfig(value) {
|
|
274
|
-
const error = invalid(value, defaults, []);
|
|
275
|
-
if (error !== null) throw new Error(error);
|
|
276
|
-
writeJson("config.json", value);
|
|
277
|
-
process.env.CLAUDE_CONFIG_DIR = value.claudeBaseDir;
|
|
278
|
-
broadcast({
|
|
279
|
-
type: "config",
|
|
280
|
-
config: value
|
|
281
|
-
});
|
|
282
|
-
return value;
|
|
283
|
-
}
|
|
284
|
-
function merge(config, keys, user) {
|
|
285
|
-
if (isObject(keys.reduce((value, key) => isObject(value) ? value[key] : void 0, config)) && isObject(user)) {
|
|
286
|
-
let next = config;
|
|
287
|
-
for (const [key, value] of Object.entries(user)) next = merge(next, [...keys, key], value);
|
|
288
|
-
return next;
|
|
289
|
-
}
|
|
290
|
-
const candidate = structuredClone(config);
|
|
291
|
-
const parent = keys.slice(0, -1).reduce((value, key) => value[key], candidate);
|
|
292
|
-
parent[keys[keys.length - 1]] = user;
|
|
293
|
-
const error = invalid(candidate, defaults, []);
|
|
294
|
-
if (error === null) return candidate;
|
|
295
|
-
console.warn(`invalid ${keys.join(".")} in ${configFile}, using its default: ${error}`);
|
|
296
|
-
return config;
|
|
297
|
-
}
|
|
298
|
-
function invalid(value, template, keys) {
|
|
299
|
-
const name = keys.join(".");
|
|
300
|
-
if (isObject(template)) {
|
|
301
|
-
if (!isObject(value)) return `${name || "config"} must be an object`;
|
|
302
|
-
for (const key of Object.keys(value)) if (!(key in template)) return `unknown setting ${[...keys, key].join(".")}`;
|
|
303
|
-
for (const key of Object.keys(template)) {
|
|
304
|
-
if (!(key in value)) return `missing setting ${[...keys, key].join(".")}`;
|
|
305
|
-
const error = invalid(value[key], template[key], [...keys, key]);
|
|
306
|
-
if (error !== null) return error;
|
|
307
|
-
}
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
if (nullable.includes(name) && value === null) return null;
|
|
311
|
-
if (name === "claudeBaseDir") return typeof value === "string" && path.isAbsolute(value) ? null : "claudeBaseDir must be an absolute path";
|
|
312
|
-
if (name in choices) {
|
|
313
|
-
if (choices[name].includes(value)) return null;
|
|
314
|
-
return `${name} must be one of ${choices[name].join(", ")}`;
|
|
315
|
-
}
|
|
316
|
-
if (template === null) {
|
|
317
|
-
if (typeof value === "string") return null;
|
|
318
|
-
return `${name} must be text or null`;
|
|
319
|
-
}
|
|
320
|
-
if (typeof value !== typeof template) return `${name} must be a ${typeof template}`;
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
function isObject(value) {
|
|
324
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
325
|
-
}
|
|
326
|
-
//#endregion
|
|
327
73
|
//#region src/lib/codex/app-server.ts
|
|
328
74
|
const IDLE_LIMIT$3 = 18e5;
|
|
329
75
|
const START_TIMEOUT = 15e3;
|
|
@@ -340,6 +86,9 @@ function appServer(home) {
|
|
|
340
86
|
function stopAppServers() {
|
|
341
87
|
for (const server of servers.values()) server.stop();
|
|
342
88
|
}
|
|
89
|
+
function stopAppServer(home) {
|
|
90
|
+
servers.get(home)?.stop();
|
|
91
|
+
}
|
|
343
92
|
function findCodex() {
|
|
344
93
|
const name = process.platform === "win32" ? "codex.exe" : "codex";
|
|
345
94
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
@@ -379,7 +128,7 @@ var AppServer = class {
|
|
|
379
128
|
clientInfo: {
|
|
380
129
|
name: "offsite",
|
|
381
130
|
title: "Offsite",
|
|
382
|
-
version
|
|
131
|
+
version: "0.0.0"
|
|
383
132
|
},
|
|
384
133
|
capabilities: {
|
|
385
134
|
experimentalApi: true,
|
|
@@ -616,82 +365,210 @@ function isMirror(dir) {
|
|
|
616
365
|
}
|
|
617
366
|
}
|
|
618
367
|
//#endregion
|
|
619
|
-
//#region src/lib/
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
368
|
+
//#region src/lib/claude/content.ts
|
|
369
|
+
const imageTypes = [
|
|
370
|
+
"image/png",
|
|
371
|
+
"image/jpeg",
|
|
372
|
+
"image/gif",
|
|
373
|
+
"image/webp"
|
|
374
|
+
];
|
|
375
|
+
function toMessage(type, item, time) {
|
|
376
|
+
return {
|
|
377
|
+
type,
|
|
378
|
+
uuid: item.uuid,
|
|
379
|
+
time,
|
|
380
|
+
content: isRecord(item.message) ? toBlocks(item.message.content, answersOf(item.tool_use_result)) : [],
|
|
381
|
+
parent: item.parent_tool_use_id
|
|
382
|
+
};
|
|
632
383
|
}
|
|
633
|
-
function
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
384
|
+
function toUserInput(prompt, uuid, priority) {
|
|
385
|
+
for (const image of prompt.images) if (!imageTypes.includes(image.mediaType)) throw new Error(`${image.mediaType} images are not supported`);
|
|
386
|
+
return {
|
|
387
|
+
type: "user",
|
|
388
|
+
message: {
|
|
389
|
+
role: "user",
|
|
390
|
+
content: [...prompt.images.map((image) => ({
|
|
391
|
+
type: "image",
|
|
392
|
+
source: {
|
|
393
|
+
type: "base64",
|
|
394
|
+
media_type: image.mediaType,
|
|
395
|
+
data: image.data
|
|
396
|
+
}
|
|
397
|
+
})), ...prompt.text === "" ? [] : [{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: prompt.text
|
|
400
|
+
}]]
|
|
401
|
+
},
|
|
402
|
+
parent_tool_use_id: null,
|
|
403
|
+
uuid,
|
|
404
|
+
...priority === void 0 ? {} : { priority }
|
|
405
|
+
};
|
|
637
406
|
}
|
|
638
|
-
function
|
|
407
|
+
function promptMessage(prompt, uuid) {
|
|
639
408
|
return {
|
|
640
|
-
|
|
641
|
-
|
|
409
|
+
type: "user",
|
|
410
|
+
uuid,
|
|
642
411
|
time: Date.now(),
|
|
643
|
-
|
|
644
|
-
|
|
412
|
+
content: [...prompt.images.map((image) => ({
|
|
413
|
+
type: "image",
|
|
414
|
+
...image
|
|
415
|
+
})), ...prompt.text === "" ? [] : [{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: prompt.text
|
|
418
|
+
}]],
|
|
419
|
+
parent: null
|
|
645
420
|
};
|
|
646
421
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
time
|
|
422
|
+
function notice(text) {
|
|
423
|
+
return {
|
|
424
|
+
type: "notice",
|
|
425
|
+
uuid: randomUUID(),
|
|
426
|
+
time: Date.now(),
|
|
427
|
+
text,
|
|
428
|
+
isError: true
|
|
652
429
|
};
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
430
|
+
}
|
|
431
|
+
function contextTokens(message) {
|
|
432
|
+
if (!isRecord(message) || !isRecord(message.usage)) return null;
|
|
433
|
+
const usage = message.usage;
|
|
434
|
+
return [
|
|
435
|
+
usage.input_tokens,
|
|
436
|
+
usage.cache_creation_input_tokens,
|
|
437
|
+
usage.cache_read_input_tokens,
|
|
438
|
+
usage.output_tokens
|
|
439
|
+
].reduce((total, value) => total + (typeof value === "number" ? value : 0), 0);
|
|
440
|
+
}
|
|
441
|
+
function answersOf(output) {
|
|
442
|
+
if (!isRecord(output) || !isRecord(output.answers)) return null;
|
|
443
|
+
const answers = {};
|
|
444
|
+
for (const [question, answer] of Object.entries(output.answers)) {
|
|
445
|
+
if (typeof answer !== "string") return null;
|
|
446
|
+
answers[question] = answer;
|
|
447
|
+
}
|
|
448
|
+
return answers;
|
|
449
|
+
}
|
|
450
|
+
function toBlocks(content, answers) {
|
|
451
|
+
if (typeof content === "string") return [{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: content
|
|
454
|
+
}];
|
|
455
|
+
if (!Array.isArray(content)) return [];
|
|
456
|
+
return content.flatMap((block) => {
|
|
457
|
+
if (!isRecord(block)) return [];
|
|
458
|
+
switch (block.type) {
|
|
459
|
+
case "text": return typeof block.text === "string" && !block.text.startsWith("<system-reminder>") && !block.text.startsWith("<local-command-caveat>") ? [{
|
|
460
|
+
type: "text",
|
|
461
|
+
text: block.text
|
|
462
|
+
}] : [];
|
|
463
|
+
case "thinking": return typeof block.thinking === "string" && block.thinking !== "" ? [{
|
|
464
|
+
type: "thinking",
|
|
465
|
+
thinking: block.thinking
|
|
466
|
+
}] : [];
|
|
467
|
+
case "image": {
|
|
468
|
+
const source = block.source;
|
|
469
|
+
return isRecord(source) && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string" ? [{
|
|
470
|
+
type: "image",
|
|
471
|
+
mediaType: source.media_type,
|
|
472
|
+
data: source.data
|
|
473
|
+
}] : [];
|
|
474
|
+
}
|
|
475
|
+
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && isRecord(block.input) ? [{
|
|
476
|
+
type: "tool_use",
|
|
477
|
+
id: block.id,
|
|
478
|
+
name: block.name,
|
|
479
|
+
input: block.input
|
|
480
|
+
}] : [];
|
|
481
|
+
case "tool_result": return typeof block.tool_use_id === "string" ? [{
|
|
482
|
+
type: "tool_result",
|
|
483
|
+
toolUseId: block.tool_use_id,
|
|
484
|
+
content: toBlocks(block.content, null).filter((item) => item.type === "text"),
|
|
485
|
+
isError: block.is_error === true,
|
|
486
|
+
answers
|
|
487
|
+
}] : [];
|
|
488
|
+
default: return [];
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function isRecord(value) {
|
|
493
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/lib/codex/items.ts
|
|
497
|
+
const statuses$1 = {
|
|
498
|
+
inProgress: "running",
|
|
499
|
+
completed: "completed",
|
|
500
|
+
failed: "failed",
|
|
501
|
+
declined: "stopped",
|
|
502
|
+
interrupted: "stopped"
|
|
503
|
+
};
|
|
504
|
+
function turnItems(turns, parent) {
|
|
505
|
+
return turns.flatMap((turn) => [...turn.items.flatMap((item) => toItems$1(turn.id, item, parent, turn.startedAt === null ? null : turn.startedAt * 1e3)), ...turn.status === "failed" && turn.error !== null ? [errorItem(`${turn.id}:error`, turn.error.message)] : []]);
|
|
506
|
+
}
|
|
507
|
+
function itemId(turnId, id) {
|
|
508
|
+
return `${turnId}:${id}`;
|
|
509
|
+
}
|
|
510
|
+
function turnOf(itemId) {
|
|
511
|
+
const index = itemId.indexOf(":");
|
|
512
|
+
if (index < 0) throw new Error(`${itemId} is not a Codex item`);
|
|
513
|
+
return itemId.slice(0, index);
|
|
514
|
+
}
|
|
515
|
+
function errorItem(id, text) {
|
|
516
|
+
return {
|
|
517
|
+
id,
|
|
518
|
+
parent: null,
|
|
519
|
+
time: Date.now(),
|
|
520
|
+
kind: "error",
|
|
521
|
+
text
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function toItems$1(turnId, item, parent, time) {
|
|
525
|
+
const base = {
|
|
526
|
+
id: itemId(turnId, item.id),
|
|
527
|
+
parent,
|
|
528
|
+
time
|
|
529
|
+
};
|
|
530
|
+
switch (item.type) {
|
|
531
|
+
case "userMessage": return [{
|
|
532
|
+
...base,
|
|
533
|
+
kind: "user",
|
|
534
|
+
text: item.content.flatMap(userText$1).join("\n").trim(),
|
|
535
|
+
images: item.content.flatMap(userImages),
|
|
536
|
+
format: "text"
|
|
537
|
+
}];
|
|
538
|
+
case "agentMessage": return item.text.trim() === "" ? [] : [{
|
|
539
|
+
...base,
|
|
540
|
+
kind: "assistant",
|
|
541
|
+
text: item.text,
|
|
542
|
+
message: base.id
|
|
543
|
+
}];
|
|
544
|
+
case "reasoning": {
|
|
545
|
+
const text = [...item.summary, ...item.content].join("\n\n").trim();
|
|
546
|
+
return text === "" ? [] : [{
|
|
547
|
+
...base,
|
|
548
|
+
kind: "reasoning",
|
|
549
|
+
text
|
|
550
|
+
}];
|
|
551
|
+
}
|
|
552
|
+
case "plan": return [{
|
|
553
|
+
...base,
|
|
554
|
+
kind: "plan",
|
|
555
|
+
plan: item.text
|
|
556
|
+
}];
|
|
557
|
+
case "commandExecution": return [{
|
|
558
|
+
...base,
|
|
559
|
+
kind: "command",
|
|
560
|
+
command: item.command,
|
|
561
|
+
description: "",
|
|
562
|
+
output: item.aggregatedOutput ?? "",
|
|
563
|
+
status: statuses$1[item.status],
|
|
564
|
+
background: null
|
|
565
|
+
}];
|
|
566
|
+
case "fileChange": return item.changes.map((change, index) => ({
|
|
567
|
+
...base,
|
|
568
|
+
id: `${base.id}:${index}`,
|
|
569
|
+
kind: "file_change",
|
|
570
|
+
path: change.path,
|
|
571
|
+
diffs: change.kind.type === "add" ? [{
|
|
695
572
|
before: "",
|
|
696
573
|
after: change.diff
|
|
697
574
|
}] : change.kind.type === "delete" ? [{
|
|
@@ -812,48 +689,291 @@ function hunks(diff) {
|
|
|
812
689
|
return result;
|
|
813
690
|
}
|
|
814
691
|
function contentText$1(content) {
|
|
815
|
-
return content.map((block) =>
|
|
692
|
+
return content.map((block) => isRecord(block) && typeof block.text === "string" ? block.text : JSON.stringify(block)).join("\n");
|
|
816
693
|
}
|
|
817
694
|
function record$1(value) {
|
|
818
|
-
|
|
819
|
-
return { value };
|
|
695
|
+
return isRecord(value) ? value : { value };
|
|
820
696
|
}
|
|
821
697
|
//#endregion
|
|
822
|
-
//#region src/
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
698
|
+
//#region src/args.ts
|
|
699
|
+
let command = "web";
|
|
700
|
+
let name = null;
|
|
701
|
+
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");
|
|
702
|
+
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
703
|
+
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
704
|
+
command = "serve";
|
|
705
|
+
});
|
|
706
|
+
program.command("pair").description("Creates a pairing token for another browser and prints its links").option("--name <name>", "Name to recognize this browser by").action((options) => {
|
|
707
|
+
command = "pair";
|
|
708
|
+
name = options.name ?? null;
|
|
709
|
+
});
|
|
710
|
+
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
711
|
+
command = "unpair";
|
|
712
|
+
});
|
|
713
|
+
program.parse();
|
|
714
|
+
const options = program.opts();
|
|
715
|
+
const args = {
|
|
716
|
+
...options,
|
|
717
|
+
dir: path.resolve(options.dir),
|
|
718
|
+
command,
|
|
719
|
+
name
|
|
720
|
+
};
|
|
721
|
+
const serverDir = path.join(import.meta.dirname, "..");
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/storage/storage.ts
|
|
724
|
+
const home = args.dir;
|
|
725
|
+
const lockFile = path.join(home, "server.pid");
|
|
726
|
+
mkdirSync(home, { recursive: true });
|
|
727
|
+
function lockHome() {
|
|
728
|
+
lock();
|
|
729
|
+
process.on("exit", () => unlinkSync(lockFile));
|
|
833
730
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
})
|
|
731
|
+
function lock() {
|
|
732
|
+
try {
|
|
733
|
+
writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
|
|
734
|
+
return;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
737
|
+
}
|
|
738
|
+
let content;
|
|
739
|
+
let modified;
|
|
740
|
+
try {
|
|
741
|
+
content = readFileSync(lockFile, "utf8");
|
|
742
|
+
modified = statSync(lockFile).mtimeMs;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (hasCode(error, "ENOENT")) return lock();
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
const pid = Number(content);
|
|
748
|
+
const validPid = Number.isInteger(pid) && pid > 0;
|
|
749
|
+
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
750
|
+
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
751
|
+
consola.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
unlinkSync(lockFile);
|
|
755
|
+
lock();
|
|
756
|
+
}
|
|
757
|
+
function isRunning(pid) {
|
|
758
|
+
try {
|
|
759
|
+
process.kill(pid, 0);
|
|
760
|
+
return true;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (hasCode(error, "ESRCH")) return false;
|
|
763
|
+
if (hasCode(error, "EPERM")) return true;
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function hasCode(error, code) {
|
|
768
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
769
|
+
}
|
|
770
|
+
function readJson(name) {
|
|
771
|
+
try {
|
|
772
|
+
return JSON.parse(readFileSync(path.join(home, name), "utf8"));
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (error.code === "ENOENT") return null;
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function writeJson(name, value) {
|
|
779
|
+
const file = path.join(home, name);
|
|
780
|
+
mkdirSync(home, { recursive: true });
|
|
781
|
+
writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
782
|
+
renameSync(`${file}.tmp`, file);
|
|
839
783
|
}
|
|
840
784
|
//#endregion
|
|
841
|
-
//#region src/
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
785
|
+
//#region src/storage/config.ts
|
|
786
|
+
const configFile = path.join(home, "config.json");
|
|
787
|
+
const modelSetting = z.strictObject({
|
|
788
|
+
hidden: z.boolean(),
|
|
789
|
+
effort: z.string().nullable()
|
|
790
|
+
});
|
|
791
|
+
const configSchema = z.strictObject({
|
|
792
|
+
claudeBaseDir: z.string().refine((value) => path.isAbsolute(value), "must be an absolute path").default(path.join(os.homedir(), ".claude")),
|
|
793
|
+
enabledProviders: z.strictObject({
|
|
794
|
+
claude: z.boolean().default(true),
|
|
795
|
+
codex: z.boolean().default(true),
|
|
796
|
+
opencode: z.boolean().default(true)
|
|
797
|
+
}).prefault({}),
|
|
798
|
+
composer: z.strictObject({
|
|
799
|
+
provider: z.enum([
|
|
800
|
+
"claude",
|
|
801
|
+
"codex",
|
|
802
|
+
"opencode"
|
|
803
|
+
]).default("claude"),
|
|
804
|
+
claude: z.strictObject({
|
|
805
|
+
accountId: z.string().nullable().default(null),
|
|
806
|
+
model: z.string().nullable().default(null),
|
|
807
|
+
effort: z.enum([
|
|
808
|
+
"low",
|
|
809
|
+
"medium",
|
|
810
|
+
"high",
|
|
811
|
+
"xhigh",
|
|
812
|
+
"max"
|
|
813
|
+
]).nullable().default(null),
|
|
814
|
+
mode: z.enum([
|
|
815
|
+
"default",
|
|
816
|
+
"acceptEdits",
|
|
817
|
+
"bypassPermissions",
|
|
818
|
+
"plan",
|
|
819
|
+
"dontAsk",
|
|
820
|
+
"auto"
|
|
821
|
+
]).nullable().default("bypassPermissions"),
|
|
822
|
+
plan: z.boolean().default(false)
|
|
823
|
+
}).prefault({}),
|
|
824
|
+
codex: z.strictObject({
|
|
825
|
+
accountId: z.string().nullable().default(null),
|
|
826
|
+
model: z.string().nullable().default(null),
|
|
827
|
+
effort: z.string().nullable().default(null),
|
|
828
|
+
mode: z.enum([
|
|
829
|
+
"read-only",
|
|
830
|
+
"auto",
|
|
831
|
+
"full-access"
|
|
832
|
+
]).nullable().default("auto"),
|
|
833
|
+
plan: z.boolean().default(false)
|
|
834
|
+
}).prefault({}),
|
|
835
|
+
opencode: z.strictObject({
|
|
836
|
+
accountId: z.null().default(null),
|
|
837
|
+
model: z.string().nullable().default(null),
|
|
838
|
+
effort: z.string().nullable().default(null),
|
|
839
|
+
mode: z.string().nullable().default(null),
|
|
840
|
+
plan: z.boolean().default(false)
|
|
841
|
+
}).prefault({})
|
|
842
|
+
}).prefault({}),
|
|
843
|
+
commitMessage: z.strictObject({
|
|
844
|
+
provider: z.enum([
|
|
845
|
+
"claude",
|
|
846
|
+
"codex",
|
|
847
|
+
"opencode"
|
|
848
|
+
]).nullable().default(null),
|
|
849
|
+
accountId: z.string().nullable().default(null),
|
|
850
|
+
model: z.string().nullable().default(null)
|
|
851
|
+
}).prefault({}),
|
|
852
|
+
models: z.strictObject({
|
|
853
|
+
claude: z.record(z.string(), modelSetting).default({}),
|
|
854
|
+
codex: z.record(z.string(), modelSetting).default({}),
|
|
855
|
+
opencode: z.record(z.string(), modelSetting).default({})
|
|
856
|
+
}).prefault({}),
|
|
857
|
+
delivery: z.enum(["queue", "steer"]).default("steer"),
|
|
858
|
+
autoResolve: z.strictObject({
|
|
859
|
+
questions: z.number().positive().nullable().default(3),
|
|
860
|
+
permissions: z.number().positive().nullable().default(1),
|
|
861
|
+
permissionDecision: z.enum(["allow", "deny"]).default("allow")
|
|
862
|
+
}).prefault({}),
|
|
863
|
+
terminal: z.strictObject({
|
|
864
|
+
shell: z.strictObject({
|
|
865
|
+
macos: z.string().nullable().default(null),
|
|
866
|
+
linux: z.string().nullable().default(null),
|
|
867
|
+
windows: z.string().nullable().default(null)
|
|
868
|
+
}).prefault({}),
|
|
869
|
+
behavior: z.strictObject({
|
|
870
|
+
terminalIdentity: z.enum([
|
|
871
|
+
"offsite",
|
|
872
|
+
"ghostty",
|
|
873
|
+
"iterm2"
|
|
874
|
+
]).default("offsite"),
|
|
875
|
+
shellIntegration: z.boolean().default(true)
|
|
876
|
+
}).prefault({})
|
|
877
|
+
}).prefault({})
|
|
878
|
+
});
|
|
879
|
+
const defaults = configSchema.parse({});
|
|
880
|
+
function platform() {
|
|
881
|
+
if (process.platform === "darwin") return "macos";
|
|
882
|
+
if (process.platform === "linux") return "linux";
|
|
883
|
+
if (process.platform === "win32") return "windows";
|
|
884
|
+
throw new Error(`unsupported platform ${process.platform}`);
|
|
885
|
+
}
|
|
886
|
+
function defaultShell() {
|
|
887
|
+
if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
|
|
888
|
+
return os.userInfo().shell ?? "/bin/sh";
|
|
889
|
+
}
|
|
890
|
+
function configInfo() {
|
|
891
|
+
return {
|
|
892
|
+
config: readConfig(),
|
|
893
|
+
defaults,
|
|
894
|
+
platform: platform(),
|
|
895
|
+
defaultShell: defaultShell()
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function readConfig() {
|
|
899
|
+
let user;
|
|
900
|
+
try {
|
|
901
|
+
user = readJson("config.json");
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.warn(`invalid ${configFile}, using the default config: ${error}`);
|
|
904
|
+
return structuredClone(defaults);
|
|
905
|
+
}
|
|
906
|
+
if (user === null) return structuredClone(defaults);
|
|
907
|
+
const result = configSchema.safeParse(user);
|
|
908
|
+
if (!result.success) {
|
|
909
|
+
console.warn(`invalid ${configFile}, using the default config:\n${z.prettifyError(result.error)}`);
|
|
910
|
+
return structuredClone(defaults);
|
|
911
|
+
}
|
|
912
|
+
return result.data;
|
|
913
|
+
}
|
|
914
|
+
function saveConfig(value) {
|
|
915
|
+
const result = configSchema.safeParse(value);
|
|
916
|
+
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
917
|
+
writeJson("config.json", result.data);
|
|
918
|
+
process.env.CLAUDE_CONFIG_DIR = result.data.claudeBaseDir;
|
|
919
|
+
broadcast({
|
|
920
|
+
type: "config",
|
|
921
|
+
config: result.data
|
|
922
|
+
});
|
|
923
|
+
return result.data;
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/lib/live.ts
|
|
927
|
+
const runs = /* @__PURE__ */ new Map();
|
|
928
|
+
function activity() {
|
|
929
|
+
const result = {};
|
|
930
|
+
for (const run of runs.values()) result[run.sessionId] = {
|
|
931
|
+
running: run.running,
|
|
932
|
+
waiting: run.pending.size > 0,
|
|
933
|
+
prompted: run.prompted,
|
|
934
|
+
tasks: run.tasks
|
|
935
|
+
};
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
function broadcastActivity() {
|
|
939
|
+
broadcast({
|
|
940
|
+
type: "activity",
|
|
941
|
+
activity: activity()
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function scheduleAutoResolve(sessionId, request) {
|
|
945
|
+
if (request.kind === "plan") return;
|
|
946
|
+
const settings = readConfig().autoResolve;
|
|
947
|
+
const minutes = request.kind === "question" ? settings.questions : settings.permissions;
|
|
948
|
+
if (minutes === null) return;
|
|
949
|
+
setTimeout(async () => {
|
|
950
|
+
const run = runs.get(sessionId);
|
|
951
|
+
if (run === void 0) return;
|
|
952
|
+
try {
|
|
953
|
+
if (request.kind === "question") await run.respond(request.id, "allow", {});
|
|
954
|
+
else await run.respond(request.id, readConfig().autoResolve.permissionDecision, null);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.warn(`could not auto-resolve ${request.id}: ${error}`);
|
|
957
|
+
}
|
|
958
|
+
}, minutes * 6e4);
|
|
959
|
+
}
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/lib/codex/requests.ts
|
|
962
|
+
function toRequest$1(requestId, method, params, items) {
|
|
963
|
+
const turnId = params.turnId;
|
|
964
|
+
switch (method) {
|
|
965
|
+
case "item/commandExecution/requestApproval": return {
|
|
966
|
+
id: requestId,
|
|
967
|
+
kind: "approval",
|
|
968
|
+
title: params.reason ?? "Run this command?",
|
|
969
|
+
canAlwaysAllow: true,
|
|
970
|
+
item: items.get(itemId(turnId, params.itemId)) ?? {
|
|
971
|
+
id: itemId(turnId, params.itemId),
|
|
972
|
+
parent: null,
|
|
973
|
+
time: Date.now(),
|
|
974
|
+
kind: "command",
|
|
975
|
+
command: params.command ?? "",
|
|
976
|
+
description: "",
|
|
857
977
|
output: "",
|
|
858
978
|
status: "running",
|
|
859
979
|
background: null
|
|
@@ -1182,6 +1302,7 @@ var CodexRun = class CodexRun {
|
|
|
1182
1302
|
request
|
|
1183
1303
|
});
|
|
1184
1304
|
broadcastActivity();
|
|
1305
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
1185
1306
|
}
|
|
1186
1307
|
async startTurn(prompt) {
|
|
1187
1308
|
clearTimeout(this.idleTimer);
|
|
@@ -1439,989 +1560,461 @@ async function readThread(threadId) {
|
|
|
1439
1560
|
})).thread;
|
|
1440
1561
|
}
|
|
1441
1562
|
//#endregion
|
|
1442
|
-
//#region src/
|
|
1443
|
-
const
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const LOGO_TYPES = [
|
|
1506
|
-
"image/png",
|
|
1507
|
-
"image/jpeg",
|
|
1508
|
-
"image/gif",
|
|
1509
|
-
"image/webp",
|
|
1510
|
-
"image/svg+xml",
|
|
1511
|
-
"image/x-icon",
|
|
1512
|
-
"image/vnd.microsoft.icon"
|
|
1513
|
-
];
|
|
1514
|
-
const LOGO_LIMIT = 524288;
|
|
1515
|
-
function projects$1() {
|
|
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;
|
|
1522
|
-
}
|
|
1523
|
-
async function addProject(input) {
|
|
1524
|
-
const folder = path.resolve(expandHome(input.trim()));
|
|
1525
|
-
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
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();
|
|
1532
|
-
publish$1();
|
|
1533
|
-
const project = projects$1().find((item) => item.path === folder);
|
|
1534
|
-
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
1535
|
-
return project;
|
|
1536
|
-
}
|
|
1537
|
-
function updateProject(id, patch) {
|
|
1538
|
-
if (patch.name !== void 0) {
|
|
1539
|
-
const name = patch.name.trim();
|
|
1540
|
-
if (name === "") throw new Error("Project name cannot be empty");
|
|
1541
|
-
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
1542
|
-
}
|
|
1543
|
-
if (patch.logo !== void 0) {
|
|
1544
|
-
if (patch.logo !== null) checkLogo(patch.logo);
|
|
1545
|
-
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
1563
|
+
//#region src/lib/opencode/items.ts
|
|
1564
|
+
const toolStatuses = {
|
|
1565
|
+
streaming: "running",
|
|
1566
|
+
running: "running",
|
|
1567
|
+
completed: "completed",
|
|
1568
|
+
error: "failed"
|
|
1569
|
+
};
|
|
1570
|
+
const todoStatuses$1 = {
|
|
1571
|
+
pending: "pending",
|
|
1572
|
+
in_progress: "running",
|
|
1573
|
+
completed: "completed",
|
|
1574
|
+
cancelled: "completed"
|
|
1575
|
+
};
|
|
1576
|
+
function messageItems(message, parent) {
|
|
1577
|
+
const base = {
|
|
1578
|
+
id: message.id,
|
|
1579
|
+
parent,
|
|
1580
|
+
time: message.time.created
|
|
1581
|
+
};
|
|
1582
|
+
switch (message.type) {
|
|
1583
|
+
case "user": return [{
|
|
1584
|
+
...base,
|
|
1585
|
+
kind: "user",
|
|
1586
|
+
text: message.text,
|
|
1587
|
+
images: (message.files ?? []).filter((file) => file.mime.startsWith("image/")).map((file) => ({
|
|
1588
|
+
mediaType: file.mime,
|
|
1589
|
+
data: file.data
|
|
1590
|
+
})),
|
|
1591
|
+
format: "text"
|
|
1592
|
+
}];
|
|
1593
|
+
case "skill": return [{
|
|
1594
|
+
...base,
|
|
1595
|
+
kind: "skill",
|
|
1596
|
+
skill: message.name,
|
|
1597
|
+
output: "",
|
|
1598
|
+
status: "completed"
|
|
1599
|
+
}];
|
|
1600
|
+
case "shell": return [{
|
|
1601
|
+
...base,
|
|
1602
|
+
kind: "command",
|
|
1603
|
+
command: message.command,
|
|
1604
|
+
description: "",
|
|
1605
|
+
output: message.output?.output ?? "",
|
|
1606
|
+
status: message.status === "running" ? "running" : message.status === "exited" ? message.exit === 0 ? "completed" : "failed" : "stopped",
|
|
1607
|
+
background: null
|
|
1608
|
+
}];
|
|
1609
|
+
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1610
|
+
case "compaction": return message.status === "failed" ? [{
|
|
1611
|
+
...errorItem(message.id, message.error.message),
|
|
1612
|
+
parent
|
|
1613
|
+
}] : [{
|
|
1614
|
+
...base,
|
|
1615
|
+
kind: "compaction"
|
|
1616
|
+
}];
|
|
1617
|
+
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
1618
|
+
...errorItem(message.id, message.error.message),
|
|
1619
|
+
parent
|
|
1620
|
+
}] : [];
|
|
1621
|
+
case "agent-switched":
|
|
1622
|
+
case "model-switched":
|
|
1623
|
+
case "location-switched":
|
|
1624
|
+
case "synthetic":
|
|
1625
|
+
case "system": return [];
|
|
1546
1626
|
}
|
|
1547
|
-
publish$1();
|
|
1548
|
-
return null;
|
|
1549
1627
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
|
|
1628
|
+
function formQuestions(form) {
|
|
1629
|
+
return form.fields.flatMap((field) => field.type === "external" ? [] : [{
|
|
1630
|
+
question: questionText(field),
|
|
1631
|
+
header: field.title ?? "",
|
|
1632
|
+
multiSelect: field.type === "multiselect",
|
|
1633
|
+
options: fieldOptions(field)
|
|
1634
|
+
}]);
|
|
1553
1635
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1636
|
+
function formAnswer(form, answers) {
|
|
1637
|
+
return Object.fromEntries(form.fields.flatMap((field) => {
|
|
1638
|
+
if (field.type === "external") return [];
|
|
1639
|
+
const answer = answers[questionText(field)];
|
|
1640
|
+
if (answer === void 0 || answer === "") return [];
|
|
1641
|
+
switch (field.type) {
|
|
1642
|
+
case "string": return [[field.key, optionValue(field.options ?? [], answer)]];
|
|
1643
|
+
case "multiselect": return [[field.key, answer.split(", ").map((label) => optionValue(field.options, label))]];
|
|
1644
|
+
case "boolean":
|
|
1645
|
+
if (answer === "Yes") return [[field.key, true]];
|
|
1646
|
+
if (answer === "No") return [[field.key, false]];
|
|
1647
|
+
throw new Error(`${field.key} needs Yes or No`);
|
|
1648
|
+
case "number":
|
|
1649
|
+
case "integer": return [[field.key, Number(answer)]];
|
|
1650
|
+
}
|
|
1651
|
+
}));
|
|
1566
1652
|
}
|
|
1567
|
-
function
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1653
|
+
function partItems(message, part, index, parent) {
|
|
1654
|
+
const base = {
|
|
1655
|
+
id: `${message.id}:${index}`,
|
|
1656
|
+
parent,
|
|
1657
|
+
time: message.time.created
|
|
1658
|
+
};
|
|
1659
|
+
switch (part.type) {
|
|
1660
|
+
case "text": return part.text.trim() === "" ? [] : [{
|
|
1661
|
+
...base,
|
|
1662
|
+
kind: "assistant",
|
|
1663
|
+
text: part.text,
|
|
1664
|
+
message: message.id
|
|
1665
|
+
}];
|
|
1666
|
+
case "reasoning": return part.text.trim() === "" ? [] : [{
|
|
1667
|
+
...base,
|
|
1668
|
+
kind: "reasoning",
|
|
1669
|
+
text: part.text.trim()
|
|
1670
|
+
}];
|
|
1671
|
+
case "tool": return toolItems({
|
|
1672
|
+
id: `${message.id}:${part.id}`,
|
|
1673
|
+
parent,
|
|
1674
|
+
time: part.time.created
|
|
1675
|
+
}, part);
|
|
1676
|
+
}
|
|
1572
1677
|
}
|
|
1573
|
-
function
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1678
|
+
function toolItems(base, tool) {
|
|
1679
|
+
const state = tool.state;
|
|
1680
|
+
const status = toolStatuses[state.status];
|
|
1681
|
+
const input = state.status === "streaming" ? {} : state.input;
|
|
1682
|
+
const metadata = state.status === "streaming" ? {} : state.metadata ?? {};
|
|
1683
|
+
const output = state.status === "error" ? state.error.message : state.status === "completed" ? contentText(state.content) : "";
|
|
1684
|
+
switch (tool.name) {
|
|
1685
|
+
case "shell": return [{
|
|
1686
|
+
...base,
|
|
1687
|
+
kind: "command",
|
|
1688
|
+
command: text$2(input.command),
|
|
1689
|
+
description: text$2(input.description),
|
|
1690
|
+
output,
|
|
1691
|
+
status,
|
|
1692
|
+
background: null
|
|
1693
|
+
}];
|
|
1694
|
+
case "edit": return [{
|
|
1695
|
+
...base,
|
|
1696
|
+
kind: "file_change",
|
|
1697
|
+
path: text$2(input.path ?? input.filePath),
|
|
1698
|
+
diffs: [{
|
|
1699
|
+
before: text$2(input.oldString),
|
|
1700
|
+
after: text$2(input.newString)
|
|
1701
|
+
}],
|
|
1702
|
+
output,
|
|
1703
|
+
status
|
|
1704
|
+
}];
|
|
1705
|
+
case "write": return [{
|
|
1706
|
+
...base,
|
|
1707
|
+
kind: "file_change",
|
|
1708
|
+
path: text$2(input.path ?? input.filePath),
|
|
1709
|
+
diffs: [{
|
|
1710
|
+
before: "",
|
|
1711
|
+
after: text$2(input.content)
|
|
1712
|
+
}],
|
|
1713
|
+
output,
|
|
1714
|
+
status
|
|
1715
|
+
}];
|
|
1716
|
+
case "patch":
|
|
1717
|
+
case "apply_patch":
|
|
1718
|
+
if (!Array.isArray(metadata.files)) break;
|
|
1719
|
+
return metadata.files.map((file, index) => ({
|
|
1720
|
+
...base,
|
|
1721
|
+
id: `${base.id}:${index}`,
|
|
1722
|
+
kind: "file_change",
|
|
1723
|
+
path: text$2(record(file).file),
|
|
1724
|
+
diffs: hunks(text$2(record(file).patch)),
|
|
1725
|
+
output,
|
|
1726
|
+
status
|
|
1727
|
+
}));
|
|
1728
|
+
case "todowrite": return [{
|
|
1729
|
+
...base,
|
|
1730
|
+
kind: "todo_list",
|
|
1731
|
+
steps: list$1(input.todos).map((todo) => ({
|
|
1732
|
+
text: text$2(record(todo).content),
|
|
1733
|
+
status: todoStatuses$1[text$2(record(todo).status)] ?? "pending"
|
|
1734
|
+
}))
|
|
1735
|
+
}];
|
|
1736
|
+
case "subagent":
|
|
1737
|
+
case "task": return [{
|
|
1738
|
+
...base,
|
|
1739
|
+
kind: "subagent",
|
|
1740
|
+
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
1741
|
+
description: text$2(input.description),
|
|
1742
|
+
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
1743
|
+
status
|
|
1744
|
+
}];
|
|
1745
|
+
case "webfetch":
|
|
1746
|
+
case "websearch": return [{
|
|
1747
|
+
...base,
|
|
1748
|
+
kind: "web_search",
|
|
1749
|
+
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
1750
|
+
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
1751
|
+
results: [],
|
|
1752
|
+
output,
|
|
1753
|
+
status
|
|
1754
|
+
}];
|
|
1755
|
+
case "question": {
|
|
1756
|
+
const questions = list$1(input.questions).map((item) => ({
|
|
1757
|
+
question: text$2(record(item).question),
|
|
1758
|
+
header: text$2(record(item).header),
|
|
1759
|
+
multiSelect: record(item).multiple === true,
|
|
1760
|
+
options: list$1(record(item).options).map((option) => ({
|
|
1761
|
+
label: text$2(record(option).label),
|
|
1762
|
+
description: text$2(record(option).description)
|
|
1763
|
+
}))
|
|
1764
|
+
}));
|
|
1765
|
+
const answers = metadata.answers;
|
|
1766
|
+
return [{
|
|
1767
|
+
...base,
|
|
1768
|
+
kind: "question",
|
|
1769
|
+
questions,
|
|
1770
|
+
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list$1(answers[index]).map(text$2).join(", ")])) : null,
|
|
1771
|
+
status
|
|
1772
|
+
}];
|
|
1773
|
+
}
|
|
1774
|
+
case "skill": return [{
|
|
1775
|
+
...base,
|
|
1776
|
+
kind: "skill",
|
|
1777
|
+
skill: text$2(input.name),
|
|
1778
|
+
output,
|
|
1779
|
+
status
|
|
1780
|
+
}];
|
|
1781
|
+
}
|
|
1782
|
+
return [{
|
|
1783
|
+
...base,
|
|
1784
|
+
kind: "tool",
|
|
1785
|
+
name: tool.name,
|
|
1786
|
+
input,
|
|
1787
|
+
output,
|
|
1788
|
+
status
|
|
1789
|
+
}];
|
|
1577
1790
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
1581
|
-
return input;
|
|
1791
|
+
function questionText(field) {
|
|
1792
|
+
return field.description ?? field.title ?? field.key;
|
|
1582
1793
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
|
1794
|
+
function fieldOptions(field) {
|
|
1795
|
+
switch (field.type) {
|
|
1796
|
+
case "string":
|
|
1797
|
+
case "multiselect": return (field.options ?? []).map((option) => ({
|
|
1798
|
+
label: option.label,
|
|
1799
|
+
description: option.description ?? ""
|
|
1800
|
+
}));
|
|
1801
|
+
case "boolean": return [{
|
|
1802
|
+
label: "Yes",
|
|
1803
|
+
description: ""
|
|
1804
|
+
}, {
|
|
1805
|
+
label: "No",
|
|
1806
|
+
description: ""
|
|
1807
|
+
}];
|
|
1808
|
+
case "number":
|
|
1809
|
+
case "integer": return [];
|
|
1613
1810
|
}
|
|
1614
|
-
};
|
|
1615
|
-
const refreshing = /* @__PURE__ */ new Map();
|
|
1616
|
-
const timers = /* @__PURE__ */ new Map();
|
|
1617
|
-
const providers = /* @__PURE__ */ new Map();
|
|
1618
|
-
let watchers = [];
|
|
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) => {
|
|
1623
|
-
const record = records.get(item.id);
|
|
1624
|
-
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
1625
|
-
return {
|
|
1626
|
-
...item,
|
|
1627
|
-
updatedAt,
|
|
1628
|
-
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
1629
|
-
};
|
|
1630
|
-
}));
|
|
1631
1811
|
}
|
|
1632
|
-
function
|
|
1633
|
-
return
|
|
1812
|
+
function optionValue(options, label) {
|
|
1813
|
+
return options.find((option) => option.label === label)?.value ?? label;
|
|
1634
1814
|
}
|
|
1635
|
-
function
|
|
1636
|
-
return
|
|
1815
|
+
function contentText(content) {
|
|
1816
|
+
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
1637
1817
|
}
|
|
1638
|
-
function
|
|
1639
|
-
|
|
1640
|
-
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
1641
|
-
return provider;
|
|
1818
|
+
function text$2(value) {
|
|
1819
|
+
return typeof value === "string" ? value : "";
|
|
1642
1820
|
}
|
|
1643
|
-
function
|
|
1644
|
-
|
|
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();
|
|
1821
|
+
function list$1(value) {
|
|
1822
|
+
return Array.isArray(value) ? value : [];
|
|
1655
1823
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
type: "sessions",
|
|
1659
|
-
sessions: sessions()
|
|
1660
|
-
});
|
|
1824
|
+
function record(value) {
|
|
1825
|
+
return isRecord(value) ? value : {};
|
|
1661
1826
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/lib/opencode/sessions.ts
|
|
1829
|
+
const PAGE = 100;
|
|
1830
|
+
const COMMIT_LIMIT = 12e4;
|
|
1831
|
+
const temporary = /* @__PURE__ */ new Set();
|
|
1832
|
+
function openCodeInstalled() {
|
|
1833
|
+
return findOpenCode() !== null;
|
|
1664
1834
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1835
|
+
async function openCodeCapabilities(cwd) {
|
|
1836
|
+
const client = await openCode();
|
|
1837
|
+
const location = { directory: cwd };
|
|
1838
|
+
const [models, commands, skills, agents] = await Promise.all([
|
|
1839
|
+
client.model.list({ location }),
|
|
1840
|
+
client.command.list({ location }),
|
|
1841
|
+
client.skill.list({ location }),
|
|
1842
|
+
client.agent.list({ location })
|
|
1843
|
+
]);
|
|
1844
|
+
return {
|
|
1845
|
+
models: models.data.filter((model) => model.enabled).map((model) => ({
|
|
1846
|
+
value: `${model.providerID}/${model.id}`,
|
|
1847
|
+
name: model.name,
|
|
1848
|
+
description: model.providerID,
|
|
1849
|
+
efforts: model.variants.map((variant) => variant.id)
|
|
1850
|
+
})),
|
|
1851
|
+
commands: [{
|
|
1852
|
+
name: "compact",
|
|
1853
|
+
description: "Summarize the conversation to free up context",
|
|
1854
|
+
hint: ""
|
|
1855
|
+
}, ...[...commands.data, ...skills.data].map((command) => ({
|
|
1856
|
+
name: command.name,
|
|
1857
|
+
description: command.description ?? "",
|
|
1858
|
+
hint: ""
|
|
1859
|
+
}))],
|
|
1860
|
+
agents: agents.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden).map((agent) => ({
|
|
1861
|
+
id: agent.id,
|
|
1862
|
+
name: agent.name,
|
|
1863
|
+
description: agent.description ?? ""
|
|
1864
|
+
}))
|
|
1865
|
+
};
|
|
1671
1866
|
}
|
|
1672
|
-
async function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1867
|
+
async function openCodeSessions(paths) {
|
|
1868
|
+
if (paths.length === 0 || !openCodeInstalled()) return [];
|
|
1869
|
+
const client = await openCode();
|
|
1870
|
+
return (await Promise.all(paths.map(async (directory) => {
|
|
1871
|
+
const found = [];
|
|
1872
|
+
let cursor = null;
|
|
1873
|
+
do {
|
|
1874
|
+
const page = await client.session.list(cursor === null ? {
|
|
1875
|
+
directory,
|
|
1876
|
+
parentID: null,
|
|
1877
|
+
limit: PAGE
|
|
1878
|
+
} : { cursor });
|
|
1879
|
+
found.push(...page.data);
|
|
1880
|
+
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
1881
|
+
} while (cursor !== null);
|
|
1882
|
+
return found;
|
|
1883
|
+
}))).flat().filter((session) => !temporary.has(session.id));
|
|
1685
1884
|
}
|
|
1686
|
-
async function
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
}
|
|
1710
|
-
} catch (error) {
|
|
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
|
-
});
|
|
1885
|
+
async function openCodeHistory(sessionId, cwd) {
|
|
1886
|
+
const run = openCodeRun(sessionId);
|
|
1887
|
+
const client = await openCode();
|
|
1888
|
+
const messages = await allMessages(client, sessionId);
|
|
1889
|
+
return {
|
|
1890
|
+
type: "history",
|
|
1891
|
+
sessionId,
|
|
1892
|
+
provider: "opencode",
|
|
1893
|
+
items: messages.flatMap((message) => messageItems(message, null)),
|
|
1894
|
+
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
1895
|
+
queue: run?.queue ?? [],
|
|
1896
|
+
settings: run?.settings ?? null,
|
|
1897
|
+
context: run?.context ?? await contextOf(client, cwd, messages),
|
|
1898
|
+
before: null,
|
|
1899
|
+
partial: run?.partial ?? ""
|
|
1900
|
+
};
|
|
1726
1901
|
}
|
|
1727
|
-
async function
|
|
1728
|
-
|
|
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
|
-
});
|
|
1769
|
-
}
|
|
1902
|
+
async function openCodeAgent(agentId, parent) {
|
|
1903
|
+
return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
|
|
1770
1904
|
}
|
|
1771
|
-
function
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
1776
|
-
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
1777
|
-
return [watcher];
|
|
1778
|
-
} catch (error) {
|
|
1779
|
-
if (error.code !== "ENOENT") throw error;
|
|
1780
|
-
return [];
|
|
1781
|
-
}
|
|
1905
|
+
async function renameOpenCode(sessionId, title) {
|
|
1906
|
+
await (await openCode()).session.update({
|
|
1907
|
+
sessionID: sessionId,
|
|
1908
|
+
title
|
|
1782
1909
|
});
|
|
1783
1910
|
}
|
|
1784
|
-
async function
|
|
1785
|
-
|
|
1786
|
-
case "claude":
|
|
1787
|
-
await renameSession(id, patch.title);
|
|
1788
|
-
break;
|
|
1789
|
-
case "codex":
|
|
1790
|
-
await renameThread(id, patch.title);
|
|
1791
|
-
break;
|
|
1792
|
-
case "opencode": await renameOpenCode(id, patch.title);
|
|
1793
|
-
}
|
|
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));
|
|
1911
|
+
async function removeOpenCode(sessionId) {
|
|
1912
|
+
await (await openCode()).session.remove({ sessionID: sessionId });
|
|
1803
1913
|
}
|
|
1804
|
-
async function
|
|
1805
|
-
const
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
break;
|
|
1815
|
-
case "codex":
|
|
1816
|
-
await deleteThread(id);
|
|
1817
|
-
break;
|
|
1818
|
-
case "opencode": await removeOpenCode(id);
|
|
1819
|
-
}
|
|
1820
|
-
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
1821
|
-
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
1914
|
+
async function forkOpenCode(sessionId, messageId) {
|
|
1915
|
+
const client = await openCode();
|
|
1916
|
+
const messages = await allMessages(client, sessionId);
|
|
1917
|
+
const index = messages.findIndex((message) => message.id === messageId);
|
|
1918
|
+
if (index < 0) throw new Error("Cannot fork the session at that point");
|
|
1919
|
+
const next = messages.at(index + 1);
|
|
1920
|
+
return { sessionId: (await client.session.fork({
|
|
1921
|
+
sessionID: sessionId,
|
|
1922
|
+
...next === void 0 ? {} : { before: next.id }
|
|
1923
|
+
})).id };
|
|
1822
1924
|
}
|
|
1823
|
-
async function
|
|
1824
|
-
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1925
|
+
async function rewindOpenCode(request) {
|
|
1926
|
+
if (request.code && !request.conversation && !request.dryRun) throw new Error("OpenCode restores files only with the conversation");
|
|
1927
|
+
const client = await openCode();
|
|
1928
|
+
const sessionID = request.sessionId;
|
|
1929
|
+
const files = (await client.session.revert.stage({
|
|
1930
|
+
sessionID,
|
|
1931
|
+
messageID: request.messageId,
|
|
1932
|
+
files: request.code
|
|
1933
|
+
})).files ?? [];
|
|
1934
|
+
const result = request.code ? {
|
|
1935
|
+
files: files.map((file) => file.file),
|
|
1936
|
+
insertions: sumBy(files, (file) => file.additions),
|
|
1937
|
+
deletions: sumBy(files, (file) => file.deletions)
|
|
1938
|
+
} : null;
|
|
1939
|
+
if (request.dryRun) await client.session.revert.clear({ sessionID });
|
|
1940
|
+
else await client.session.revert.commit({ sessionID });
|
|
1829
1941
|
return result;
|
|
1830
1942
|
}
|
|
1831
|
-
async function
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
upToMessageId: messageId
|
|
1838
|
-
})).sessionId };
|
|
1839
|
-
}
|
|
1840
|
-
//#endregion
|
|
1841
|
-
//#region src/lib/opencode/client.ts
|
|
1842
|
-
const listChanges = /* @__PURE__ */ new Set([
|
|
1843
|
-
"session.created",
|
|
1844
|
-
"session.renamed",
|
|
1845
|
-
"session.moved",
|
|
1846
|
-
"session.forked",
|
|
1847
|
-
"session.deleted",
|
|
1848
|
-
"session.execution.succeeded",
|
|
1849
|
-
"session.execution.failed",
|
|
1850
|
-
"session.execution.interrupted"
|
|
1851
|
-
]);
|
|
1852
|
-
const SILENCE_LIMIT$1 = 45e3;
|
|
1853
|
-
const RECONNECT_ATTEMPTS = 5;
|
|
1854
|
-
const RECONNECT_DELAY = 2e3;
|
|
1855
|
-
let connection;
|
|
1856
|
-
function findOpenCode() {
|
|
1857
|
-
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
1858
|
-
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
1859
|
-
const candidate = path.join(dir, name);
|
|
1860
|
-
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
1861
|
-
}
|
|
1862
|
-
return null;
|
|
1863
|
-
}
|
|
1864
|
-
function openCode() {
|
|
1865
|
-
connection ??= start();
|
|
1866
|
-
return connection;
|
|
1867
|
-
}
|
|
1868
|
-
async function start() {
|
|
1869
|
-
try {
|
|
1870
|
-
const client = await connect();
|
|
1871
|
-
listen(client);
|
|
1872
|
-
return client;
|
|
1873
|
-
} catch (error) {
|
|
1874
|
-
connection = void 0;
|
|
1875
|
-
throw error;
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
async function connect() {
|
|
1879
|
-
const endpoint = await ensure();
|
|
1880
|
-
return OpenCode.make({
|
|
1881
|
-
baseUrl: endpoint.url,
|
|
1882
|
-
headers: headers(endpoint)
|
|
1943
|
+
async function openCodeCommitMessage(cwd, model, prompt) {
|
|
1944
|
+
const client = await openCode();
|
|
1945
|
+
const session = await client.session.create({
|
|
1946
|
+
location: { directory: cwd },
|
|
1947
|
+
agent: "plan",
|
|
1948
|
+
model: modelRef(model, null)
|
|
1883
1949
|
});
|
|
1884
|
-
|
|
1885
|
-
async function listen(client) {
|
|
1886
|
-
const controller = new AbortController();
|
|
1887
|
-
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
1950
|
+
temporary.add(session.id);
|
|
1888
1951
|
try {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
})
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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;
|
|
1952
|
+
await client.session.prompt({
|
|
1953
|
+
sessionID: session.id,
|
|
1954
|
+
text: prompt
|
|
1955
|
+
});
|
|
1956
|
+
await client.session.wait({ sessionID: session.id }, { signal: AbortSignal.timeout(COMMIT_LIMIT) });
|
|
1957
|
+
const messages = await allMessages(client, session.id);
|
|
1958
|
+
for (const message of messages.toReversed()) {
|
|
1959
|
+
if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
|
|
1960
|
+
if (message.type !== "assistant") continue;
|
|
1961
|
+
const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
|
|
1962
|
+
if (text !== "") return { message: text };
|
|
1912
1963
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1964
|
+
throw new Error("OpenCode did not reply");
|
|
1965
|
+
} finally {
|
|
1966
|
+
await client.session.remove({ sessionID: session.id });
|
|
1967
|
+
temporary.delete(session.id);
|
|
1916
1968
|
}
|
|
1917
|
-
console.warn(`could not reconnect to OpenCode: ${failure}`);
|
|
1918
|
-
connection = void 0;
|
|
1919
|
-
for (const run of openCodeRuns()) run.exited();
|
|
1920
|
-
refreshSessions("opencode");
|
|
1921
|
-
throw failure;
|
|
1922
1969
|
}
|
|
1923
|
-
async function
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1970
|
+
async function contextOf(client, cwd, messages) {
|
|
1971
|
+
const last = messages.findLast((message) => message.type === "assistant" && message.tokens !== void 0);
|
|
1972
|
+
if (last?.type !== "assistant" || last.tokens === void 0) return null;
|
|
1973
|
+
const tokens = last.tokens;
|
|
1974
|
+
const model = (await client.model.list({ location: { directory: cwd } })).data.find((item) => item.providerID === last.model.providerID && item.id === last.model.id);
|
|
1975
|
+
return {
|
|
1976
|
+
tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write,
|
|
1977
|
+
window: model?.limit.context ?? null
|
|
1978
|
+
};
|
|
1930
1979
|
}
|
|
1931
|
-
function
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1980
|
+
function modelRef(value, effort) {
|
|
1981
|
+
const index = value.indexOf("/");
|
|
1982
|
+
if (index < 0) throw new Error(`${value} is not an OpenCode model`);
|
|
1983
|
+
return {
|
|
1984
|
+
providerID: value.slice(0, index),
|
|
1985
|
+
id: value.slice(index + 1),
|
|
1986
|
+
...effort === null ? {} : { variant: effort }
|
|
1987
|
+
};
|
|
1937
1988
|
}
|
|
1938
|
-
function
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1989
|
+
async function allMessages(client, sessionId) {
|
|
1990
|
+
const messages = [];
|
|
1991
|
+
let cursor = null;
|
|
1992
|
+
do {
|
|
1993
|
+
const page = await client.message.list(cursor === null ? {
|
|
1994
|
+
sessionID: sessionId,
|
|
1995
|
+
limit: PAGE,
|
|
1996
|
+
order: "asc"
|
|
1997
|
+
} : {
|
|
1998
|
+
sessionID: sessionId,
|
|
1999
|
+
cursor
|
|
2000
|
+
});
|
|
2001
|
+
messages.push(...page.data);
|
|
2002
|
+
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2003
|
+
} while (cursor !== null);
|
|
2004
|
+
return messages;
|
|
1943
2005
|
}
|
|
1944
|
-
function
|
|
1945
|
-
|
|
2006
|
+
function openCodeRun(sessionId) {
|
|
2007
|
+
const run = runs.get(sessionId);
|
|
2008
|
+
return run?.provider === "opencode" ? run : void 0;
|
|
1946
2009
|
}
|
|
1947
2010
|
//#endregion
|
|
1948
|
-
//#region src/lib/opencode/
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
const todoStatuses$1 = {
|
|
1956
|
-
pending: "pending",
|
|
1957
|
-
in_progress: "running",
|
|
1958
|
-
completed: "completed",
|
|
1959
|
-
cancelled: "completed"
|
|
1960
|
-
};
|
|
1961
|
-
function messageItems(message, parent) {
|
|
1962
|
-
const base = {
|
|
1963
|
-
id: message.id,
|
|
1964
|
-
parent,
|
|
1965
|
-
time: message.time.created
|
|
1966
|
-
};
|
|
1967
|
-
switch (message.type) {
|
|
1968
|
-
case "user": return [{
|
|
1969
|
-
...base,
|
|
1970
|
-
kind: "user",
|
|
1971
|
-
text: message.text,
|
|
1972
|
-
images: (message.files ?? []).filter((file) => file.mime.startsWith("image/")).map((file) => ({
|
|
1973
|
-
mediaType: file.mime,
|
|
1974
|
-
data: file.data
|
|
1975
|
-
})),
|
|
1976
|
-
format: "text"
|
|
1977
|
-
}];
|
|
1978
|
-
case "skill": return [{
|
|
1979
|
-
...base,
|
|
1980
|
-
kind: "skill",
|
|
1981
|
-
skill: message.name,
|
|
1982
|
-
output: "",
|
|
1983
|
-
status: "completed"
|
|
1984
|
-
}];
|
|
1985
|
-
case "shell": return [{
|
|
1986
|
-
...base,
|
|
1987
|
-
kind: "command",
|
|
1988
|
-
command: message.command,
|
|
1989
|
-
description: "",
|
|
1990
|
-
output: message.output?.output ?? "",
|
|
1991
|
-
status: message.status === "running" ? "running" : message.status === "exited" ? message.exit === 0 ? "completed" : "failed" : "stopped",
|
|
1992
|
-
background: null
|
|
1993
|
-
}];
|
|
1994
|
-
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1995
|
-
case "compaction": return message.status === "failed" ? [{
|
|
1996
|
-
...errorItem(message.id, message.error.message),
|
|
1997
|
-
parent
|
|
1998
|
-
}] : [{
|
|
1999
|
-
...base,
|
|
2000
|
-
kind: "compaction"
|
|
2001
|
-
}];
|
|
2002
|
-
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
2003
|
-
...errorItem(message.id, message.error.message),
|
|
2004
|
-
parent
|
|
2005
|
-
}] : [];
|
|
2006
|
-
case "agent-switched":
|
|
2007
|
-
case "model-switched":
|
|
2008
|
-
case "location-switched":
|
|
2009
|
-
case "synthetic":
|
|
2010
|
-
case "system": return [];
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
function formQuestions(form) {
|
|
2014
|
-
return form.fields.flatMap((field) => field.type === "external" ? [] : [{
|
|
2015
|
-
question: questionText(field),
|
|
2016
|
-
header: field.title ?? "",
|
|
2017
|
-
multiSelect: field.type === "multiselect",
|
|
2018
|
-
options: fieldOptions(field)
|
|
2019
|
-
}]);
|
|
2020
|
-
}
|
|
2021
|
-
function formAnswer(form, answers) {
|
|
2022
|
-
return Object.fromEntries(form.fields.flatMap((field) => {
|
|
2023
|
-
if (field.type === "external") return [];
|
|
2024
|
-
const answer = answers[questionText(field)];
|
|
2025
|
-
if (answer === void 0 || answer === "") return [];
|
|
2026
|
-
switch (field.type) {
|
|
2027
|
-
case "string": return [[field.key, optionValue(field.options ?? [], answer)]];
|
|
2028
|
-
case "multiselect": return [[field.key, answer.split(", ").map((label) => optionValue(field.options, label))]];
|
|
2029
|
-
case "boolean":
|
|
2030
|
-
if (answer === "Yes") return [[field.key, true]];
|
|
2031
|
-
if (answer === "No") return [[field.key, false]];
|
|
2032
|
-
throw new Error(`${field.key} needs Yes or No`);
|
|
2033
|
-
case "number":
|
|
2034
|
-
case "integer": return [[field.key, Number(answer)]];
|
|
2035
|
-
}
|
|
2036
|
-
}));
|
|
2037
|
-
}
|
|
2038
|
-
function partItems(message, part, index, parent) {
|
|
2039
|
-
const base = {
|
|
2040
|
-
id: `${message.id}:${index}`,
|
|
2041
|
-
parent,
|
|
2042
|
-
time: message.time.created
|
|
2043
|
-
};
|
|
2044
|
-
switch (part.type) {
|
|
2045
|
-
case "text": return part.text.trim() === "" ? [] : [{
|
|
2046
|
-
...base,
|
|
2047
|
-
kind: "assistant",
|
|
2048
|
-
text: part.text,
|
|
2049
|
-
message: message.id
|
|
2050
|
-
}];
|
|
2051
|
-
case "reasoning": return part.text.trim() === "" ? [] : [{
|
|
2052
|
-
...base,
|
|
2053
|
-
kind: "reasoning",
|
|
2054
|
-
text: part.text.trim()
|
|
2055
|
-
}];
|
|
2056
|
-
case "tool": return toolItems({
|
|
2057
|
-
id: `${message.id}:${part.id}`,
|
|
2058
|
-
parent,
|
|
2059
|
-
time: part.time.created
|
|
2060
|
-
}, part);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
function toolItems(base, tool) {
|
|
2064
|
-
const state = tool.state;
|
|
2065
|
-
const status = toolStatuses[state.status];
|
|
2066
|
-
const input = state.status === "streaming" ? {} : state.input;
|
|
2067
|
-
const metadata = state.status === "streaming" ? {} : state.metadata ?? {};
|
|
2068
|
-
const output = state.status === "error" ? state.error.message : state.status === "completed" ? contentText(state.content) : "";
|
|
2069
|
-
switch (tool.name) {
|
|
2070
|
-
case "shell": return [{
|
|
2071
|
-
...base,
|
|
2072
|
-
kind: "command",
|
|
2073
|
-
command: text$2(input.command),
|
|
2074
|
-
description: text$2(input.description),
|
|
2075
|
-
output,
|
|
2076
|
-
status,
|
|
2077
|
-
background: null
|
|
2078
|
-
}];
|
|
2079
|
-
case "edit": return [{
|
|
2080
|
-
...base,
|
|
2081
|
-
kind: "file_change",
|
|
2082
|
-
path: text$2(input.path ?? input.filePath),
|
|
2083
|
-
diffs: [{
|
|
2084
|
-
before: text$2(input.oldString),
|
|
2085
|
-
after: text$2(input.newString)
|
|
2086
|
-
}],
|
|
2087
|
-
output,
|
|
2088
|
-
status
|
|
2089
|
-
}];
|
|
2090
|
-
case "write": return [{
|
|
2091
|
-
...base,
|
|
2092
|
-
kind: "file_change",
|
|
2093
|
-
path: text$2(input.path ?? input.filePath),
|
|
2094
|
-
diffs: [{
|
|
2095
|
-
before: "",
|
|
2096
|
-
after: text$2(input.content)
|
|
2097
|
-
}],
|
|
2098
|
-
output,
|
|
2099
|
-
status
|
|
2100
|
-
}];
|
|
2101
|
-
case "patch":
|
|
2102
|
-
case "apply_patch":
|
|
2103
|
-
if (!Array.isArray(metadata.files)) break;
|
|
2104
|
-
return metadata.files.map((file, index) => ({
|
|
2105
|
-
...base,
|
|
2106
|
-
id: `${base.id}:${index}`,
|
|
2107
|
-
kind: "file_change",
|
|
2108
|
-
path: text$2(record(file).file),
|
|
2109
|
-
diffs: hunks(text$2(record(file).patch)),
|
|
2110
|
-
output,
|
|
2111
|
-
status
|
|
2112
|
-
}));
|
|
2113
|
-
case "todowrite": return [{
|
|
2114
|
-
...base,
|
|
2115
|
-
kind: "todo_list",
|
|
2116
|
-
steps: list(input.todos).map((todo) => ({
|
|
2117
|
-
text: text$2(record(todo).content),
|
|
2118
|
-
status: todoStatuses$1[text$2(record(todo).status)] ?? "pending"
|
|
2119
|
-
}))
|
|
2120
|
-
}];
|
|
2121
|
-
case "subagent":
|
|
2122
|
-
case "task": return [{
|
|
2123
|
-
...base,
|
|
2124
|
-
kind: "subagent",
|
|
2125
|
-
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
2126
|
-
description: text$2(input.description),
|
|
2127
|
-
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
2128
|
-
status
|
|
2129
|
-
}];
|
|
2130
|
-
case "webfetch":
|
|
2131
|
-
case "websearch": return [{
|
|
2132
|
-
...base,
|
|
2133
|
-
kind: "web_search",
|
|
2134
|
-
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
2135
|
-
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
2136
|
-
results: [],
|
|
2137
|
-
output,
|
|
2138
|
-
status
|
|
2139
|
-
}];
|
|
2140
|
-
case "question": {
|
|
2141
|
-
const questions = list(input.questions).map((item) => ({
|
|
2142
|
-
question: text$2(record(item).question),
|
|
2143
|
-
header: text$2(record(item).header),
|
|
2144
|
-
multiSelect: record(item).multiple === true,
|
|
2145
|
-
options: list(record(item).options).map((option) => ({
|
|
2146
|
-
label: text$2(record(option).label),
|
|
2147
|
-
description: text$2(record(option).description)
|
|
2148
|
-
}))
|
|
2149
|
-
}));
|
|
2150
|
-
const answers = metadata.answers;
|
|
2151
|
-
return [{
|
|
2152
|
-
...base,
|
|
2153
|
-
kind: "question",
|
|
2154
|
-
questions,
|
|
2155
|
-
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list(answers[index]).map(text$2).join(", ")])) : null,
|
|
2156
|
-
status
|
|
2157
|
-
}];
|
|
2158
|
-
}
|
|
2159
|
-
case "skill": return [{
|
|
2160
|
-
...base,
|
|
2161
|
-
kind: "skill",
|
|
2162
|
-
skill: text$2(input.name),
|
|
2163
|
-
output,
|
|
2164
|
-
status
|
|
2165
|
-
}];
|
|
2166
|
-
}
|
|
2167
|
-
return [{
|
|
2168
|
-
...base,
|
|
2169
|
-
kind: "tool",
|
|
2170
|
-
name: tool.name,
|
|
2171
|
-
input,
|
|
2172
|
-
output,
|
|
2173
|
-
status
|
|
2174
|
-
}];
|
|
2175
|
-
}
|
|
2176
|
-
function questionText(field) {
|
|
2177
|
-
return field.description ?? field.title ?? field.key;
|
|
2178
|
-
}
|
|
2179
|
-
function fieldOptions(field) {
|
|
2180
|
-
switch (field.type) {
|
|
2181
|
-
case "string":
|
|
2182
|
-
case "multiselect": return (field.options ?? []).map((option) => ({
|
|
2183
|
-
label: option.label,
|
|
2184
|
-
description: option.description ?? ""
|
|
2185
|
-
}));
|
|
2186
|
-
case "boolean": return [{
|
|
2187
|
-
label: "Yes",
|
|
2188
|
-
description: ""
|
|
2189
|
-
}, {
|
|
2190
|
-
label: "No",
|
|
2191
|
-
description: ""
|
|
2192
|
-
}];
|
|
2193
|
-
case "number":
|
|
2194
|
-
case "integer": return [];
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
function optionValue(options, label) {
|
|
2198
|
-
return options.find((option) => option.label === label)?.value ?? label;
|
|
2199
|
-
}
|
|
2200
|
-
function contentText(content) {
|
|
2201
|
-
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
2202
|
-
}
|
|
2203
|
-
function text$2(value) {
|
|
2204
|
-
return typeof value === "string" ? value : "";
|
|
2205
|
-
}
|
|
2206
|
-
function list(value) {
|
|
2207
|
-
return Array.isArray(value) ? value : [];
|
|
2208
|
-
}
|
|
2209
|
-
function record(value) {
|
|
2210
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2211
|
-
}
|
|
2212
|
-
//#endregion
|
|
2213
|
-
//#region src/lib/opencode/sessions.ts
|
|
2214
|
-
const PAGE = 100;
|
|
2215
|
-
const COMMIT_LIMIT = 12e4;
|
|
2216
|
-
const temporary = /* @__PURE__ */ new Set();
|
|
2217
|
-
function openCodeAccount() {
|
|
2218
|
-
return {
|
|
2219
|
-
id: "opencode",
|
|
2220
|
-
provider: "opencode",
|
|
2221
|
-
name: "OpenCode",
|
|
2222
|
-
configDir: "",
|
|
2223
|
-
email: null,
|
|
2224
|
-
plan: null,
|
|
2225
|
-
error: null,
|
|
2226
|
-
limits: [],
|
|
2227
|
-
linked: true,
|
|
2228
|
-
valid: true
|
|
2229
|
-
};
|
|
2230
|
-
}
|
|
2231
|
-
function openCodeInstalled() {
|
|
2232
|
-
return findOpenCode() !== null;
|
|
2233
|
-
}
|
|
2234
|
-
async function openCodeCapabilities(cwd) {
|
|
2235
|
-
const client = await openCode();
|
|
2236
|
-
const location = { directory: cwd };
|
|
2237
|
-
const [models, commands, skills, agents] = await Promise.all([
|
|
2238
|
-
client.model.list({ location }),
|
|
2239
|
-
client.command.list({ location }),
|
|
2240
|
-
client.skill.list({ location }),
|
|
2241
|
-
client.agent.list({ location })
|
|
2242
|
-
]);
|
|
2243
|
-
return {
|
|
2244
|
-
models: models.data.filter((model) => model.enabled).map((model) => ({
|
|
2245
|
-
value: `${model.providerID}/${model.id}`,
|
|
2246
|
-
name: model.name,
|
|
2247
|
-
description: model.providerID,
|
|
2248
|
-
efforts: model.variants.map((variant) => variant.id)
|
|
2249
|
-
})),
|
|
2250
|
-
commands: [
|
|
2251
|
-
{
|
|
2252
|
-
name: "compact",
|
|
2253
|
-
description: "Summarize the conversation to free up context",
|
|
2254
|
-
hint: ""
|
|
2255
|
-
},
|
|
2256
|
-
...commands.data.map((command) => ({
|
|
2257
|
-
name: command.name,
|
|
2258
|
-
description: command.description ?? "",
|
|
2259
|
-
hint: ""
|
|
2260
|
-
})),
|
|
2261
|
-
...skills.data.map((skill) => ({
|
|
2262
|
-
name: skill.name,
|
|
2263
|
-
description: skill.description ?? "",
|
|
2264
|
-
hint: ""
|
|
2265
|
-
}))
|
|
2266
|
-
],
|
|
2267
|
-
agents: agents.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden).map((agent) => ({
|
|
2268
|
-
id: agent.id,
|
|
2269
|
-
name: agent.name,
|
|
2270
|
-
description: agent.description ?? ""
|
|
2271
|
-
}))
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
async function openCodeSessions(paths) {
|
|
2275
|
-
if (paths.length === 0 || !openCodeInstalled()) return [];
|
|
2276
|
-
const client = await openCode();
|
|
2277
|
-
return (await Promise.all(paths.map(async (directory) => {
|
|
2278
|
-
const found = [];
|
|
2279
|
-
let cursor = null;
|
|
2280
|
-
do {
|
|
2281
|
-
const page = await client.session.list(cursor === null ? {
|
|
2282
|
-
directory,
|
|
2283
|
-
parentID: null,
|
|
2284
|
-
limit: PAGE
|
|
2285
|
-
} : { cursor });
|
|
2286
|
-
found.push(...page.data);
|
|
2287
|
-
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2288
|
-
} while (cursor !== null);
|
|
2289
|
-
return found;
|
|
2290
|
-
}))).flat().filter((session) => !temporary.has(session.id));
|
|
2291
|
-
}
|
|
2292
|
-
async function openCodeHistory(sessionId, cwd) {
|
|
2293
|
-
const run = openCodeRun(sessionId);
|
|
2294
|
-
const client = await openCode();
|
|
2295
|
-
const messages = await allMessages(client, sessionId);
|
|
2296
|
-
return {
|
|
2297
|
-
type: "history",
|
|
2298
|
-
sessionId,
|
|
2299
|
-
provider: "opencode",
|
|
2300
|
-
items: messages.flatMap((message) => messageItems(message, null)),
|
|
2301
|
-
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
2302
|
-
queue: run?.queue ?? [],
|
|
2303
|
-
settings: run?.settings ?? null,
|
|
2304
|
-
context: run?.context ?? await contextOf(client, cwd, messages),
|
|
2305
|
-
before: null,
|
|
2306
|
-
partial: run?.partial ?? ""
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
async function openCodeAgent(agentId, parent) {
|
|
2310
|
-
return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
|
|
2311
|
-
}
|
|
2312
|
-
async function renameOpenCode(sessionId, title) {
|
|
2313
|
-
await (await openCode()).session.update({
|
|
2314
|
-
sessionID: sessionId,
|
|
2315
|
-
title
|
|
2316
|
-
});
|
|
2317
|
-
}
|
|
2318
|
-
async function removeOpenCode(sessionId) {
|
|
2319
|
-
await (await openCode()).session.remove({ sessionID: sessionId });
|
|
2320
|
-
}
|
|
2321
|
-
async function forkOpenCode(sessionId, messageId) {
|
|
2322
|
-
const client = await openCode();
|
|
2323
|
-
const messages = await allMessages(client, sessionId);
|
|
2324
|
-
const index = messages.findIndex((message) => message.id === messageId);
|
|
2325
|
-
if (index < 0) throw new Error("Cannot fork the session at that point");
|
|
2326
|
-
const next = messages.at(index + 1);
|
|
2327
|
-
return { sessionId: (await client.session.fork({
|
|
2328
|
-
sessionID: sessionId,
|
|
2329
|
-
...next === void 0 ? {} : { before: next.id }
|
|
2330
|
-
})).id };
|
|
2331
|
-
}
|
|
2332
|
-
async function rewindOpenCode(request) {
|
|
2333
|
-
if (request.code && !request.conversation && !request.dryRun) throw new Error("OpenCode restores files only with the conversation");
|
|
2334
|
-
const client = await openCode();
|
|
2335
|
-
const sessionID = request.sessionId;
|
|
2336
|
-
const files = (await client.session.revert.stage({
|
|
2337
|
-
sessionID,
|
|
2338
|
-
messageID: request.messageId,
|
|
2339
|
-
files: request.code
|
|
2340
|
-
})).files ?? [];
|
|
2341
|
-
const result = request.code ? {
|
|
2342
|
-
files: files.map((file) => file.file),
|
|
2343
|
-
insertions: files.reduce((sum, file) => sum + file.additions, 0),
|
|
2344
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0)
|
|
2345
|
-
} : null;
|
|
2346
|
-
if (request.dryRun) await client.session.revert.clear({ sessionID });
|
|
2347
|
-
else await client.session.revert.commit({ sessionID });
|
|
2348
|
-
return result;
|
|
2349
|
-
}
|
|
2350
|
-
async function openCodeCommitMessage(cwd, model, prompt) {
|
|
2351
|
-
const client = await openCode();
|
|
2352
|
-
const session = await client.session.create({
|
|
2353
|
-
location: { directory: cwd },
|
|
2354
|
-
agent: "plan",
|
|
2355
|
-
model: modelRef(model, null)
|
|
2356
|
-
});
|
|
2357
|
-
temporary.add(session.id);
|
|
2358
|
-
try {
|
|
2359
|
-
await client.session.prompt({
|
|
2360
|
-
sessionID: session.id,
|
|
2361
|
-
text: prompt
|
|
2362
|
-
});
|
|
2363
|
-
await client.session.wait({ sessionID: session.id }, { signal: AbortSignal.timeout(COMMIT_LIMIT) });
|
|
2364
|
-
const messages = await allMessages(client, session.id);
|
|
2365
|
-
for (const message of messages.toReversed()) {
|
|
2366
|
-
if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
|
|
2367
|
-
if (message.type !== "assistant") continue;
|
|
2368
|
-
const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
|
|
2369
|
-
if (text !== "") return { message: text };
|
|
2370
|
-
}
|
|
2371
|
-
throw new Error("OpenCode did not reply");
|
|
2372
|
-
} finally {
|
|
2373
|
-
await client.session.remove({ sessionID: session.id });
|
|
2374
|
-
temporary.delete(session.id);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
async function contextOf(client, cwd, messages) {
|
|
2378
|
-
const last = messages.findLast((message) => message.type === "assistant" && message.tokens !== void 0);
|
|
2379
|
-
if (last?.type !== "assistant" || last.tokens === void 0) return null;
|
|
2380
|
-
const tokens = last.tokens;
|
|
2381
|
-
const model = (await client.model.list({ location: { directory: cwd } })).data.find((item) => item.providerID === last.model.providerID && item.id === last.model.id);
|
|
2382
|
-
return {
|
|
2383
|
-
tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write,
|
|
2384
|
-
window: model?.limit.context ?? null
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
function modelRef(value, effort) {
|
|
2388
|
-
const index = value.indexOf("/");
|
|
2389
|
-
if (index < 0) throw new Error(`${value} is not an OpenCode model`);
|
|
2390
|
-
return {
|
|
2391
|
-
providerID: value.slice(0, index),
|
|
2392
|
-
id: value.slice(index + 1),
|
|
2393
|
-
...effort === null ? {} : { variant: effort }
|
|
2394
|
-
};
|
|
2395
|
-
}
|
|
2396
|
-
async function allMessages(client, sessionId) {
|
|
2397
|
-
const messages = [];
|
|
2398
|
-
let cursor = null;
|
|
2399
|
-
do {
|
|
2400
|
-
const page = await client.message.list(cursor === null ? {
|
|
2401
|
-
sessionID: sessionId,
|
|
2402
|
-
limit: PAGE,
|
|
2403
|
-
order: "asc"
|
|
2404
|
-
} : {
|
|
2405
|
-
sessionID: sessionId,
|
|
2406
|
-
cursor
|
|
2407
|
-
});
|
|
2408
|
-
messages.push(...page.data);
|
|
2409
|
-
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2410
|
-
} while (cursor !== null);
|
|
2411
|
-
return messages;
|
|
2412
|
-
}
|
|
2413
|
-
function openCodeRun(sessionId) {
|
|
2414
|
-
const run = runs.get(sessionId);
|
|
2415
|
-
return run?.provider === "opencode" ? run : void 0;
|
|
2416
|
-
}
|
|
2417
|
-
//#endregion
|
|
2418
|
-
//#region src/lib/opencode/run.ts
|
|
2419
|
-
const IDLE_LIMIT$1 = 18e5;
|
|
2420
|
-
const RECENT = 30;
|
|
2421
|
-
const replies = {
|
|
2422
|
-
allow: "once",
|
|
2423
|
-
always: "always",
|
|
2424
|
-
deny: "reject"
|
|
2011
|
+
//#region src/lib/opencode/run.ts
|
|
2012
|
+
const IDLE_LIMIT$1 = 18e5;
|
|
2013
|
+
const RECENT = 30;
|
|
2014
|
+
const replies = {
|
|
2015
|
+
allow: "once",
|
|
2016
|
+
always: "always",
|
|
2017
|
+
deny: "reject"
|
|
2425
2018
|
};
|
|
2426
2019
|
var OpenCodeRun = class OpenCodeRun {
|
|
2427
2020
|
provider = "opencode";
|
|
@@ -2616,187 +2209,698 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2616
2209
|
context
|
|
2617
2210
|
});
|
|
2618
2211
|
}
|
|
2619
|
-
async startTurn(prompt) {
|
|
2620
|
-
clearTimeout(this.idleTimer);
|
|
2621
|
-
this.running = true;
|
|
2622
|
-
this.prompted = true;
|
|
2623
|
-
broadcastActivity();
|
|
2624
|
-
try {
|
|
2625
|
-
await this.apply();
|
|
2626
|
-
await this.send(prompt);
|
|
2627
|
-
} catch (error) {
|
|
2628
|
-
this.running = false;
|
|
2629
|
-
broadcastActivity();
|
|
2630
|
-
this.armIdleTimer();
|
|
2631
|
-
throw error;
|
|
2632
|
-
}
|
|
2212
|
+
async startTurn(prompt) {
|
|
2213
|
+
clearTimeout(this.idleTimer);
|
|
2214
|
+
this.running = true;
|
|
2215
|
+
this.prompted = true;
|
|
2216
|
+
broadcastActivity();
|
|
2217
|
+
try {
|
|
2218
|
+
await this.apply();
|
|
2219
|
+
await this.send(prompt);
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
this.running = false;
|
|
2222
|
+
broadcastActivity();
|
|
2223
|
+
this.armIdleTimer();
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async send(prompt) {
|
|
2228
|
+
const client = await openCode();
|
|
2229
|
+
const sessionID = this.sessionId;
|
|
2230
|
+
const location = { directory: this.cwd };
|
|
2231
|
+
const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
|
|
2232
|
+
if (match !== null) {
|
|
2233
|
+
if (match[1] === "compact" && prompt.images.length === 0) {
|
|
2234
|
+
await client.session.compact({ sessionID });
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
|
|
2238
|
+
await client.session.command({
|
|
2239
|
+
sessionID,
|
|
2240
|
+
name: match[1],
|
|
2241
|
+
text: match[2],
|
|
2242
|
+
files: files(prompt)
|
|
2243
|
+
});
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
2247
|
+
if (skill !== void 0) {
|
|
2248
|
+
await client.session.prompt({
|
|
2249
|
+
sessionID,
|
|
2250
|
+
text: match[2],
|
|
2251
|
+
files: files(prompt),
|
|
2252
|
+
skills: [{ id: skill.id }]
|
|
2253
|
+
});
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
await client.session.prompt({
|
|
2258
|
+
sessionID,
|
|
2259
|
+
text: prompt.text,
|
|
2260
|
+
files: files(prompt)
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
async apply() {
|
|
2264
|
+
const client = await openCode();
|
|
2265
|
+
const settings = this.settings;
|
|
2266
|
+
const model = settings.model ?? this.applied.model;
|
|
2267
|
+
if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await client.session.switchModel({
|
|
2268
|
+
sessionID: this.sessionId,
|
|
2269
|
+
model: modelRef(model, settings.effort)
|
|
2270
|
+
});
|
|
2271
|
+
if (settings.mode !== null && settings.mode !== this.applied.mode) await client.session.switchAgent({
|
|
2272
|
+
sessionID: this.sessionId,
|
|
2273
|
+
agent: settings.mode
|
|
2274
|
+
});
|
|
2275
|
+
this.applied = {
|
|
2276
|
+
...settings,
|
|
2277
|
+
model,
|
|
2278
|
+
mode: settings.mode ?? this.applied.mode
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
ask(permission) {
|
|
2282
|
+
const source = permission.source;
|
|
2283
|
+
const item = source === void 0 ? void 0 : this.items.get(`${source.messageID}:${source.id}`);
|
|
2284
|
+
this.addPending({
|
|
2285
|
+
kind: "permission",
|
|
2286
|
+
request: {
|
|
2287
|
+
id: permission.id,
|
|
2288
|
+
kind: "approval",
|
|
2289
|
+
title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
|
|
2290
|
+
canAlwaysAllow: (permission.save ?? []).length > 0,
|
|
2291
|
+
item: item ?? {
|
|
2292
|
+
id: permission.id,
|
|
2293
|
+
parent: null,
|
|
2294
|
+
time: Date.now(),
|
|
2295
|
+
kind: "tool",
|
|
2296
|
+
name: permission.action,
|
|
2297
|
+
input: { resources: permission.resources },
|
|
2298
|
+
output: "",
|
|
2299
|
+
status: "running"
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
askForm(form) {
|
|
2305
|
+
this.addPending({
|
|
2306
|
+
kind: "form",
|
|
2307
|
+
request: {
|
|
2308
|
+
id: form.id,
|
|
2309
|
+
kind: "question",
|
|
2310
|
+
questions: formQuestions(form)
|
|
2311
|
+
},
|
|
2312
|
+
form
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
addPending(pending) {
|
|
2316
|
+
this.pending.set(pending.request.id, pending);
|
|
2317
|
+
publish$2(this.sessionId, {
|
|
2318
|
+
type: "request",
|
|
2319
|
+
sessionId: this.sessionId,
|
|
2320
|
+
request: pending.request
|
|
2321
|
+
});
|
|
2322
|
+
broadcastActivity();
|
|
2323
|
+
scheduleAutoResolve(this.sessionId, pending.request);
|
|
2324
|
+
}
|
|
2325
|
+
finishTurn() {
|
|
2326
|
+
this.partial = "";
|
|
2327
|
+
const [next, ...rest] = this.queue;
|
|
2328
|
+
if (next !== void 0) {
|
|
2329
|
+
this.queue = rest;
|
|
2330
|
+
this.publishQueue();
|
|
2331
|
+
this.startTurn(next.prompt).catch((error) => this.fail(error));
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
this.running = false;
|
|
2335
|
+
broadcastActivity();
|
|
2336
|
+
this.armIdleTimer();
|
|
2337
|
+
}
|
|
2338
|
+
settle(requestId) {
|
|
2339
|
+
if (!this.pending.delete(requestId)) return;
|
|
2340
|
+
publish$2(this.sessionId, {
|
|
2341
|
+
type: "requestDone",
|
|
2342
|
+
sessionId: this.sessionId,
|
|
2343
|
+
requestId
|
|
2344
|
+
});
|
|
2345
|
+
broadcastActivity();
|
|
2346
|
+
}
|
|
2347
|
+
put(items) {
|
|
2348
|
+
if (items.length === 0) return;
|
|
2349
|
+
for (const item of items) this.items.set(item.id, item);
|
|
2350
|
+
if (items.some((item) => item.kind === "assistant")) this.partial = "";
|
|
2351
|
+
publish$2(this.sessionId, {
|
|
2352
|
+
type: "items",
|
|
2353
|
+
sessionId: this.sessionId,
|
|
2354
|
+
items
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
fail(error) {
|
|
2358
|
+
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2359
|
+
}
|
|
2360
|
+
armIdleTimer() {
|
|
2361
|
+
clearTimeout(this.idleTimer);
|
|
2362
|
+
if (this.running) return;
|
|
2363
|
+
this.idleTimer = setTimeout(() => {
|
|
2364
|
+
if (!this.running && this.pending.size === 0) this.stop();
|
|
2365
|
+
}, IDLE_LIMIT$1);
|
|
2366
|
+
}
|
|
2367
|
+
end() {
|
|
2368
|
+
clearTimeout(this.idleTimer);
|
|
2369
|
+
clearTimeout(this.refreshTimer);
|
|
2370
|
+
for (const requestId of [...this.pending.keys()]) this.settle(requestId);
|
|
2371
|
+
this.running = false;
|
|
2372
|
+
broadcastActivity();
|
|
2373
|
+
this.close();
|
|
2374
|
+
}
|
|
2375
|
+
publishQueue() {
|
|
2376
|
+
publish$2(this.sessionId, {
|
|
2377
|
+
type: "queue",
|
|
2378
|
+
sessionId: this.sessionId,
|
|
2379
|
+
queue: this.queue
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
function sessionSettings(session, settings) {
|
|
2384
|
+
const variant = session.model?.variant;
|
|
2385
|
+
return {
|
|
2386
|
+
...settings,
|
|
2387
|
+
model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
|
|
2388
|
+
effort: variant === void 0 || variant === "default" ? null : variant,
|
|
2389
|
+
mode: session.agent ?? null
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function files(prompt) {
|
|
2393
|
+
return prompt.images.map((image) => ({ uri: `data:${image.mediaType};base64,${image.data}` }));
|
|
2394
|
+
}
|
|
2395
|
+
//#endregion
|
|
2396
|
+
//#region src/storage/db.ts
|
|
2397
|
+
const file = path.join(home, "offsite.db");
|
|
2398
|
+
closeSync(openSync(file, "a", 384));
|
|
2399
|
+
chmodSync(file, 384);
|
|
2400
|
+
const client = new DatabaseSync(file);
|
|
2401
|
+
client.exec(`
|
|
2402
|
+
PRAGMA journal_mode = WAL;
|
|
2403
|
+
PRAGMA busy_timeout = 5000;
|
|
2404
|
+
PRAGMA synchronous = NORMAL;
|
|
2405
|
+
PRAGMA foreign_keys = ON;
|
|
2406
|
+
`);
|
|
2407
|
+
const db = drizzle({ client });
|
|
2408
|
+
migrate(db, { migrationsFolder: path.join(serverDir, "drizzle") });
|
|
2409
|
+
//#endregion
|
|
2410
|
+
//#region src/storage/schema.ts
|
|
2411
|
+
const projects$2 = snakeCase.table("projects", {
|
|
2412
|
+
id: text().primaryKey(),
|
|
2413
|
+
path: text().notNull().unique(),
|
|
2414
|
+
name: text().notNull(),
|
|
2415
|
+
logo: text()
|
|
2416
|
+
});
|
|
2417
|
+
const sessions$1 = snakeCase.table("sessions", {
|
|
2418
|
+
sessionId: text().primaryKey(),
|
|
2419
|
+
interactedAt: integer(),
|
|
2420
|
+
status: text({ enum: [
|
|
2421
|
+
"active",
|
|
2422
|
+
"pinned",
|
|
2423
|
+
"minimized"
|
|
2424
|
+
] }).notNull()
|
|
2425
|
+
});
|
|
2426
|
+
const tokens = snakeCase.table("tokens", {
|
|
2427
|
+
hash: text().primaryKey(),
|
|
2428
|
+
name: text(),
|
|
2429
|
+
createdAt: integer().notNull(),
|
|
2430
|
+
lastUsedAt: integer()
|
|
2431
|
+
});
|
|
2432
|
+
const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
2433
|
+
endpoint: text().primaryKey(),
|
|
2434
|
+
p256dh: text().notNull(),
|
|
2435
|
+
auth: text().notNull(),
|
|
2436
|
+
publicKey: text().notNull(),
|
|
2437
|
+
privateKey: text().notNull(),
|
|
2438
|
+
machine: text().notNull(),
|
|
2439
|
+
server: text().notNull()
|
|
2440
|
+
});
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/services/projects.ts
|
|
2443
|
+
const LOGO_TYPES = [
|
|
2444
|
+
"image/png",
|
|
2445
|
+
"image/jpeg",
|
|
2446
|
+
"image/gif",
|
|
2447
|
+
"image/webp",
|
|
2448
|
+
"image/svg+xml",
|
|
2449
|
+
"image/x-icon",
|
|
2450
|
+
"image/vnd.microsoft.icon"
|
|
2451
|
+
];
|
|
2452
|
+
const LOGO_LIMIT = 524288;
|
|
2453
|
+
function projects$1() {
|
|
2454
|
+
return db.select().from(projects$2).all();
|
|
2455
|
+
}
|
|
2456
|
+
function projectPath(id) {
|
|
2457
|
+
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
2458
|
+
if (project === void 0) throw new Error("That project was removed");
|
|
2459
|
+
return project.path;
|
|
2460
|
+
}
|
|
2461
|
+
async function addProject(input) {
|
|
2462
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2463
|
+
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
2464
|
+
db.insert(projects$2).values({
|
|
2465
|
+
id: randomUUID(),
|
|
2466
|
+
path: folder,
|
|
2467
|
+
name: path.basename(folder) || folder,
|
|
2468
|
+
logo: null
|
|
2469
|
+
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
2470
|
+
publish$1();
|
|
2471
|
+
const project = projects$1().find((item) => item.path === folder);
|
|
2472
|
+
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
2473
|
+
return project;
|
|
2474
|
+
}
|
|
2475
|
+
function updateProject(id, patch) {
|
|
2476
|
+
if (patch.name !== void 0) {
|
|
2477
|
+
const name = patch.name.trim();
|
|
2478
|
+
if (name === "") throw new Error("Project name cannot be empty");
|
|
2479
|
+
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
2480
|
+
}
|
|
2481
|
+
if (patch.logo !== void 0) {
|
|
2482
|
+
if (patch.logo !== null) checkLogo(patch.logo);
|
|
2483
|
+
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
2633
2484
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2485
|
+
publish$1();
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
function removeProject(id) {
|
|
2489
|
+
db.delete(projects$2).where(eq(projects$2.id, id)).run();
|
|
2490
|
+
publish$1();
|
|
2491
|
+
}
|
|
2492
|
+
async function directories(input) {
|
|
2493
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2494
|
+
const entries = await readdir(folder, { withFileTypes: true });
|
|
2495
|
+
const parent = path.dirname(folder);
|
|
2496
|
+
return {
|
|
2497
|
+
path: folder,
|
|
2498
|
+
parent: parent === folder ? null : parent,
|
|
2499
|
+
directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
2500
|
+
name: entry.name,
|
|
2501
|
+
path: path.join(folder, entry.name)
|
|
2502
|
+
})).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function publish$1() {
|
|
2506
|
+
broadcast({
|
|
2507
|
+
type: "projects",
|
|
2508
|
+
projects: projects$1()
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
function checkLogo(logo) {
|
|
2512
|
+
const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
|
|
2513
|
+
if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
|
|
2514
|
+
if (logo.length > LOGO_LIMIT) throw new Error("Use an image smaller than 384 KB");
|
|
2515
|
+
}
|
|
2516
|
+
function expandHome(input) {
|
|
2517
|
+
if (input === "~") return os.homedir();
|
|
2518
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
2519
|
+
return input;
|
|
2520
|
+
}
|
|
2521
|
+
//#endregion
|
|
2522
|
+
//#region src/services/sessions.ts
|
|
2523
|
+
const day = 864e5;
|
|
2524
|
+
const LIST_TIMEOUT = 1e4;
|
|
2525
|
+
const REFRESH_DELAY = 1e3;
|
|
2526
|
+
const allProviders = [
|
|
2527
|
+
"claude",
|
|
2528
|
+
"codex",
|
|
2529
|
+
"opencode"
|
|
2530
|
+
];
|
|
2531
|
+
const listed = {
|
|
2532
|
+
claude: [],
|
|
2533
|
+
codex: [],
|
|
2534
|
+
opencode: []
|
|
2535
|
+
};
|
|
2536
|
+
const statuses = {
|
|
2537
|
+
claude: {
|
|
2538
|
+
provider: "claude",
|
|
2539
|
+
state: "loading",
|
|
2540
|
+
error: null
|
|
2541
|
+
},
|
|
2542
|
+
codex: {
|
|
2543
|
+
provider: "codex",
|
|
2544
|
+
state: "loading",
|
|
2545
|
+
error: null
|
|
2546
|
+
},
|
|
2547
|
+
opencode: {
|
|
2548
|
+
provider: "opencode",
|
|
2549
|
+
state: "loading",
|
|
2550
|
+
error: null
|
|
2669
2551
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
mode: settings.mode ?? this.applied.mode
|
|
2552
|
+
};
|
|
2553
|
+
const refreshing = /* @__PURE__ */ new Map();
|
|
2554
|
+
const timers = /* @__PURE__ */ new Map();
|
|
2555
|
+
const providers = /* @__PURE__ */ new Map();
|
|
2556
|
+
let watchers = [];
|
|
2557
|
+
function sessions() {
|
|
2558
|
+
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
2559
|
+
const known = new Set(projects$1().map((project) => project.id));
|
|
2560
|
+
return allProviders.flatMap((provider) => listed[provider].filter((item) => known.has(item.projectId)).map((item) => {
|
|
2561
|
+
const record = records.get(item.id);
|
|
2562
|
+
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
2563
|
+
return {
|
|
2564
|
+
...item,
|
|
2565
|
+
updatedAt,
|
|
2566
|
+
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
2686
2567
|
};
|
|
2568
|
+
}));
|
|
2569
|
+
}
|
|
2570
|
+
function providerStatuses() {
|
|
2571
|
+
return allProviders.map((provider) => statuses[provider]);
|
|
2572
|
+
}
|
|
2573
|
+
function listedUpdatedAt(provider, sessionId) {
|
|
2574
|
+
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
2575
|
+
}
|
|
2576
|
+
function providerOf(sessionId) {
|
|
2577
|
+
const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
2578
|
+
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
2579
|
+
return provider;
|
|
2580
|
+
}
|
|
2581
|
+
function recordInteraction(id) {
|
|
2582
|
+
db.insert(sessions$1).values({
|
|
2583
|
+
sessionId: id,
|
|
2584
|
+
interactedAt: Date.now(),
|
|
2585
|
+
status: "active"
|
|
2586
|
+
}).onConflictDoUpdate({
|
|
2587
|
+
target: sessions$1.sessionId,
|
|
2588
|
+
set: {
|
|
2589
|
+
interactedAt: Date.now(),
|
|
2590
|
+
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
2591
|
+
}
|
|
2592
|
+
}).run();
|
|
2593
|
+
}
|
|
2594
|
+
function publishSessions() {
|
|
2595
|
+
broadcast({
|
|
2596
|
+
type: "sessions",
|
|
2597
|
+
sessions: sessions()
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
function refreshAll() {
|
|
2601
|
+
for (const provider of allProviders) refresh(provider);
|
|
2602
|
+
}
|
|
2603
|
+
function refreshSessions(provider) {
|
|
2604
|
+
if (timers.has(provider)) return;
|
|
2605
|
+
timers.set(provider, setTimeout(() => {
|
|
2606
|
+
timers.delete(provider);
|
|
2607
|
+
refresh(provider);
|
|
2608
|
+
}, REFRESH_DELAY));
|
|
2609
|
+
}
|
|
2610
|
+
async function refresh(provider) {
|
|
2611
|
+
const current = refreshing.get(provider);
|
|
2612
|
+
if (current !== void 0) {
|
|
2613
|
+
current.again = true;
|
|
2614
|
+
return;
|
|
2687
2615
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
request: {
|
|
2694
|
-
id: permission.id,
|
|
2695
|
-
kind: "approval",
|
|
2696
|
-
title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
|
|
2697
|
-
canAlwaysAllow: (permission.save ?? []).length > 0,
|
|
2698
|
-
item: item ?? {
|
|
2699
|
-
id: permission.id,
|
|
2700
|
-
parent: null,
|
|
2701
|
-
time: Date.now(),
|
|
2702
|
-
kind: "tool",
|
|
2703
|
-
name: permission.action,
|
|
2704
|
-
input: { resources: permission.resources },
|
|
2705
|
-
output: "",
|
|
2706
|
-
status: "running"
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
});
|
|
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
|
-
});
|
|
2616
|
+
const entry = { again: true };
|
|
2617
|
+
refreshing.set(provider, entry);
|
|
2618
|
+
while (entry.again) {
|
|
2619
|
+
entry.again = false;
|
|
2620
|
+
await list(provider);
|
|
2721
2621
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2622
|
+
refreshing.delete(provider);
|
|
2623
|
+
}
|
|
2624
|
+
async function list(provider) {
|
|
2625
|
+
const paths = new Map(projects$1().map((project) => [project.path, project.id]));
|
|
2626
|
+
const before = statuses[provider];
|
|
2627
|
+
let timer;
|
|
2628
|
+
try {
|
|
2629
|
+
if (!readConfig().enabledProviders[provider]) {
|
|
2630
|
+
listed[provider] = [];
|
|
2631
|
+
statuses[provider] = {
|
|
2632
|
+
provider,
|
|
2633
|
+
state: "disabled",
|
|
2634
|
+
error: null
|
|
2635
|
+
};
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
const found = await Promise.race([find(provider, paths), new Promise((_, reject) => {
|
|
2639
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
|
|
2640
|
+
})]);
|
|
2641
|
+
if (found === null) {
|
|
2642
|
+
listed[provider] = [];
|
|
2643
|
+
statuses[provider] = {
|
|
2644
|
+
provider,
|
|
2645
|
+
state: "missing",
|
|
2646
|
+
error: null
|
|
2647
|
+
};
|
|
2648
|
+
} else {
|
|
2649
|
+
listed[provider] = found;
|
|
2650
|
+
statuses[provider] = {
|
|
2651
|
+
provider,
|
|
2652
|
+
state: "ready",
|
|
2653
|
+
error: null
|
|
2654
|
+
};
|
|
2655
|
+
for (const item of found) providers.set(item.id, provider);
|
|
2656
|
+
}
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2659
|
+
console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
2660
|
+
statuses[provider] = {
|
|
2661
|
+
provider,
|
|
2662
|
+
state: "error",
|
|
2663
|
+
error: message
|
|
2664
|
+
};
|
|
2665
|
+
} finally {
|
|
2666
|
+
clearTimeout(timer);
|
|
2667
|
+
publishSessions();
|
|
2668
|
+
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
2669
|
+
type: "providers",
|
|
2670
|
+
providers: providerStatuses()
|
|
2728
2671
|
});
|
|
2729
|
-
broadcastActivity();
|
|
2730
2672
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2673
|
+
}
|
|
2674
|
+
async function find(provider, paths) {
|
|
2675
|
+
switch (provider) {
|
|
2676
|
+
case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => (await listSessions({
|
|
2677
|
+
dir: cwd,
|
|
2678
|
+
includeWorktrees: false,
|
|
2679
|
+
includeProgrammatic: false
|
|
2680
|
+
})).map((item) => ({
|
|
2681
|
+
id: item.sessionId,
|
|
2682
|
+
provider: "claude",
|
|
2683
|
+
title: item.summary,
|
|
2684
|
+
projectId,
|
|
2685
|
+
branch: item.gitBranch ?? null,
|
|
2686
|
+
updatedAt: item.lastModified
|
|
2687
|
+
}))))).flat();
|
|
2688
|
+
case "codex":
|
|
2689
|
+
if (!codexInstalled()) return null;
|
|
2690
|
+
return (await codexThreads([...paths.keys()])).flatMap((thread) => {
|
|
2691
|
+
const projectId = paths.get(thread.cwd);
|
|
2692
|
+
if (projectId === void 0) return [];
|
|
2693
|
+
return [{
|
|
2694
|
+
id: thread.id,
|
|
2695
|
+
provider: "codex",
|
|
2696
|
+
title: thread.name ?? thread.preview,
|
|
2697
|
+
projectId,
|
|
2698
|
+
branch: thread.gitInfo?.branch ?? null,
|
|
2699
|
+
updatedAt: thread.updatedAt * 1e3
|
|
2700
|
+
}];
|
|
2701
|
+
});
|
|
2702
|
+
case "opencode":
|
|
2703
|
+
if (!openCodeInstalled()) return null;
|
|
2704
|
+
return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
|
|
2705
|
+
const projectId = paths.get(session.location.directory);
|
|
2706
|
+
if (projectId === void 0) return [];
|
|
2707
|
+
return [{
|
|
2708
|
+
id: session.id,
|
|
2709
|
+
provider: "opencode",
|
|
2710
|
+
title: session.title ?? "New session",
|
|
2711
|
+
projectId,
|
|
2712
|
+
branch: null,
|
|
2713
|
+
updatedAt: session.time.updated
|
|
2714
|
+
}];
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function watchSessions() {
|
|
2719
|
+
for (const watcher of watchers) watcher.close();
|
|
2720
|
+
watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
2721
|
+
if (!readConfig().enabledProviders[provider]) return [];
|
|
2722
|
+
try {
|
|
2723
|
+
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
2724
|
+
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
2725
|
+
return [watcher];
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
if (error.code !== "ENOENT") throw error;
|
|
2728
|
+
return [];
|
|
2739
2729
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
async function updateSession(id, patch) {
|
|
2733
|
+
if (patch.title !== void 0) switch (providerOf(id)) {
|
|
2734
|
+
case "claude":
|
|
2735
|
+
await renameSession(id, patch.title);
|
|
2736
|
+
break;
|
|
2737
|
+
case "codex":
|
|
2738
|
+
await renameThread(id, patch.title);
|
|
2739
|
+
break;
|
|
2740
|
+
case "opencode": await renameOpenCode(id, patch.title);
|
|
2743
2741
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2742
|
+
if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2743
|
+
else if (patch.status !== void 0) db.insert(sessions$1).values({
|
|
2744
|
+
sessionId: id,
|
|
2745
|
+
status: patch.status
|
|
2746
|
+
}).onConflictDoUpdate({
|
|
2747
|
+
target: sessions$1.sessionId,
|
|
2748
|
+
set: { status: patch.status }
|
|
2749
|
+
}).run();
|
|
2750
|
+
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
2751
|
+
}
|
|
2752
|
+
async function removeSession(id) {
|
|
2753
|
+
const provider = providerOf(id);
|
|
2754
|
+
const run = runs.get(id);
|
|
2755
|
+
if (run !== void 0) {
|
|
2756
|
+
run.stop();
|
|
2757
|
+
await run.closed;
|
|
2752
2758
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
});
|
|
2759
|
+
switch (provider) {
|
|
2760
|
+
case "claude":
|
|
2761
|
+
await deleteSession(id);
|
|
2762
|
+
break;
|
|
2763
|
+
case "codex":
|
|
2764
|
+
await deleteThread(id);
|
|
2765
|
+
break;
|
|
2766
|
+
case "opencode": await removeOpenCode(id);
|
|
2762
2767
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2768
|
+
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2769
|
+
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
2770
|
+
}
|
|
2771
|
+
async function fork(sessionId, projectId, messageId) {
|
|
2772
|
+
const provider = providerOf(sessionId);
|
|
2773
|
+
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
2774
|
+
providers.set(result.sessionId, provider);
|
|
2775
|
+
recordInteraction(result.sessionId);
|
|
2776
|
+
refreshSessions(provider);
|
|
2777
|
+
return result;
|
|
2778
|
+
}
|
|
2779
|
+
async function forkWith(provider, sessionId, cwd, messageId) {
|
|
2780
|
+
if (provider === "codex") return forkThread(sessionId, messageId);
|
|
2781
|
+
if (provider === "opencode") return forkOpenCode(sessionId, messageId);
|
|
2782
|
+
if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
|
|
2783
|
+
return { sessionId: (await forkSession(sessionId, {
|
|
2784
|
+
dir: cwd,
|
|
2785
|
+
upToMessageId: messageId
|
|
2786
|
+
})).sessionId };
|
|
2787
|
+
}
|
|
2788
|
+
//#endregion
|
|
2789
|
+
//#region src/lib/opencode/client.ts
|
|
2790
|
+
const listChanges = /* @__PURE__ */ new Set([
|
|
2791
|
+
"session.created",
|
|
2792
|
+
"session.renamed",
|
|
2793
|
+
"session.moved",
|
|
2794
|
+
"session.forked",
|
|
2795
|
+
"session.deleted",
|
|
2796
|
+
"session.execution.succeeded",
|
|
2797
|
+
"session.execution.failed",
|
|
2798
|
+
"session.execution.interrupted"
|
|
2799
|
+
]);
|
|
2800
|
+
const SILENCE_LIMIT$1 = 45e3;
|
|
2801
|
+
const RECONNECT_ATTEMPTS = 5;
|
|
2802
|
+
const RECONNECT_DELAY = 2e3;
|
|
2803
|
+
let connection;
|
|
2804
|
+
let listener;
|
|
2805
|
+
function findOpenCode() {
|
|
2806
|
+
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
2807
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
2808
|
+
const candidate = path.join(dir, name);
|
|
2809
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
2765
2810
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
function openCode() {
|
|
2814
|
+
connection ??= start();
|
|
2815
|
+
return connection;
|
|
2816
|
+
}
|
|
2817
|
+
function disconnectOpenCode() {
|
|
2818
|
+
const controller = listener;
|
|
2819
|
+
listener = void 0;
|
|
2820
|
+
connection = void 0;
|
|
2821
|
+
controller?.abort();
|
|
2822
|
+
}
|
|
2823
|
+
async function start() {
|
|
2824
|
+
try {
|
|
2825
|
+
const client = await connect();
|
|
2826
|
+
listen(client);
|
|
2827
|
+
return client;
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
connection = void 0;
|
|
2830
|
+
throw error;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
async function connect() {
|
|
2834
|
+
const endpoint = await ensure();
|
|
2835
|
+
return OpenCode.make({
|
|
2836
|
+
baseUrl: endpoint.url,
|
|
2837
|
+
headers: headers(endpoint)
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
async function listen(client) {
|
|
2841
|
+
const controller = new AbortController();
|
|
2842
|
+
listener = controller;
|
|
2843
|
+
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
2844
|
+
try {
|
|
2845
|
+
for await (const event of client.event.subscribe({
|
|
2846
|
+
signal: controller.signal,
|
|
2847
|
+
onActivity: () => silence.refresh()
|
|
2848
|
+
})) route(event);
|
|
2849
|
+
console.warn("the OpenCode event stream ended");
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
if (listener !== controller) return;
|
|
2852
|
+
console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
|
|
2853
|
+
} finally {
|
|
2854
|
+
clearTimeout(silence);
|
|
2772
2855
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2856
|
+
if (listener !== controller) return;
|
|
2857
|
+
connection = reconnect();
|
|
2858
|
+
connection.catch(() => void 0);
|
|
2859
|
+
}
|
|
2860
|
+
async function reconnect() {
|
|
2861
|
+
let failure;
|
|
2862
|
+
for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
|
|
2863
|
+
await setTimeout$1(RECONNECT_DELAY);
|
|
2864
|
+
let client;
|
|
2865
|
+
try {
|
|
2866
|
+
client = await connect();
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
failure = error;
|
|
2869
|
+
continue;
|
|
2870
|
+
}
|
|
2871
|
+
listen(client);
|
|
2872
|
+
await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
|
|
2873
|
+
return client;
|
|
2780
2874
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2875
|
+
console.warn(`could not reconnect to OpenCode: ${failure}`);
|
|
2876
|
+
connection = void 0;
|
|
2877
|
+
for (const run of openCodeRuns()) run.exited();
|
|
2878
|
+
refreshSessions("opencode");
|
|
2879
|
+
throw failure;
|
|
2880
|
+
}
|
|
2881
|
+
async function catchUp(client) {
|
|
2882
|
+
const active = await client.session.active();
|
|
2883
|
+
for (const run of openCodeRuns()) {
|
|
2884
|
+
const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
|
|
2885
|
+
run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
|
|
2787
2886
|
}
|
|
2788
|
-
|
|
2789
|
-
function sessionSettings(session, settings) {
|
|
2790
|
-
const variant = session.model?.variant;
|
|
2791
|
-
return {
|
|
2792
|
-
...settings,
|
|
2793
|
-
model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
|
|
2794
|
-
effort: variant === void 0 || variant === "default" ? null : variant,
|
|
2795
|
-
mode: session.agent ?? null
|
|
2796
|
-
};
|
|
2887
|
+
refreshSessions("opencode");
|
|
2797
2888
|
}
|
|
2798
|
-
function
|
|
2799
|
-
|
|
2889
|
+
function route(event) {
|
|
2890
|
+
if (listChanges.has(event.type)) refreshSessions("opencode");
|
|
2891
|
+
const sessionId = sessionOf(event);
|
|
2892
|
+
if (sessionId === null) return;
|
|
2893
|
+
const run = runs.get(sessionId);
|
|
2894
|
+
if (run?.provider === "opencode") run.receive(event);
|
|
2895
|
+
}
|
|
2896
|
+
function sessionOf(event) {
|
|
2897
|
+
if (event.type === "form.created") return event.data.form.sessionID;
|
|
2898
|
+
const data = "data" in event ? event.data : null;
|
|
2899
|
+
if (isRecord(data) && typeof data.sessionID === "string") return data.sessionID;
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
function openCodeRuns() {
|
|
2903
|
+
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
2800
2904
|
}
|
|
2801
2905
|
//#endregion
|
|
2802
2906
|
//#region src/services/accounts.ts
|
|
@@ -2809,7 +2913,36 @@ function providerName(provider) {
|
|
|
2809
2913
|
return providerNames[provider];
|
|
2810
2914
|
}
|
|
2811
2915
|
function storedAccounts() {
|
|
2812
|
-
return readJson("accounts.json") ?? []
|
|
2916
|
+
return (readJson("accounts.json") ?? []).map((item) => ({
|
|
2917
|
+
id: item.id,
|
|
2918
|
+
provider: item.provider,
|
|
2919
|
+
name: item.name,
|
|
2920
|
+
configDir: item.configDir,
|
|
2921
|
+
enabled: item.enabled ?? true,
|
|
2922
|
+
hiddenModels: item.hiddenModels ?? {}
|
|
2923
|
+
}));
|
|
2924
|
+
}
|
|
2925
|
+
function assertEnabled(provider, accountId) {
|
|
2926
|
+
if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
|
|
2927
|
+
if (provider === "opencode") return;
|
|
2928
|
+
if (!storedAccount(provider, accountId).enabled) throw new Error("That account is disabled in Settings");
|
|
2929
|
+
}
|
|
2930
|
+
async function stopProvider(provider) {
|
|
2931
|
+
const stopping = [...runs.values()].filter((run) => run.provider === provider);
|
|
2932
|
+
for (const run of stopping) run.stop();
|
|
2933
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2934
|
+
if (provider === "codex") stopAppServers();
|
|
2935
|
+
if (provider === "opencode") disconnectOpenCode();
|
|
2936
|
+
if (provider === "claude") {
|
|
2937
|
+
for (const account of storedAccounts()) if (account.provider === "claude") forgetClaude(account.id);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
async function stopAccount(account) {
|
|
2941
|
+
const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
|
|
2942
|
+
for (const run of stopping) run.stop();
|
|
2943
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2944
|
+
if (account.provider === "codex") stopAppServer(account.configDir);
|
|
2945
|
+
if (account.provider === "claude") forgetClaude(account.id);
|
|
2813
2946
|
}
|
|
2814
2947
|
function storedAccount(provider, accountId) {
|
|
2815
2948
|
if (accountId === null) throw new Error(`Add a ${providerName(provider)} account in Settings → Accounts`);
|
|
@@ -2820,49 +2953,92 @@ function storedAccount(provider, accountId) {
|
|
|
2820
2953
|
}
|
|
2821
2954
|
async function accounts() {
|
|
2822
2955
|
const stored = await Promise.all(storedAccounts().map((account) => {
|
|
2956
|
+
if (!account.enabled || !readConfig().enabledProviders[account.provider]) return disabledAccount(account);
|
|
2823
2957
|
switch (account.provider) {
|
|
2824
2958
|
case "claude": return claudeAccount(account);
|
|
2825
2959
|
case "codex": return codexAccount(account);
|
|
2826
|
-
case "opencode": throw new Error("OpenCode has no stored accounts");
|
|
2827
2960
|
}
|
|
2828
2961
|
}));
|
|
2829
|
-
return
|
|
2962
|
+
return {
|
|
2963
|
+
claudeAccounts: stored.filter((item) => item.provider === "claude"),
|
|
2964
|
+
codexAccounts: stored.filter((item) => item.provider === "codex"),
|
|
2965
|
+
openCode: { available: openCodeInstalled() && readConfig().enabledProviders.opencode }
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function disabledAccount(account) {
|
|
2969
|
+
return {
|
|
2970
|
+
...account,
|
|
2971
|
+
email: null,
|
|
2972
|
+
plan: null,
|
|
2973
|
+
error: null,
|
|
2974
|
+
limits: [],
|
|
2975
|
+
linked: true,
|
|
2976
|
+
valid: false
|
|
2977
|
+
};
|
|
2830
2978
|
}
|
|
2831
2979
|
async function publishAccounts() {
|
|
2832
2980
|
try {
|
|
2833
2981
|
broadcast({
|
|
2834
2982
|
type: "accounts",
|
|
2835
|
-
|
|
2983
|
+
...await accounts()
|
|
2836
2984
|
});
|
|
2837
2985
|
} catch (error) {
|
|
2838
2986
|
console.warn(`could not list accounts: ${error}`);
|
|
2839
2987
|
}
|
|
2840
2988
|
}
|
|
2841
|
-
function capabilities(accountId, cwd) {
|
|
2842
|
-
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2989
|
+
async function capabilities(provider, accountId, cwd) {
|
|
2990
|
+
assertEnabled(provider, accountId);
|
|
2991
|
+
const result = await providerCapabilities(provider, accountId, cwd);
|
|
2992
|
+
const settings = readConfig().models[provider];
|
|
2993
|
+
const overrides = provider === "opencode" ? {} : storedAccount(provider, accountId).hiddenModels;
|
|
2994
|
+
return {
|
|
2995
|
+
...result,
|
|
2996
|
+
defaultModel: defaultModels[provider],
|
|
2997
|
+
models: result.models.map((model) => {
|
|
2998
|
+
const deprecated = deprecatedModels[provider].includes(model.value);
|
|
2999
|
+
const setting = settings[model.value] ?? {
|
|
3000
|
+
hidden: deprecated,
|
|
3001
|
+
effort: null
|
|
3002
|
+
};
|
|
3003
|
+
return {
|
|
3004
|
+
...model,
|
|
3005
|
+
deprecated,
|
|
3006
|
+
hidden: overrides[model.value] ?? setting.hidden,
|
|
3007
|
+
effort: setting.effort
|
|
3008
|
+
};
|
|
3009
|
+
})
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
function providerCapabilities(provider, accountId, cwd) {
|
|
3013
|
+
switch (provider) {
|
|
3014
|
+
case "claude": return claudeCapabilities(storedAccount(provider, accountId), cwd);
|
|
3015
|
+
case "codex": return codexCapabilities(storedAccount(provider, accountId), cwd);
|
|
3016
|
+
case "opencode":
|
|
3017
|
+
if (accountId !== null) throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
3018
|
+
return openCodeCapabilities(cwd);
|
|
2849
3019
|
}
|
|
2850
3020
|
}
|
|
2851
3021
|
function addAccount(provider, name, configDir) {
|
|
2852
|
-
if (provider === "opencode") throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
2853
3022
|
saveAccounts([...storedAccounts(), {
|
|
2854
3023
|
id: randomUUID(),
|
|
2855
3024
|
provider,
|
|
2856
|
-
...validAccount(name, configDir)
|
|
3025
|
+
...validAccount(name, configDir),
|
|
3026
|
+
enabled: true,
|
|
3027
|
+
hiddenModels: {}
|
|
2857
3028
|
}]);
|
|
2858
3029
|
}
|
|
2859
|
-
function updateAccount(
|
|
2860
|
-
const
|
|
2861
|
-
|
|
3030
|
+
async function updateAccount(update) {
|
|
3031
|
+
const before = storedAccounts().find((item) => item.id === update.id);
|
|
3032
|
+
if (before === void 0) throw new Error("That account was removed");
|
|
3033
|
+
const account = validAccount(update.name, update.configDir);
|
|
3034
|
+
saveAccounts(storedAccounts().map((item) => item.id === update.id ? {
|
|
2862
3035
|
...item,
|
|
2863
|
-
...account
|
|
3036
|
+
...account,
|
|
3037
|
+
enabled: update.enabled,
|
|
3038
|
+
hiddenModels: update.hiddenModels
|
|
2864
3039
|
} : item));
|
|
2865
|
-
forgetClaude(id);
|
|
3040
|
+
forgetClaude(update.id);
|
|
3041
|
+
if (before.enabled && !update.enabled) await stopAccount(before);
|
|
2866
3042
|
}
|
|
2867
3043
|
function removeAccount(id) {
|
|
2868
3044
|
saveAccounts(storedAccounts().filter((item) => item.id !== id));
|
|
@@ -2885,81 +3061,6 @@ function saveAccounts(items) {
|
|
|
2885
3061
|
writeJson("accounts.json", items);
|
|
2886
3062
|
}
|
|
2887
3063
|
//#endregion
|
|
2888
|
-
//#region src/lib/claude/content.ts
|
|
2889
|
-
function toMessage(type, item, time) {
|
|
2890
|
-
return {
|
|
2891
|
-
type,
|
|
2892
|
-
uuid: item.uuid,
|
|
2893
|
-
time,
|
|
2894
|
-
content: isRecord(item.message) ? toBlocks(item.message.content, answersOf(item.tool_use_result)) : [],
|
|
2895
|
-
parent: item.parent_tool_use_id
|
|
2896
|
-
};
|
|
2897
|
-
}
|
|
2898
|
-
function contextTokens(message) {
|
|
2899
|
-
if (!isRecord(message) || !isRecord(message.usage)) return null;
|
|
2900
|
-
const usage = message.usage;
|
|
2901
|
-
return [
|
|
2902
|
-
usage.input_tokens,
|
|
2903
|
-
usage.cache_creation_input_tokens,
|
|
2904
|
-
usage.cache_read_input_tokens,
|
|
2905
|
-
usage.output_tokens
|
|
2906
|
-
].reduce((total, value) => total + (typeof value === "number" ? value : 0), 0);
|
|
2907
|
-
}
|
|
2908
|
-
function answersOf(output) {
|
|
2909
|
-
if (!isRecord(output) || !isRecord(output.answers)) return null;
|
|
2910
|
-
const answers = {};
|
|
2911
|
-
for (const [question, answer] of Object.entries(output.answers)) {
|
|
2912
|
-
if (typeof answer !== "string") return null;
|
|
2913
|
-
answers[question] = answer;
|
|
2914
|
-
}
|
|
2915
|
-
return answers;
|
|
2916
|
-
}
|
|
2917
|
-
function toBlocks(content, answers) {
|
|
2918
|
-
if (typeof content === "string") return [{
|
|
2919
|
-
type: "text",
|
|
2920
|
-
text: content
|
|
2921
|
-
}];
|
|
2922
|
-
if (!Array.isArray(content)) return [];
|
|
2923
|
-
return content.flatMap((block) => {
|
|
2924
|
-
if (!isRecord(block)) return [];
|
|
2925
|
-
switch (block.type) {
|
|
2926
|
-
case "text": return typeof block.text === "string" && !block.text.startsWith("<system-reminder>") && !block.text.startsWith("<local-command-caveat>") ? [{
|
|
2927
|
-
type: "text",
|
|
2928
|
-
text: block.text
|
|
2929
|
-
}] : [];
|
|
2930
|
-
case "thinking": return typeof block.thinking === "string" && block.thinking !== "" ? [{
|
|
2931
|
-
type: "thinking",
|
|
2932
|
-
thinking: block.thinking
|
|
2933
|
-
}] : [];
|
|
2934
|
-
case "image": {
|
|
2935
|
-
const source = block.source;
|
|
2936
|
-
return isRecord(source) && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string" ? [{
|
|
2937
|
-
type: "image",
|
|
2938
|
-
mediaType: source.media_type,
|
|
2939
|
-
data: source.data
|
|
2940
|
-
}] : [];
|
|
2941
|
-
}
|
|
2942
|
-
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && isRecord(block.input) ? [{
|
|
2943
|
-
type: "tool_use",
|
|
2944
|
-
id: block.id,
|
|
2945
|
-
name: block.name,
|
|
2946
|
-
input: block.input
|
|
2947
|
-
}] : [];
|
|
2948
|
-
case "tool_result": return typeof block.tool_use_id === "string" ? [{
|
|
2949
|
-
type: "tool_result",
|
|
2950
|
-
toolUseId: block.tool_use_id,
|
|
2951
|
-
content: toBlocks(block.content, null).filter((item) => item.type === "text"),
|
|
2952
|
-
isError: block.is_error === true,
|
|
2953
|
-
answers
|
|
2954
|
-
}] : [];
|
|
2955
|
-
default: return [];
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
}
|
|
2959
|
-
function isRecord(value) {
|
|
2960
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2961
|
-
}
|
|
2962
|
-
//#endregion
|
|
2963
3064
|
//#region src/lib/claude/claude.ts
|
|
2964
3065
|
const efforts = [
|
|
2965
3066
|
"low",
|
|
@@ -3002,13 +3103,15 @@ function claudeMode(value) {
|
|
|
3002
3103
|
}
|
|
3003
3104
|
function claudeOptions(accountId, cwd) {
|
|
3004
3105
|
const account = storedAccount("claude", accountId);
|
|
3106
|
+
const env = {
|
|
3107
|
+
...process.env,
|
|
3108
|
+
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
3109
|
+
CLAUDE_CONFIG_DIR: account.configDir
|
|
3110
|
+
};
|
|
3111
|
+
if (path.resolve(account.configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
|
|
3005
3112
|
return {
|
|
3006
3113
|
cwd,
|
|
3007
|
-
env
|
|
3008
|
-
...process.env,
|
|
3009
|
-
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
3010
|
-
CLAUDE_CONFIG_DIR: account.configDir
|
|
3011
|
-
},
|
|
3114
|
+
env,
|
|
3012
3115
|
pathToClaudeCodeExecutable: executable$1,
|
|
3013
3116
|
systemPrompt: {
|
|
3014
3117
|
type: "preset",
|
|
@@ -3017,7 +3120,7 @@ function claudeOptions(accountId, cwd) {
|
|
|
3017
3120
|
hooks: { PostToolUse: [{
|
|
3018
3121
|
matcher: "PushNotification",
|
|
3019
3122
|
hooks: [async (input) => {
|
|
3020
|
-
if (input.hook_event_name !== "PostToolUse" ||
|
|
3123
|
+
if (input.hook_event_name !== "PostToolUse" || !isRecord(input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
|
|
3021
3124
|
broadcast({
|
|
3022
3125
|
type: "notification",
|
|
3023
3126
|
sessionId: input.session_id,
|
|
@@ -3038,12 +3141,15 @@ function claudeOptions(accountId, cwd) {
|
|
|
3038
3141
|
async function claudeCapabilities(account, cwd) {
|
|
3039
3142
|
const result = await initialization(account.id, cwd);
|
|
3040
3143
|
return {
|
|
3041
|
-
models: result.models.map((model) =>
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3144
|
+
models: result.models.filter((model) => model.value !== "default").map((model) => {
|
|
3145
|
+
if (model.resolvedModel === void 0) throw new Error(`Claude did not resolve the ${model.value} model`);
|
|
3146
|
+
return {
|
|
3147
|
+
value: model.resolvedModel,
|
|
3148
|
+
name: model.displayName,
|
|
3149
|
+
description: model.description,
|
|
3150
|
+
efforts: model.supportedEffortLevels ?? []
|
|
3151
|
+
};
|
|
3152
|
+
}),
|
|
3047
3153
|
commands: result.commands.map((command) => ({
|
|
3048
3154
|
name: command.name,
|
|
3049
3155
|
description: command.description,
|
|
@@ -3104,6 +3210,28 @@ function recordLimit(accountId, info) {
|
|
|
3104
3210
|
limits.set(accountId, current);
|
|
3105
3211
|
return accountLimits(accountId);
|
|
3106
3212
|
}
|
|
3213
|
+
async function recordUsage(accountId, probe) {
|
|
3214
|
+
try {
|
|
3215
|
+
const usage = await probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true });
|
|
3216
|
+
if (usage.rate_limits === null) return;
|
|
3217
|
+
const current = limits.get(accountId) ?? /* @__PURE__ */ new Map();
|
|
3218
|
+
const windows = {
|
|
3219
|
+
five_hour: usage.rate_limits.five_hour,
|
|
3220
|
+
seven_day: usage.rate_limits.seven_day,
|
|
3221
|
+
seven_day_opus: usage.rate_limits.seven_day_opus,
|
|
3222
|
+
seven_day_sonnet: usage.rate_limits.seven_day_sonnet
|
|
3223
|
+
};
|
|
3224
|
+
for (const [name, window] of Object.entries(windows)) if (window !== void 0 && window !== null) current.set(name, {
|
|
3225
|
+
window: name,
|
|
3226
|
+
utilization: window.utilization === null ? null : window.utilization / 100,
|
|
3227
|
+
resetsAt: window.resets_at === null ? null : Date.parse(window.resets_at) / 1e3,
|
|
3228
|
+
rejected: window.utilization !== null && window.utilization >= 100
|
|
3229
|
+
});
|
|
3230
|
+
limits.set(accountId, current);
|
|
3231
|
+
} catch (error) {
|
|
3232
|
+
console.warn(`could not read Claude usage limits: ${error}`);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3107
3235
|
function isLinked(configDir) {
|
|
3108
3236
|
const base = readConfig().claudeBaseDir;
|
|
3109
3237
|
try {
|
|
@@ -3133,7 +3261,9 @@ async function runProbe(accountId, cwd) {
|
|
|
3133
3261
|
options: claudeOptions(accountId, cwd)
|
|
3134
3262
|
});
|
|
3135
3263
|
try {
|
|
3136
|
-
|
|
3264
|
+
const result = await probe.initializationResult();
|
|
3265
|
+
await recordUsage(accountId, probe);
|
|
3266
|
+
return result;
|
|
3137
3267
|
} finally {
|
|
3138
3268
|
closed.resolve();
|
|
3139
3269
|
probe.close();
|
|
@@ -3192,14 +3322,15 @@ function toItems(messages) {
|
|
|
3192
3322
|
}
|
|
3193
3323
|
if (message.type === "assistant") {
|
|
3194
3324
|
message.content.forEach((block, index) => {
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3325
|
+
const shared = {
|
|
3326
|
+
id: `${message.uuid}:${index}`,
|
|
3327
|
+
parent: message.parent,
|
|
3328
|
+
time: message.time
|
|
3329
|
+
};
|
|
3197
3330
|
switch (block.type) {
|
|
3198
3331
|
case "text":
|
|
3199
3332
|
if (block.text.trim() !== "") items.push({
|
|
3200
|
-
|
|
3201
|
-
parent,
|
|
3202
|
-
time: message.time,
|
|
3333
|
+
...shared,
|
|
3203
3334
|
kind: "assistant",
|
|
3204
3335
|
text: block.text,
|
|
3205
3336
|
message: message.uuid
|
|
@@ -3207,9 +3338,7 @@ function toItems(messages) {
|
|
|
3207
3338
|
break;
|
|
3208
3339
|
case "thinking":
|
|
3209
3340
|
if (block.thinking.trim() !== "") items.push({
|
|
3210
|
-
|
|
3211
|
-
parent,
|
|
3212
|
-
time: message.time,
|
|
3341
|
+
...shared,
|
|
3213
3342
|
kind: "reasoning",
|
|
3214
3343
|
text: block.thinking
|
|
3215
3344
|
});
|
|
@@ -3221,7 +3350,7 @@ function toItems(messages) {
|
|
|
3221
3350
|
}
|
|
3222
3351
|
if (block.name === "TaskStop") stops.set(block.id, text$1(block.input.task_id));
|
|
3223
3352
|
tools.set(block.id, items.length);
|
|
3224
|
-
items.push(toolItem(block, parent, message.time, (to) => {
|
|
3353
|
+
items.push(toolItem(block, message.parent, message.time, (to) => {
|
|
3225
3354
|
const owner = owners.get(to);
|
|
3226
3355
|
return owner === void 0 ? null : items[owner].id;
|
|
3227
3356
|
}));
|
|
@@ -3725,12 +3854,6 @@ async function agentMessages(sessionId, cwd, agentId, parent) {
|
|
|
3725
3854
|
//#endregion
|
|
3726
3855
|
//#region src/lib/claude/run.ts
|
|
3727
3856
|
const IDLE_LIMIT = 6e5;
|
|
3728
|
-
const imageTypes = [
|
|
3729
|
-
"image/png",
|
|
3730
|
-
"image/jpeg",
|
|
3731
|
-
"image/gif",
|
|
3732
|
-
"image/webp"
|
|
3733
|
-
];
|
|
3734
3857
|
var Run = class {
|
|
3735
3858
|
provider = "claude";
|
|
3736
3859
|
sessionId;
|
|
@@ -3860,42 +3983,10 @@ var Run = class {
|
|
|
3860
3983
|
}
|
|
3861
3984
|
push(prompt, priority) {
|
|
3862
3985
|
clearTimeout(this.idleTimer);
|
|
3863
|
-
for (const image of prompt.images) if (!imageTypes.includes(image.mediaType)) throw new Error(`${image.mediaType} images are not supported`);
|
|
3864
3986
|
const uuid = randomUUID();
|
|
3865
|
-
this.inbox.push(
|
|
3866
|
-
type: "user",
|
|
3867
|
-
message: {
|
|
3868
|
-
role: "user",
|
|
3869
|
-
content: [...prompt.images.map((image) => ({
|
|
3870
|
-
type: "image",
|
|
3871
|
-
source: {
|
|
3872
|
-
type: "base64",
|
|
3873
|
-
media_type: image.mediaType,
|
|
3874
|
-
data: image.data
|
|
3875
|
-
}
|
|
3876
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3877
|
-
type: "text",
|
|
3878
|
-
text: prompt.text
|
|
3879
|
-
}]]
|
|
3880
|
-
},
|
|
3881
|
-
parent_tool_use_id: null,
|
|
3882
|
-
uuid,
|
|
3883
|
-
...priority === void 0 ? {} : { priority }
|
|
3884
|
-
});
|
|
3987
|
+
this.inbox.push(toUserInput(prompt, uuid, priority));
|
|
3885
3988
|
this.wake?.();
|
|
3886
|
-
this.add(
|
|
3887
|
-
type: "user",
|
|
3888
|
-
uuid,
|
|
3889
|
-
time: Date.now(),
|
|
3890
|
-
content: [...prompt.images.map((image) => ({
|
|
3891
|
-
type: "image",
|
|
3892
|
-
...image
|
|
3893
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3894
|
-
type: "text",
|
|
3895
|
-
text: prompt.text
|
|
3896
|
-
}]],
|
|
3897
|
-
parent: null
|
|
3898
|
-
});
|
|
3989
|
+
this.add(promptMessage(prompt, uuid));
|
|
3899
3990
|
this.running = true;
|
|
3900
3991
|
this.prompted = true;
|
|
3901
3992
|
broadcastActivity();
|
|
@@ -4089,6 +4180,7 @@ var Run = class {
|
|
|
4089
4180
|
request
|
|
4090
4181
|
});
|
|
4091
4182
|
broadcastActivity();
|
|
4183
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
4092
4184
|
options.signal.addEventListener("abort", () => this.settle(request.id, {
|
|
4093
4185
|
behavior: "deny",
|
|
4094
4186
|
message: "Cancelled"
|
|
@@ -4139,21 +4231,13 @@ var Run = class {
|
|
|
4139
4231
|
});
|
|
4140
4232
|
}
|
|
4141
4233
|
};
|
|
4142
|
-
function notice(text) {
|
|
4143
|
-
return {
|
|
4144
|
-
type: "notice",
|
|
4145
|
-
uuid: randomUUID(),
|
|
4146
|
-
time: Date.now(),
|
|
4147
|
-
text,
|
|
4148
|
-
isError: true
|
|
4149
|
-
};
|
|
4150
|
-
}
|
|
4151
4234
|
//#endregion
|
|
4152
4235
|
//#region src/services/git.ts
|
|
4153
4236
|
const DIFF_LIMIT = 8e4;
|
|
4154
4237
|
const UNTRACKED_LIMIT = 100;
|
|
4155
4238
|
const NEW_FILE_LIMIT = 262144;
|
|
4156
4239
|
const REPO_DEPTH = 3;
|
|
4240
|
+
const run$1 = promisify(execFile);
|
|
4157
4241
|
async function gitStatus(cwd) {
|
|
4158
4242
|
let output;
|
|
4159
4243
|
try {
|
|
@@ -4207,20 +4291,24 @@ async function push$1(cwd) {
|
|
|
4207
4291
|
return gitStatus(cwd);
|
|
4208
4292
|
}
|
|
4209
4293
|
async function createPullRequest(cwd) {
|
|
4210
|
-
const
|
|
4294
|
+
const url = (await run$1("gh", [
|
|
4211
4295
|
"pr",
|
|
4212
4296
|
"create",
|
|
4213
4297
|
"--fill"
|
|
4214
|
-
], { cwd });
|
|
4215
|
-
const url = stdout.trim().split("\n").at(-1);
|
|
4298
|
+
], { cwd })).stdout.trim().split("\n").at(-1);
|
|
4216
4299
|
if (url === void 0) throw new Error("gh did not return a pull request URL");
|
|
4217
4300
|
return { url };
|
|
4218
4301
|
}
|
|
4219
|
-
async function suggestCommitMessage(
|
|
4220
|
-
const
|
|
4302
|
+
async function suggestCommitMessage(cwd) {
|
|
4303
|
+
const settings = readConfig().commitMessage;
|
|
4304
|
+
const provider = settings.provider;
|
|
4305
|
+
const accountId = settings.accountId;
|
|
4306
|
+
const model = settings.model;
|
|
4307
|
+
if (provider === null || model === null) throw new Error("Choose a commit message model in Settings → Git");
|
|
4308
|
+
assertEnabled(provider, accountId);
|
|
4309
|
+
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4310
|
+
const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
|
|
4221
4311
|
if (diff === "") throw new Error("There is nothing to commit");
|
|
4222
|
-
const model = readConfig().commitModels[provider];
|
|
4223
|
-
if (model === null) throw new Error(`Choose a ${providerName(provider)} commit model in Settings → Accounts`);
|
|
4224
4312
|
const prompt = `Write a git commit message for this diff. Reply with only the message: a short summary line, optionally followed by a blank line and a brief body.\n\n${diff}`;
|
|
4225
4313
|
if (provider === "codex") return codexCommitMessage(accountId, cwd, model, prompt);
|
|
4226
4314
|
if (provider === "opencode") return openCodeCommitMessage(cwd, model, prompt);
|
|
@@ -4243,10 +4331,6 @@ async function suggestCommitMessage(provider, accountId, cwd) {
|
|
|
4243
4331
|
}
|
|
4244
4332
|
throw new Error("Claude did not reply");
|
|
4245
4333
|
}
|
|
4246
|
-
async function workingDiff(cwd) {
|
|
4247
|
-
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4248
|
-
return repoDiff(cwd, "");
|
|
4249
|
-
}
|
|
4250
4334
|
async function projectDiff(cwd) {
|
|
4251
4335
|
if (await isRepo(cwd)) return repoDiff(cwd, "");
|
|
4252
4336
|
const repos = childRepos(cwd, "", REPO_DEPTH);
|
|
@@ -4254,7 +4338,7 @@ async function projectDiff(cwd) {
|
|
|
4254
4338
|
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), `${repo}/`)))).join("");
|
|
4255
4339
|
}
|
|
4256
4340
|
function git(cwd, args) {
|
|
4257
|
-
return
|
|
4341
|
+
return run$1("git", args, {
|
|
4258
4342
|
cwd,
|
|
4259
4343
|
maxBuffer: 67108864
|
|
4260
4344
|
});
|
|
@@ -4403,7 +4487,7 @@ async function taskOutput(sessionId, file) {
|
|
|
4403
4487
|
try {
|
|
4404
4488
|
stats = await lstat(file);
|
|
4405
4489
|
} catch (error) {
|
|
4406
|
-
if (error
|
|
4490
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
4407
4491
|
throw error;
|
|
4408
4492
|
}
|
|
4409
4493
|
if (!stats.isFile()) throw new Error("Background task output is not a regular file");
|
|
@@ -4447,42 +4531,57 @@ const TIME_TO_LIVE = 900;
|
|
|
4447
4531
|
const SEND_TIMEOUT = 1e4;
|
|
4448
4532
|
const SUBJECT = "https://github.com/NazmusSayad/offsite";
|
|
4449
4533
|
let server$1 = "";
|
|
4534
|
+
let notifier = null;
|
|
4450
4535
|
function startPush(address) {
|
|
4451
4536
|
server$1 = address;
|
|
4452
4537
|
}
|
|
4453
4538
|
function pushActive() {
|
|
4454
|
-
return
|
|
4539
|
+
return targets().length > 0;
|
|
4540
|
+
}
|
|
4541
|
+
function notifierClient() {
|
|
4542
|
+
return notifier === null ? null : notifier.client;
|
|
4455
4543
|
}
|
|
4456
|
-
function
|
|
4457
|
-
|
|
4544
|
+
function claimNotifier(client, endpoint) {
|
|
4545
|
+
notifier = {
|
|
4546
|
+
client,
|
|
4547
|
+
endpoint
|
|
4548
|
+
};
|
|
4549
|
+
for (const item of clients) item.send({
|
|
4550
|
+
type: "notifier",
|
|
4551
|
+
active: item === client
|
|
4552
|
+
});
|
|
4458
4553
|
}
|
|
4459
|
-
function subscribe(
|
|
4460
|
-
if (new URL(subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4554
|
+
function subscribe(registration, client) {
|
|
4555
|
+
if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4461
4556
|
const values = {
|
|
4462
|
-
endpoint: subscription.endpoint,
|
|
4463
|
-
p256dh: subscription.keys.p256dh,
|
|
4464
|
-
auth: subscription.keys.auth,
|
|
4557
|
+
endpoint: registration.subscription.endpoint,
|
|
4558
|
+
p256dh: registration.subscription.keys.p256dh,
|
|
4559
|
+
auth: registration.subscription.keys.auth,
|
|
4560
|
+
publicKey: registration.publicKey,
|
|
4561
|
+
privateKey: registration.privateKey,
|
|
4562
|
+
machine: registration.machine,
|
|
4465
4563
|
server: server$1
|
|
4466
4564
|
};
|
|
4467
4565
|
db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
|
|
4468
4566
|
target: pushSubscriptions.endpoint,
|
|
4469
4567
|
set: values
|
|
4470
4568
|
}).run();
|
|
4569
|
+
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4471
4570
|
console.log("a browser subscribed to push notifications");
|
|
4472
4571
|
}
|
|
4473
4572
|
function unsubscribe(endpoint) {
|
|
4474
4573
|
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4574
|
+
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4475
4575
|
console.log("a browser unsubscribed from push notifications");
|
|
4476
4576
|
}
|
|
4477
4577
|
async function sendPush(message) {
|
|
4478
|
-
const subscriptions =
|
|
4578
|
+
const subscriptions = targets();
|
|
4479
4579
|
if (subscriptions.length === 0) return;
|
|
4480
|
-
const
|
|
4481
|
-
const payload = JSON.stringify({
|
|
4580
|
+
const trimmed = {
|
|
4482
4581
|
...message,
|
|
4483
4582
|
title: [...message.title].slice(0, TITLE_LIMIT).join(""),
|
|
4484
4583
|
body: [...message.body].slice(0, BODY_LIMIT).join("")
|
|
4485
|
-
}
|
|
4584
|
+
};
|
|
4486
4585
|
const gone = [];
|
|
4487
4586
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4488
4587
|
try {
|
|
@@ -4492,11 +4591,14 @@ async function sendPush(message) {
|
|
|
4492
4591
|
p256dh: subscription.p256dh,
|
|
4493
4592
|
auth: subscription.auth
|
|
4494
4593
|
}
|
|
4495
|
-
},
|
|
4594
|
+
}, JSON.stringify({
|
|
4595
|
+
...trimmed,
|
|
4596
|
+
machineId: subscription.machine
|
|
4597
|
+
}), {
|
|
4496
4598
|
vapidDetails: {
|
|
4497
4599
|
subject: SUBJECT,
|
|
4498
|
-
publicKey:
|
|
4499
|
-
privateKey:
|
|
4600
|
+
publicKey: subscription.publicKey,
|
|
4601
|
+
privateKey: subscription.privateKey
|
|
4500
4602
|
},
|
|
4501
4603
|
TTL: TIME_TO_LIVE,
|
|
4502
4604
|
urgency: "high",
|
|
@@ -4520,8 +4622,9 @@ async function sendPush(message) {
|
|
|
4520
4622
|
if (gone.length === 0) return;
|
|
4521
4623
|
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4522
4624
|
}
|
|
4523
|
-
function
|
|
4524
|
-
|
|
4625
|
+
function targets() {
|
|
4626
|
+
if (notifier === null || notifier.endpoint === null) return [];
|
|
4627
|
+
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4525
4628
|
}
|
|
4526
4629
|
//#endregion
|
|
4527
4630
|
//#region src/services/runs.ts
|
|
@@ -4595,6 +4698,7 @@ async function open$1(client, sessionId, projectId) {
|
|
|
4595
4698
|
client.send(event);
|
|
4596
4699
|
}
|
|
4597
4700
|
async function send$1(client, request) {
|
|
4701
|
+
assertEnabled(request.settings.provider, request.settings.accountId);
|
|
4598
4702
|
const cwd = projectPath(request.projectId);
|
|
4599
4703
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
|
|
4600
4704
|
if (request.sessionId === null) {
|
|
@@ -4618,11 +4722,7 @@ async function sendExisting(client, sessionId, cwd, request) {
|
|
|
4618
4722
|
const provider = providerOf(sessionId);
|
|
4619
4723
|
if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
|
|
4620
4724
|
const existing = runs.get(sessionId);
|
|
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) {
|
|
4725
|
+
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing, cwd) || existing?.provider === "codex" && !existing.running && existing.pending.size === 0 && (listedUpdatedAt("codex", sessionId) ?? 0) > existing.activeAt) {
|
|
4626
4726
|
existing.stop();
|
|
4627
4727
|
await existing.closed;
|
|
4628
4728
|
}
|
|
@@ -4772,7 +4872,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
4772
4872
|
}
|
|
4773
4873
|
//#endregion
|
|
4774
4874
|
//#region src/services/terminal/shell-integration.ts
|
|
4775
|
-
const scripts = path.join(serverDir, "src
|
|
4875
|
+
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
4776
4876
|
const folder = path.join(home, "shell-integration");
|
|
4777
4877
|
function prepare(env, command, shell) {
|
|
4778
4878
|
if (shellIntegrationIssue(command, platform(), shell) !== null) return null;
|
|
@@ -4989,7 +5089,7 @@ function launch() {
|
|
|
4989
5089
|
offsite: [
|
|
4990
5090
|
"offsite",
|
|
4991
5091
|
"offsite",
|
|
4992
|
-
|
|
5092
|
+
"0.0.0"
|
|
4993
5093
|
],
|
|
4994
5094
|
ghostty: [
|
|
4995
5095
|
"ghostty",
|
|
@@ -5457,7 +5557,7 @@ function scan(id, terminal, data) {
|
|
|
5457
5557
|
return;
|
|
5458
5558
|
}
|
|
5459
5559
|
const alert = terminal.scanner.scan(data);
|
|
5460
|
-
if (alert === null ||
|
|
5560
|
+
if (alert === null || notifierWatching()) return;
|
|
5461
5561
|
if (!(alert.type === "bell" ? terminal.alerts.startBell() : terminal.alerts.startNotification())) return;
|
|
5462
5562
|
const windowTitle = terminal.scanner.title;
|
|
5463
5563
|
setTimeout(() => push(id, alert, windowTitle), 500);
|
|
@@ -5474,7 +5574,7 @@ function push(id, alert, windowTitle) {
|
|
|
5474
5574
|
title = alert.title;
|
|
5475
5575
|
body = alert.body;
|
|
5476
5576
|
}
|
|
5477
|
-
if (
|
|
5577
|
+
if (notifierWatching()) return;
|
|
5478
5578
|
const entry = [...projects].find(([, project]) => project.tabs.some((tab) => tab.id === id));
|
|
5479
5579
|
if (entry === void 0) return;
|
|
5480
5580
|
const tab = findTab(id);
|
|
@@ -5482,19 +5582,17 @@ function push(id, alert, windowTitle) {
|
|
|
5482
5582
|
sendPush({
|
|
5483
5583
|
title,
|
|
5484
5584
|
body,
|
|
5485
|
-
machineId: machineInfo().id,
|
|
5486
5585
|
projectId: entry[0],
|
|
5487
5586
|
tabId: id
|
|
5488
5587
|
});
|
|
5489
5588
|
}
|
|
5490
|
-
function
|
|
5491
|
-
|
|
5589
|
+
function notifierWatching() {
|
|
5590
|
+
const client = notifierClient();
|
|
5591
|
+
if (client === null || !connections.has(client)) return false;
|
|
5592
|
+
return Date.now() - client.heardAt < SILENCE_LIMIT;
|
|
5492
5593
|
}
|
|
5493
5594
|
function findTab(id) {
|
|
5494
|
-
|
|
5495
|
-
const tab = project.tabs.find((item) => item.id === id);
|
|
5496
|
-
if (tab !== void 0) return tab;
|
|
5497
|
-
}
|
|
5595
|
+
return [...projects.values()].flatMap((project) => project.tabs).find((tab) => tab.id === id);
|
|
5498
5596
|
}
|
|
5499
5597
|
function nearestVisible(tabs, index) {
|
|
5500
5598
|
for (let position = index; position < tabs.length; position++) if (!tabs[position].minimized) return position;
|
|
@@ -5527,8 +5625,6 @@ const handlers = {
|
|
|
5527
5625
|
refreshAll();
|
|
5528
5626
|
return {
|
|
5529
5627
|
...configInfo(),
|
|
5530
|
-
protocol: 3,
|
|
5531
|
-
machineId: machineInfo().id,
|
|
5532
5628
|
hostname: os.hostname(),
|
|
5533
5629
|
projects: projects$1(),
|
|
5534
5630
|
sessions: sessions(),
|
|
@@ -5537,10 +5633,15 @@ const handlers = {
|
|
|
5537
5633
|
};
|
|
5538
5634
|
},
|
|
5539
5635
|
"config.get": () => configInfo(),
|
|
5540
|
-
"config.save": (params) => {
|
|
5541
|
-
const before = readConfig()
|
|
5636
|
+
"config.save": async (params) => {
|
|
5637
|
+
const before = readConfig();
|
|
5542
5638
|
const saved = saveConfig(params);
|
|
5543
|
-
|
|
5639
|
+
for (const provider of [
|
|
5640
|
+
"claude",
|
|
5641
|
+
"codex",
|
|
5642
|
+
"opencode"
|
|
5643
|
+
]) if (before.enabledProviders[provider] && !saved.enabledProviders[provider]) await stopProvider(provider);
|
|
5644
|
+
if (saved.claudeBaseDir === before.claudeBaseDir && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
|
|
5544
5645
|
watchSessions();
|
|
5545
5646
|
refreshAll();
|
|
5546
5647
|
publishAccounts();
|
|
@@ -5630,8 +5731,8 @@ const handlers = {
|
|
|
5630
5731
|
publishAccounts();
|
|
5631
5732
|
return null;
|
|
5632
5733
|
},
|
|
5633
|
-
"accounts.update": (params) => {
|
|
5634
|
-
updateAccount(params
|
|
5734
|
+
"accounts.update": async (params) => {
|
|
5735
|
+
await updateAccount(params);
|
|
5635
5736
|
publishAccounts();
|
|
5636
5737
|
return null;
|
|
5637
5738
|
},
|
|
@@ -5640,7 +5741,7 @@ const handlers = {
|
|
|
5640
5741
|
publishAccounts();
|
|
5641
5742
|
return null;
|
|
5642
5743
|
},
|
|
5643
|
-
capabilities: (params) => capabilities(params.accountId, projectPath(params.projectId)),
|
|
5744
|
+
capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
|
|
5644
5745
|
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
5645
5746
|
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
5646
5747
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
@@ -5650,16 +5751,19 @@ const handlers = {
|
|
|
5650
5751
|
"git.commit": (params) => commit(projectPath(params.projectId), params.message),
|
|
5651
5752
|
"git.push": (params) => push$1(projectPath(params.projectId)),
|
|
5652
5753
|
"git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
|
|
5653
|
-
"git.message": (params) => suggestCommitMessage(
|
|
5654
|
-
"push.
|
|
5655
|
-
|
|
5656
|
-
subscribe(params);
|
|
5754
|
+
"git.message": (params) => suggestCommitMessage(projectPath(params.projectId)),
|
|
5755
|
+
"push.subscribe": (params, client) => {
|
|
5756
|
+
subscribe(params, client);
|
|
5657
5757
|
return null;
|
|
5658
5758
|
},
|
|
5659
5759
|
"push.unsubscribe": (params) => {
|
|
5660
5760
|
unsubscribe(params.endpoint);
|
|
5661
5761
|
return null;
|
|
5662
5762
|
},
|
|
5763
|
+
"notifications.claim": (params, client) => {
|
|
5764
|
+
claimNotifier(client, params.endpoint);
|
|
5765
|
+
return null;
|
|
5766
|
+
},
|
|
5663
5767
|
terminal: async (params, client) => {
|
|
5664
5768
|
await terminalRequest(client, params);
|
|
5665
5769
|
return null;
|
|
@@ -5769,12 +5873,7 @@ const address = `http://${args.host}:${port}`;
|
|
|
5769
5873
|
const fallback = args.fallback === void 0 ? null : readFallback(args.fallback);
|
|
5770
5874
|
if (args.command === "pair") {
|
|
5771
5875
|
const links = pairingLinks(addToken(args.name));
|
|
5772
|
-
console.log(
|
|
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:");
|
|
5777
|
-
console.log(` ${links.hosted}`);
|
|
5876
|
+
console.log("Open this link in the browser to pair:");
|
|
5778
5877
|
console.log(` ${links.local}`);
|
|
5779
5878
|
if (fallback !== null) console.log(`Fallback: ${fallback}`);
|
|
5780
5879
|
process.exit(0);
|
|
@@ -5795,20 +5894,9 @@ const publicDir = path.join(import.meta.dirname, "..", "..", "web", "dist");
|
|
|
5795
5894
|
const token = args.auth && args.pair && !hasTokens() ? addToken(null) : null;
|
|
5796
5895
|
const server = createServer((request, response) => {
|
|
5797
5896
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5798
|
-
if (url.pathname === "/version") {
|
|
5799
|
-
response.setHeader("access-control-allow-origin", "*");
|
|
5800
|
-
response.setHeader("content-type", "application/json");
|
|
5801
|
-
response.end(JSON.stringify({
|
|
5802
|
-
version,
|
|
5803
|
-
protocol: 3,
|
|
5804
|
-
machineId: machineInfo().id,
|
|
5805
|
-
authorized: authorized(url)
|
|
5806
|
-
}));
|
|
5807
|
-
return;
|
|
5808
|
-
}
|
|
5809
5897
|
if (args.command === "serve") {
|
|
5810
5898
|
response.statusCode = 404;
|
|
5811
|
-
response.end(
|
|
5899
|
+
response.end("offsite serve has no web app");
|
|
5812
5900
|
return;
|
|
5813
5901
|
}
|
|
5814
5902
|
serveFile(url.pathname, response);
|
|
@@ -5818,7 +5906,7 @@ const paired = /* @__PURE__ */ new Map();
|
|
|
5818
5906
|
server.on("upgrade", (request, socket, head) => {
|
|
5819
5907
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5820
5908
|
const given = url.searchParams.get("token");
|
|
5821
|
-
if (url.pathname !== "/ws" ||
|
|
5909
|
+
if (url.pathname !== "/ws" || !authorized(url)) {
|
|
5822
5910
|
socket.destroy();
|
|
5823
5911
|
return;
|
|
5824
5912
|
}
|
|
@@ -5838,13 +5926,9 @@ server.listen(port, args.host, () => {
|
|
|
5838
5926
|
if (args.pair && (token !== null || !args.auth)) {
|
|
5839
5927
|
const links = pairingLinks(token);
|
|
5840
5928
|
if (args.command === "web") console.log(`offsite is running at ${links.local}`);
|
|
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");
|
|
5845
5929
|
} else {
|
|
5846
5930
|
console.log(`offsite is running at ${address}`);
|
|
5847
|
-
console.log(
|
|
5931
|
+
if (args.pair) console.log("Run offsite pair to pair another browser");
|
|
5848
5932
|
}
|
|
5849
5933
|
if (!args.auth) console.warn("auth is off; anyone who can reach this port can control this machine");
|
|
5850
5934
|
});
|
|
@@ -5902,15 +5986,10 @@ function readFallback(link) {
|
|
|
5902
5986
|
return url.origin;
|
|
5903
5987
|
}
|
|
5904
5988
|
function pairingLinks(token) {
|
|
5905
|
-
|
|
5989
|
+
return { local: `${address}/pair#${new URLSearchParams({
|
|
5906
5990
|
machine: address,
|
|
5907
|
-
...fallback !== null && { fallback },
|
|
5908
5991
|
...token !== null && { token }
|
|
5909
|
-
});
|
|
5910
|
-
return {
|
|
5911
|
-
local: token === null ? address : `${address}/pair#${new URLSearchParams({ token })}`,
|
|
5912
|
-
hosted: `${HOSTED_APP}/pair#${hosted}`
|
|
5913
|
-
};
|
|
5992
|
+
})}` };
|
|
5914
5993
|
}
|
|
5915
5994
|
function serveFile(pathname, response) {
|
|
5916
5995
|
const requested = path.join(publicDir, path.normalize(pathname));
|