offsite 0.0.7 → 0.0.10

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