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.
Files changed (89) hide show
  1. package/README.md +7 -7
  2. package/apps/server/dist/main.mjs +1829 -1750
  3. package/apps/server/dist/main.mjs.map +1 -1
  4. package/apps/server/drizzle/20261017103559_browser_push_keys/migration.sql +11 -0
  5. package/apps/server/drizzle/20261017103559_browser_push_keys/snapshot.json +265 -0
  6. package/apps/server/drizzle/20261017110336_browser_machine_ids/migration.sql +3 -0
  7. package/apps/server/drizzle/20261017110336_browser_machine_ids/snapshot.json +252 -0
  8. package/apps/web/dist/assets/{angular-html-BEUtPJ31.js → angular-html-DTNmgvth.js} +1 -1
  9. package/apps/web/dist/assets/{angular-ts-Bnr4rzFr.js → angular-ts-DOOLa99A.js} +1 -1
  10. package/apps/web/dist/assets/{apl-B-JNBTdP.js → apl-CSWmNqJI.js} +1 -1
  11. package/apps/web/dist/assets/{astro-DLjK95IW.js → astro-CK7We5bo.js} +1 -1
  12. package/apps/web/dist/assets/{blade-PLMCMBeb.js → blade-B9Q94PMc.js} +1 -1
  13. package/apps/web/dist/assets/{c-D5z8xNe6.js → c-D05zRIsd.js} +1 -1
  14. package/apps/web/dist/assets/{chapel-B9y2HxI_.js → chapel-CJbjuVPR.js} +1 -1
  15. package/apps/web/dist/assets/{cobol-BSwRu2X7.js → cobol-CE0VhU2R.js} +1 -1
  16. package/apps/web/dist/assets/{coffee-BAWFR6iF.js → coffee-C0i-RL39.js} +1 -1
  17. package/apps/web/dist/assets/{cpp-CO-vZI4m.js → cpp-CBJhfD_V.js} +1 -1
  18. package/apps/web/dist/assets/{crystal-BSfyPt6R.js → crystal-DRdgVTW1.js} +1 -1
  19. package/apps/web/dist/assets/{css-BOzKs1dt.js → css-DgMip4qW.js} +1 -1
  20. package/apps/web/dist/assets/{edge-OBqkt95y.js → edge-BCmeW_3H.js} +1 -1
  21. package/apps/web/dist/assets/{elixir-B14D2jMM.js → elixir-BiRSxKnc.js} +1 -1
  22. package/apps/web/dist/assets/{elm-DgfIayFq.js → elm-zBiqCp9W.js} +1 -1
  23. package/apps/web/dist/assets/{erb-w-PUv7ic.js → erb-1QviMyT9.js} +1 -1
  24. package/apps/web/dist/assets/{git-rebase-DnzmyV_3.js → git-rebase-TjFhp_Gn.js} +1 -1
  25. package/apps/web/dist/assets/{glimmer-js-CLephpyX.js → glimmer-js-dThwC_hZ.js} +1 -1
  26. package/apps/web/dist/assets/{glimmer-ts-CvphZOog.js → glimmer-ts-cCThNW17.js} +1 -1
  27. package/apps/web/dist/assets/{glsl-Ddj2GHKO.js → glsl-BxmyIQYG.js} +1 -1
  28. package/apps/web/dist/assets/{graphql-CeGXMmoK.js → graphql-DygyczJj.js} +1 -1
  29. package/apps/web/dist/assets/{hack-CtBKZ5VW.js → hack-Bl1_oIjY.js} +1 -1
  30. package/apps/web/dist/assets/{haml-DVvVd-Mp.js → haml-CeOBocB0.js} +1 -1
  31. package/apps/web/dist/assets/{handlebars-Blx6Nluh.js → handlebars-Bw1KMTCv.js} +1 -1
  32. package/apps/web/dist/assets/{html-O-gZ5M1-.js → html-BaNo1IFo.js} +1 -1
  33. package/apps/web/dist/assets/{html-derivative-BsDe9c3P.js → html-derivative-f4LhlsLO.js} +1 -1
  34. package/apps/web/dist/assets/{http-CAUZeWmG.js → http-Be2FOF-U.js} +1 -1
  35. package/apps/web/dist/assets/{hurl-CYnRsJrb.js → hurl-BrtPQQhS.js} +1 -1
  36. package/apps/web/dist/assets/index-6ofGnhX4.css +2 -0
  37. package/apps/web/dist/assets/index-NWUHLKte.js +1203 -0
  38. package/apps/web/dist/assets/{java-DjjpMT1c.js → java-Bum5bcia.js} +1 -1
  39. package/apps/web/dist/assets/{javascript-BtiswSWS.js → javascript-C5oYhZKk.js} +1 -1
  40. package/apps/web/dist/assets/{jinja-ZQGJY58d.js → jinja-CNXoe8pn.js} +1 -1
  41. package/apps/web/dist/assets/{jison-VPkdeMFr.js → jison-g1hzxo4W.js} +1 -1
  42. package/apps/web/dist/assets/{json-D5BAJIHo.js → json-CiAAuc5j.js} +1 -1
  43. package/apps/web/dist/assets/{jsx-DMEKVcfT.js → jsx-Z0awY4ip.js} +1 -1
  44. package/apps/web/dist/assets/{julia-BmOLQFch.js → julia-DX3JjwcM.js} +1 -1
  45. package/apps/web/dist/assets/{just-YZGutsxo.js → just-DS6eCX0g.js} +1 -1
  46. package/apps/web/dist/assets/{latex-CvrF79xl.js → latex-CaBic_pJ.js} +1 -1
  47. package/apps/web/dist/assets/{liquid-B--WbaSh.js → liquid-DMaOBf3v.js} +1 -1
  48. package/apps/web/dist/assets/{lua-BNEqLlv-.js → lua-DeLz5pfA.js} +1 -1
  49. package/apps/web/dist/assets/{marko-jEzbzXvj.js → marko-DqlzpgnR.js} +1 -1
  50. package/apps/web/dist/assets/{mdc-yfiYVuXh.js → mdc-CRcSVpSn.js} +1 -1
  51. package/apps/web/dist/assets/{nginx-j7vnr3lB.js → nginx-BvszHPa-.js} +1 -1
  52. package/apps/web/dist/assets/{nim-CV2OP2RC.js → nim-B19-Hebo.js} +1 -1
  53. package/apps/web/dist/assets/{org-DTTS5Tv_.js → org-Dx7yMTgG.js} +1 -1
  54. package/apps/web/dist/assets/{perl-DuF7yMdF.js → perl-C-dISCJW.js} +1 -1
  55. package/apps/web/dist/assets/{php-8ufnwSAo.js → php-BHBxrwsl.js} +1 -1
  56. package/apps/web/dist/assets/{pug-D-_t9dIn.js → pug-4a3x4vRs.js} +1 -1
  57. package/apps/web/dist/assets/{qml-DFxD5uAC.js → qml-CGOhzFqi.js} +1 -1
  58. package/apps/web/dist/assets/{r-D8-Yi2KS.js → r-8jr0rtQL.js} +1 -1
  59. package/apps/web/dist/assets/{razor-DhBq7-pb.js → razor-BDrURPIA.js} +1 -1
  60. package/apps/web/dist/assets/{regexp-C20CUsFr.js → regexp-Cg-MfP18.js} +1 -1
  61. package/apps/web/dist/assets/{rst-DsIaukoD.js → rst-0OrP55QZ.js} +1 -1
  62. package/apps/web/dist/assets/{ruby-C__EgK1w.js → ruby-dAApWLhj.js} +1 -1
  63. package/apps/web/dist/assets/{sas-BdDDRZUF.js → sas-3_hKTjL9.js} +1 -1
  64. package/apps/web/dist/assets/{scss-nPFPh8nF.js → scss-D6ollEQv.js} +1 -1
  65. package/apps/web/dist/assets/{shellscript-Cfn0DgiQ.js → shellscript-V3H5Rwnn.js} +1 -1
  66. package/apps/web/dist/assets/{shellsession-DFGAcvZ4.js → shellsession-TxydKEoU.js} +1 -1
  67. package/apps/web/dist/assets/{soy-8DrmW46F.js → soy-t3rHc1tL.js} +1 -1
  68. package/apps/web/dist/assets/{sql-BeU26hky.js → sql-B-NkeM8R.js} +1 -1
  69. package/apps/web/dist/assets/{stata-OFfE-Kh9.js → stata-BW6neeVL.js} +1 -1
  70. package/apps/web/dist/assets/{surrealql-Bwt4eVgP.js → surrealql-CRBKdx6K.js} +1 -1
  71. package/apps/web/dist/assets/{svelte-DF7N9uBp.js → svelte-DTTmtyn-.js} +1 -1
  72. package/apps/web/dist/assets/{templ-CEirsWl9.js → templ-CCBYQttY.js} +1 -1
  73. package/apps/web/dist/assets/{tex-BvBhDkV4.js → tex-D9XxqR5N.js} +1 -1
  74. package/apps/web/dist/assets/{ts-tags-Dx8bn0ts.js → ts-tags-CI4pWHnF.js} +1 -1
  75. package/apps/web/dist/assets/{tsx-vXk5Q55O.js → tsx-DlfK3Vuf.js} +1 -1
  76. package/apps/web/dist/assets/{twig-CIgLDHIW.js → twig-D-C2s1f2.js} +1 -1
  77. package/apps/web/dist/assets/{typescript-CCW6p4Fc.js → typescript-BnqYTwqM.js} +1 -1
  78. package/apps/web/dist/assets/{typst-CQL-qDz3.js → typst-DdTyavvE.js} +1 -1
  79. package/apps/web/dist/assets/{vue-html-BHuiCeAo.js → vue-html-CIXNcZez.js} +1 -1
  80. package/apps/web/dist/assets/{vue-vine-DxeD8nHa.js → vue-vine-Czo_W6u3.js} +1 -1
  81. package/apps/web/dist/assets/{vue-gFAUYOUp.js → vue-zbXTTw4n.js} +1 -1
  82. package/apps/web/dist/assets/{xml-B43ZEHE5.js → xml-DWv3jkXo.js} +1 -1
  83. package/apps/web/dist/assets/{xsl-icvYLDXv.js → xsl-BiggF1J3.js} +1 -1
  84. package/apps/web/dist/assets/{yaml-Bj0dlmrV.js → yaml-D9oC3m1d.js} +1 -1
  85. package/apps/web/dist/index.html +2 -2
  86. package/apps/web/dist/sw.js +1 -1
  87. package/package.json +4 -3
  88. package/apps/web/dist/assets/index-5vPpgW8A.css +0 -2
  89. 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/codex/items.ts
620
- const statuses$1 = {
621
- inProgress: "running",
622
- completed: "completed",
623
- failed: "failed",
624
- declined: "stopped",
625
- interrupted: "stopped"
626
- };
627
- function turnItems(turns, parent) {
628
- 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)] : []]);
629
- }
630
- function itemId(turnId, id) {
631
- return `${turnId}:${id}`;
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 turnOf(itemId) {
634
- const index = itemId.indexOf(":");
635
- if (index < 0) throw new Error(`${itemId} is not a Codex item`);
636
- return itemId.slice(0, index);
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 errorItem(id, text) {
407
+ function promptMessage(prompt, uuid) {
639
408
  return {
640
- id,
641
- parent: null,
409
+ type: "user",
410
+ uuid,
642
411
  time: Date.now(),
643
- kind: "error",
644
- text
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 toItems$1(turnId, item, parent, time) {
648
- const base = {
649
- id: itemId(turnId, item.id),
650
- parent,
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
- switch (item.type) {
654
- case "userMessage": return [{
655
- ...base,
656
- kind: "user",
657
- text: item.content.flatMap((input) => userText$1(input)).join("\n").trim(),
658
- images: item.content.flatMap((input) => userImages(input)),
659
- format: "text"
660
- }];
661
- case "agentMessage": return item.text.trim() === "" ? [] : [{
662
- ...base,
663
- kind: "assistant",
664
- text: item.text,
665
- message: base.id
666
- }];
667
- case "reasoning": {
668
- const text = [...item.summary, ...item.content].join("\n\n").trim();
669
- return text === "" ? [] : [{
670
- ...base,
671
- kind: "reasoning",
672
- text
673
- }];
674
- }
675
- case "plan": return [{
676
- ...base,
677
- kind: "plan",
678
- plan: item.text
679
- }];
680
- case "commandExecution": return [{
681
- ...base,
682
- kind: "command",
683
- command: item.command,
684
- description: "",
685
- output: item.aggregatedOutput ?? "",
686
- status: statuses$1[item.status],
687
- background: null
688
- }];
689
- case "fileChange": return item.changes.map((change, index) => ({
690
- ...base,
691
- id: `${base.id}:${index}`,
692
- kind: "file_change",
693
- path: change.path,
694
- diffs: change.kind.type === "add" ? [{
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) => typeof block === "object" && block !== null && "text" in block && typeof block.text === "string" ? block.text : JSON.stringify(block)).join("\n");
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
- if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
819
- return { value };
695
+ return isRecord(value) ? value : { value };
820
696
  }
821
697
  //#endregion
822
- //#region src/lib/live.ts
823
- const runs = /* @__PURE__ */ new Map();
824
- function activity() {
825
- const result = {};
826
- for (const run of runs.values()) result[run.sessionId] = {
827
- running: run.running,
828
- waiting: run.pending.size > 0,
829
- prompted: run.prompted,
830
- tasks: run.tasks
831
- };
832
- return result;
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 broadcastActivity() {
835
- broadcast({
836
- type: "activity",
837
- activity: activity()
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/lib/codex/requests.ts
842
- function toRequest$1(requestId, method, params, items) {
843
- const turnId = params.turnId;
844
- switch (method) {
845
- case "item/commandExecution/requestApproval": return {
846
- id: requestId,
847
- kind: "approval",
848
- title: params.reason ?? "Run this command?",
849
- canAlwaysAllow: true,
850
- item: items.get(itemId(turnId, params.itemId)) ?? {
851
- id: itemId(turnId, params.itemId),
852
- parent: null,
853
- time: Date.now(),
854
- kind: "command",
855
- command: params.command ?? "",
856
- description: "",
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/storage/schema.ts
1443
- const machine = snakeCase.table("machine", {
1444
- id: text().primaryKey(),
1445
- pushPublicKey: text().notNull(),
1446
- pushPrivateKey: text().notNull()
1447
- });
1448
- const projects$2 = snakeCase.table("projects", {
1449
- id: text().primaryKey(),
1450
- path: text().notNull().unique(),
1451
- name: text().notNull(),
1452
- logo: text()
1453
- });
1454
- const sessions$1 = snakeCase.table("sessions", {
1455
- sessionId: text().primaryKey(),
1456
- interactedAt: integer(),
1457
- status: text({ enum: [
1458
- "active",
1459
- "pinned",
1460
- "minimized"
1461
- ] }).notNull()
1462
- });
1463
- const tokens = snakeCase.table("tokens", {
1464
- hash: text().primaryKey(),
1465
- name: text(),
1466
- createdAt: integer().notNull(),
1467
- lastUsedAt: integer()
1468
- });
1469
- const pushSubscriptions = snakeCase.table("push_subscriptions", {
1470
- endpoint: text().primaryKey(),
1471
- p256dh: text().notNull(),
1472
- auth: text().notNull(),
1473
- server: text().notNull()
1474
- });
1475
- //#endregion
1476
- //#region src/storage/db.ts
1477
- const file = path.join(home, "offsite.db");
1478
- closeSync(openSync(file, "a", 384));
1479
- chmodSync(file, 384);
1480
- const client = new DatabaseSync(file);
1481
- client.exec(`
1482
- PRAGMA journal_mode = WAL;
1483
- PRAGMA busy_timeout = 5000;
1484
- PRAGMA synchronous = NORMAL;
1485
- PRAGMA foreign_keys = ON;
1486
- `);
1487
- const db = drizzle({ client });
1488
- migrate(db, { migrationsFolder: path.join(serverDir, "drizzle") });
1489
- db.transaction((tx) => {
1490
- if (tx.select().from(machine).get() !== void 0) return;
1491
- const keys = webPush.generateVAPIDKeys();
1492
- tx.insert(machine).values({
1493
- id: randomUUID(),
1494
- pushPublicKey: keys.publicKey,
1495
- pushPrivateKey: keys.privateKey
1496
- }).run();
1497
- }, { behavior: "immediate" });
1498
- function machineInfo() {
1499
- const row = db.select().from(machine).get();
1500
- if (row === void 0) throw new Error("the machine row is missing");
1501
- return row;
1502
- }
1503
- //#endregion
1504
- //#region src/services/projects.ts
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 removeProject(id) {
1551
- db.delete(projects$2).where(eq(projects$2.id, id)).run();
1552
- publish$1();
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
- async function directories(input) {
1555
- const folder = path.resolve(expandHome(input.trim()));
1556
- const entries = await readdir(folder, { withFileTypes: true });
1557
- const parent = path.dirname(folder);
1558
- return {
1559
- path: folder,
1560
- parent: parent === folder ? null : parent,
1561
- directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
1562
- name: entry.name,
1563
- path: path.join(folder, entry.name)
1564
- })).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
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 publish$1() {
1568
- broadcast({
1569
- type: "projects",
1570
- projects: projects$1()
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 checkLogo(logo) {
1574
- const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
1575
- if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
1576
- if (logo.length > LOGO_LIMIT) throw new Error("Use an image smaller than 384 KB");
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 expandHome(input) {
1579
- if (input === "~") return os.homedir();
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
- //#endregion
1584
- //#region src/services/sessions.ts
1585
- const day = 864e5;
1586
- const LIST_TIMEOUT = 1e4;
1587
- const REFRESH_DELAY = 1e3;
1588
- const allProviders = [
1589
- "claude",
1590
- "codex",
1591
- "opencode"
1592
- ];
1593
- const listed = {
1594
- claude: [],
1595
- codex: [],
1596
- opencode: []
1597
- };
1598
- const statuses = {
1599
- claude: {
1600
- provider: "claude",
1601
- state: "loading",
1602
- error: null
1603
- },
1604
- codex: {
1605
- provider: "codex",
1606
- state: "loading",
1607
- error: null
1608
- },
1609
- opencode: {
1610
- provider: "opencode",
1611
- state: "loading",
1612
- error: null
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 providerStatuses() {
1633
- return allProviders.map((provider) => statuses[provider]);
1812
+ function optionValue(options, label) {
1813
+ return options.find((option) => option.label === label)?.value ?? label;
1634
1814
  }
1635
- function listedUpdatedAt(provider, sessionId) {
1636
- return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
1815
+ function contentText(content) {
1816
+ return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
1637
1817
  }
1638
- function providerOf(sessionId) {
1639
- const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
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 recordInteraction(id) {
1644
- db.insert(sessions$1).values({
1645
- sessionId: id,
1646
- interactedAt: Date.now(),
1647
- status: "active"
1648
- }).onConflictDoUpdate({
1649
- target: sessions$1.sessionId,
1650
- set: {
1651
- interactedAt: Date.now(),
1652
- status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
1653
- }
1654
- }).run();
1821
+ function list$1(value) {
1822
+ return Array.isArray(value) ? value : [];
1655
1823
  }
1656
- function publishSessions() {
1657
- broadcast({
1658
- type: "sessions",
1659
- sessions: sessions()
1660
- });
1824
+ function record(value) {
1825
+ return isRecord(value) ? value : {};
1661
1826
  }
1662
- function refreshAll() {
1663
- for (const provider of allProviders) refresh(provider);
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 refreshSessions(provider) {
1666
- if (timers.has(provider)) return;
1667
- timers.set(provider, setTimeout(() => {
1668
- timers.delete(provider);
1669
- refresh(provider);
1670
- }, REFRESH_DELAY));
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 refresh(provider) {
1673
- const current = refreshing.get(provider);
1674
- if (current !== void 0) {
1675
- current.again = true;
1676
- return;
1677
- }
1678
- const entry = { again: true };
1679
- refreshing.set(provider, entry);
1680
- while (entry.again) {
1681
- entry.again = false;
1682
- await list$1(provider);
1683
- }
1684
- refreshing.delete(provider);
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 list$1(provider) {
1687
- const paths = new Map(projects$1().map((project) => [project.path, project.id]));
1688
- const before = statuses[provider];
1689
- let timer;
1690
- try {
1691
- const found = await Promise.race([find(provider, paths), new Promise((_, reject) => {
1692
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
1693
- })]);
1694
- if (found === null) {
1695
- listed[provider] = [];
1696
- statuses[provider] = {
1697
- provider,
1698
- state: "missing",
1699
- error: null
1700
- };
1701
- } else {
1702
- listed[provider] = found;
1703
- statuses[provider] = {
1704
- provider,
1705
- state: "ready",
1706
- error: null
1707
- };
1708
- for (const item of found) providers.set(item.id, provider);
1709
- }
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 find(provider, paths) {
1728
- switch (provider) {
1729
- case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => (await listSessions({
1730
- dir: cwd,
1731
- includeWorktrees: false,
1732
- includeProgrammatic: false
1733
- })).map((item) => ({
1734
- id: item.sessionId,
1735
- provider: "claude",
1736
- title: item.summary,
1737
- projectId,
1738
- branch: item.gitBranch ?? null,
1739
- updatedAt: item.lastModified
1740
- }))))).flat();
1741
- case "codex":
1742
- if (!codexInstalled()) return null;
1743
- return (await codexThreads([...paths.keys()])).flatMap((thread) => {
1744
- const projectId = paths.get(thread.cwd);
1745
- if (projectId === void 0) return [];
1746
- return [{
1747
- id: thread.id,
1748
- provider: "codex",
1749
- title: thread.name ?? thread.preview,
1750
- projectId,
1751
- branch: thread.gitInfo?.branch ?? null,
1752
- updatedAt: thread.updatedAt * 1e3
1753
- }];
1754
- });
1755
- case "opencode":
1756
- if (!openCodeInstalled()) return null;
1757
- return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
1758
- const projectId = paths.get(session.location.directory);
1759
- if (projectId === void 0) return [];
1760
- return [{
1761
- id: session.id,
1762
- provider: "opencode",
1763
- title: session.title ?? "New session",
1764
- projectId,
1765
- branch: null,
1766
- updatedAt: session.time.updated
1767
- }];
1768
- });
1769
- }
1902
+ async function openCodeAgent(agentId, parent) {
1903
+ return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
1770
1904
  }
1771
- function watchSessions() {
1772
- for (const watcher of watchers) watcher.close();
1773
- watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
1774
- try {
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 updateSession(id, patch) {
1785
- if (patch.title !== void 0) switch (providerOf(id)) {
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 removeSession(id) {
1805
- const provider = providerOf(id);
1806
- const run = runs.get(id);
1807
- if (run !== void 0) {
1808
- run.stop();
1809
- await run.closed;
1810
- }
1811
- switch (provider) {
1812
- case "claude":
1813
- await deleteSession(id);
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 fork(sessionId, projectId, messageId) {
1824
- const provider = providerOf(sessionId);
1825
- const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
1826
- providers.set(result.sessionId, provider);
1827
- recordInteraction(result.sessionId);
1828
- refreshSessions(provider);
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 forkWith(provider, sessionId, cwd, messageId) {
1832
- if (provider === "codex") return forkThread(sessionId, messageId);
1833
- if (provider === "opencode") return forkOpenCode(sessionId, messageId);
1834
- if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
1835
- return { sessionId: (await forkSession(sessionId, {
1836
- dir: cwd,
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
- for await (const event of client.event.subscribe({
1890
- signal: controller.signal,
1891
- onActivity: () => silence.refresh()
1892
- })) route(event);
1893
- console.warn("the OpenCode event stream ended");
1894
- } catch (error) {
1895
- console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
1896
- } finally {
1897
- clearTimeout(silence);
1898
- }
1899
- connection = reconnect();
1900
- connection.catch(() => void 0);
1901
- }
1902
- async function reconnect() {
1903
- let failure;
1904
- for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
1905
- await setTimeout$1(RECONNECT_DELAY);
1906
- let client;
1907
- try {
1908
- client = await connect();
1909
- } catch (error) {
1910
- failure = error;
1911
- continue;
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
- listen(client);
1914
- await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
1915
- return client;
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 catchUp(client) {
1924
- const active = await client.session.active();
1925
- for (const run of openCodeRuns()) {
1926
- const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
1927
- run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
1928
- }
1929
- refreshSessions("opencode");
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 route(event) {
1932
- if (listChanges.has(event.type)) refreshSessions("opencode");
1933
- const sessionId = sessionOf(event);
1934
- if (sessionId === null) return;
1935
- const run = runs.get(sessionId);
1936
- if (run?.provider === "opencode") run.receive(event);
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 sessionOf(event) {
1939
- if (event.type === "form.created") return event.data.form.sessionID;
1940
- const data = "data" in event ? event.data : null;
1941
- if (typeof data === "object" && data !== null && "sessionID" in data && typeof data.sessionID === "string") return data.sessionID;
1942
- return null;
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 openCodeRuns() {
1945
- return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
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/items.ts
1949
- const toolStatuses = {
1950
- streaming: "running",
1951
- running: "running",
1952
- completed: "completed",
1953
- error: "failed"
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
- async send(prompt) {
2635
- const client = await openCode();
2636
- const sessionID = this.sessionId;
2637
- const location = { directory: this.cwd };
2638
- const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
2639
- if (match !== null) {
2640
- if (match[1] === "compact" && prompt.images.length === 0) {
2641
- await client.session.compact({ sessionID });
2642
- return;
2643
- }
2644
- if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
2645
- await client.session.command({
2646
- sessionID,
2647
- name: match[1],
2648
- text: match[2],
2649
- files: files(prompt)
2650
- });
2651
- return;
2652
- }
2653
- const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
2654
- if (skill !== void 0) {
2655
- await client.session.prompt({
2656
- sessionID,
2657
- text: match[2],
2658
- files: files(prompt),
2659
- skills: [{ id: skill.id }]
2660
- });
2661
- return;
2662
- }
2663
- }
2664
- await client.session.prompt({
2665
- sessionID,
2666
- text: prompt.text,
2667
- files: files(prompt)
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
- async apply() {
2671
- const client = await openCode();
2672
- const settings = this.settings;
2673
- const model = settings.model ?? this.applied.model;
2674
- if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await client.session.switchModel({
2675
- sessionID: this.sessionId,
2676
- model: modelRef(model, settings.effort)
2677
- });
2678
- if (settings.mode !== null && settings.mode !== this.applied.mode) await client.session.switchAgent({
2679
- sessionID: this.sessionId,
2680
- agent: settings.mode
2681
- });
2682
- this.applied = {
2683
- ...settings,
2684
- model,
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
- ask(permission) {
2689
- const source = permission.source;
2690
- const item = source === void 0 ? void 0 : this.items.get(`${source.messageID}:${source.id}`);
2691
- this.addPending({
2692
- kind: "permission",
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
- addPending(pending) {
2723
- this.pending.set(pending.request.id, pending);
2724
- publish$2(this.sessionId, {
2725
- type: "request",
2726
- sessionId: this.sessionId,
2727
- request: pending.request
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
- finishTurn() {
2732
- this.partial = "";
2733
- const [next, ...rest] = this.queue;
2734
- if (next !== void 0) {
2735
- this.queue = rest;
2736
- this.publishQueue();
2737
- this.startTurn(next.prompt).catch((error) => this.fail(error));
2738
- return;
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
- this.running = false;
2741
- broadcastActivity();
2742
- this.armIdleTimer();
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
- settle(requestId) {
2745
- if (!this.pending.delete(requestId)) return;
2746
- publish$2(this.sessionId, {
2747
- type: "requestDone",
2748
- sessionId: this.sessionId,
2749
- requestId
2750
- });
2751
- broadcastActivity();
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
- put(items) {
2754
- if (items.length === 0) return;
2755
- for (const item of items) this.items.set(item.id, item);
2756
- if (items.some((item) => item.kind === "assistant")) this.partial = "";
2757
- publish$2(this.sessionId, {
2758
- type: "items",
2759
- sessionId: this.sessionId,
2760
- items
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
- fail(error) {
2764
- this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
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
- armIdleTimer() {
2767
- clearTimeout(this.idleTimer);
2768
- if (this.running) return;
2769
- this.idleTimer = setTimeout(() => {
2770
- if (!this.running && this.pending.size === 0) this.stop();
2771
- }, IDLE_LIMIT$1);
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
- end() {
2774
- clearTimeout(this.idleTimer);
2775
- clearTimeout(this.refreshTimer);
2776
- for (const requestId of [...this.pending.keys()]) this.settle(requestId);
2777
- this.running = false;
2778
- broadcastActivity();
2779
- this.close();
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
- publishQueue() {
2782
- publish$2(this.sessionId, {
2783
- type: "queue",
2784
- sessionId: this.sessionId,
2785
- queue: this.queue
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 files(prompt) {
2799
- return prompt.images.map((image) => ({ uri: `data:${image.mediaType};base64,${image.data}` }));
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 openCodeInstalled() ? [...stored, openCodeAccount()] : stored;
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
- accounts: await accounts()
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
- if (accountId === openCodeAccount().id) return openCodeCapabilities(cwd);
2843
- const account = storedAccounts().find((item) => item.id === accountId);
2844
- if (account === void 0) throw new Error("That account was removed");
2845
- switch (account.provider) {
2846
- case "claude": return claudeCapabilities(account, cwd);
2847
- case "codex": return codexCapabilities(account, cwd);
2848
- case "opencode": throw new Error("OpenCode has no stored accounts");
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(id, name, configDir) {
2860
- const account = validAccount(name, configDir);
2861
- saveAccounts(storedAccounts().map((item) => item.id === id ? {
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" || typeof input.tool_input !== "object" || input.tool_input === null || !("message" in input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
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
- value: model.value,
3043
- name: model.displayName,
3044
- description: model.description,
3045
- efforts: model.supportedEffortLevels ?? []
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
- return await probe.initializationResult();
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 id = `${message.uuid}:${index}`;
3196
- const parent = message.parent;
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
- id,
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
- id,
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 { stdout } = await promisify(execFile)("gh", [
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(provider, accountId, cwd) {
4220
- const diff = (await workingDiff(cwd)).slice(0, DIFF_LIMIT);
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 promisify(execFile)("git", args, {
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 instanceof Error && "code" in error && error.code === "ENOENT") return null;
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 ours().length > 0;
4539
+ return targets().length > 0;
4540
+ }
4541
+ function notifierClient() {
4542
+ return notifier === null ? null : notifier.client;
4455
4543
  }
4456
- function pushKey() {
4457
- return { publicKey: machineInfo().pushPublicKey };
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(subscription) {
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 = ours();
4578
+ const subscriptions = targets();
4479
4579
  if (subscriptions.length === 0) return;
4480
- const keys = machineInfo();
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
- }, payload, {
4594
+ }, JSON.stringify({
4595
+ ...trimmed,
4596
+ machineId: subscription.machine
4597
+ }), {
4496
4598
  vapidDetails: {
4497
4599
  subject: SUBJECT,
4498
- publicKey: keys.pushPublicKey,
4499
- privateKey: keys.pushPrivateKey
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 ours() {
4524
- return db.select().from(pushSubscriptions).where(eq(pushSubscriptions.server, server$1)).all();
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", "services", "terminal", "shell-integration");
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
- version
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 || browserOpen()) return;
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 (browserOpen()) return;
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 browserOpen() {
5491
- return [...connections.values()].some((connection) => Date.now() - connection.client.heardAt < SILENCE_LIMIT);
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
- for (const project of projects.values()) {
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().claudeBaseDir;
5636
+ "config.save": async (params) => {
5637
+ const before = readConfig();
5542
5638
  const saved = saveConfig(params);
5543
- if (saved.claudeBaseDir === before) return saved;
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.id, params.name, params.configDir);
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(params.provider, params.accountId, projectPath(params.projectId)),
5654
- "push.key": () => pushKey(),
5655
- "push.subscribe": (params) => {
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(`Scan this to pair a phone with ${HOSTED_APP}:`);
5773
- console.log("");
5774
- console.log(renderUnicodeCompact(links.hosted, { border: 2 }));
5775
- console.log("");
5776
- console.log("Or open one of these links in the browser to pair:");
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(`offsite serve has no web app; open ${HOSTED_APP}`);
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" || url.searchParams.get("protocol") !== String(3) || !authorized(url)) {
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(`Open it on ${HOSTED_APP}`);
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
- const hosted = new URLSearchParams({
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));