offsite 0.0.14 → 0.0.15

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 +423 -249
  2. package/apps/server/dist/main.mjs.map +1 -1
  3. package/apps/web/dist/assets/{angular-html-6NaagU1P.js → angular-html-CG4HtNlx.js} +1 -1
  4. package/apps/web/dist/assets/{angular-ts-D8XutLPw.js → angular-ts-CsUs7XSt.js} +1 -1
  5. package/apps/web/dist/assets/{apl-BKJEkz6Z.js → apl-BZNYboe1.js} +1 -1
  6. package/apps/web/dist/assets/{astro-DkrGLOry.js → astro-BJJJ2TCY.js} +1 -1
  7. package/apps/web/dist/assets/{blade-BIc0yPkX.js → blade-DPPHNv9A.js} +1 -1
  8. package/apps/web/dist/assets/{c-Ds8U4gsq.js → c-DewL-caT.js} +1 -1
  9. package/apps/web/dist/assets/{chapel-COXTL8xW.js → chapel-BC24MUCE.js} +1 -1
  10. package/apps/web/dist/assets/{cobol-D0VSGQqK.js → cobol-CrP0JgFd.js} +1 -1
  11. package/apps/web/dist/assets/{coffee-DBrey7sA.js → coffee-Do6yzljS.js} +1 -1
  12. package/apps/web/dist/assets/{cpp-SKUDYlNR.js → cpp-BkpYSJP8.js} +1 -1
  13. package/apps/web/dist/assets/{crystal-CoTn_b8r.js → crystal-CeGzC-WW.js} +1 -1
  14. package/apps/web/dist/assets/{css-BErHy7fN.js → css-Dw9LKU-N.js} +1 -1
  15. package/apps/web/dist/assets/{edge-CACFpVrz.js → edge-DOlsjMpU.js} +1 -1
  16. package/apps/web/dist/assets/{elixir-aPcGiByD.js → elixir-Of98ZUP3.js} +1 -1
  17. package/apps/web/dist/assets/{elm-DN5FfPkL.js → elm-DcO8avoe.js} +1 -1
  18. package/apps/web/dist/assets/{erb-Dj675b5b.js → erb-DGRCSrUQ.js} +1 -1
  19. package/apps/web/dist/assets/{git-rebase-DepNW1h5.js → git-rebase-CF43k64m.js} +1 -1
  20. package/apps/web/dist/assets/{glimmer-js-fCJatm6u.js → glimmer-js-DzTtVcUh.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-ts-YwpKoyPh.js → glimmer-ts-BGpaKkl4.js} +1 -1
  22. package/apps/web/dist/assets/{glsl-PGMqKJzL.js → glsl-B6O-e93J.js} +1 -1
  23. package/apps/web/dist/assets/{graphql-3IK2PC2Y.js → graphql-8XdLbcFd.js} +1 -1
  24. package/apps/web/dist/assets/{hack-C8CJl4zQ.js → hack-DF9S8RPk.js} +1 -1
  25. package/apps/web/dist/assets/{haml-CiYnV6_a.js → haml-D7K9Vy7l.js} +1 -1
  26. package/apps/web/dist/assets/{handlebars-C4m4mY7m.js → handlebars-CodAKXOQ.js} +1 -1
  27. package/apps/web/dist/assets/{html-D1Ar4Xkj.js → html-DINw2T6o.js} +1 -1
  28. package/apps/web/dist/assets/{html-derivative-CwhughXj.js → html-derivative-Bm8ARfNX.js} +1 -1
  29. package/apps/web/dist/assets/{http-pzvpiwNw.js → http-CzdVlK4g.js} +1 -1
  30. package/apps/web/dist/assets/{hurl-hkZwo5Ou.js → hurl-DSRYfTy3.js} +1 -1
  31. package/apps/web/dist/assets/index-DKU43bgp.css +2 -0
  32. package/apps/web/dist/assets/index-_3a5liAb.js +1205 -0
  33. package/apps/web/dist/assets/{java-CjDcLNWV.js → java-CJrEr5nb.js} +1 -1
  34. package/apps/web/dist/assets/{javascript-B1pleYoe.js → javascript-BRzSAlK0.js} +1 -1
  35. package/apps/web/dist/assets/{jinja-erX4wFzj.js → jinja-BCkNTi07.js} +1 -1
  36. package/apps/web/dist/assets/{jison-CajpWJAS.js → jison-CuVfSCOU.js} +1 -1
  37. package/apps/web/dist/assets/{json-CwL6i1-C.js → json-CivFBkRR.js} +1 -1
  38. package/apps/web/dist/assets/{jsx-DmOLIUT0.js → jsx-Bn8oTRCQ.js} +1 -1
  39. package/apps/web/dist/assets/{julia-DK5bpj3S.js → julia-S7B4dPpO.js} +1 -1
  40. package/apps/web/dist/assets/{just-BtwHGit4.js → just-BkwYofB3.js} +1 -1
  41. package/apps/web/dist/assets/{latex-Dnf1mLFf.js → latex-CHtrQEQW.js} +1 -1
  42. package/apps/web/dist/assets/{liquid-Bv72E2Is.js → liquid-BiWMrFWE.js} +1 -1
  43. package/apps/web/dist/assets/{lua-94ufuXur.js → lua-DH2dy4b5.js} +1 -1
  44. package/apps/web/dist/assets/{marko-BKL0CGzx.js → marko-D1J2FQwH.js} +1 -1
  45. package/apps/web/dist/assets/{mdc-DNhqDQoU.js → mdc-DdU0BPrI.js} +1 -1
  46. package/apps/web/dist/assets/{nginx-BcYwGlC2.js → nginx-aVr0h4x0.js} +1 -1
  47. package/apps/web/dist/assets/{nim-CRMgcK-K.js → nim-D3uKiFs9.js} +1 -1
  48. package/apps/web/dist/assets/{org-DfnsnSld.js → org-Dlc-mmBJ.js} +1 -1
  49. package/apps/web/dist/assets/{perl-BEwyXv2a.js → perl-Db2FD7eY.js} +1 -1
  50. package/apps/web/dist/assets/{php-CG9qDJ82.js → php-BKnY42CF.js} +1 -1
  51. package/apps/web/dist/assets/{pug-Dg3kkpP7.js → pug-Dv1XMHZo.js} +1 -1
  52. package/apps/web/dist/assets/{qml-Vs1TgYWl.js → qml-DLrUK6Mn.js} +1 -1
  53. package/apps/web/dist/assets/{r-CFy73Kus.js → r-YY5yALTA.js} +1 -1
  54. package/apps/web/dist/assets/{razor-Dcng6YrP.js → razor-CHwb0eZk.js} +1 -1
  55. package/apps/web/dist/assets/{regexp-Bwbv2WPD.js → regexp-BbKQ-CAd.js} +1 -1
  56. package/apps/web/dist/assets/{rst-Cul2jxkE.js → rst-CfgFwgqW.js} +1 -1
  57. package/apps/web/dist/assets/{ruby-TMvLq4yj.js → ruby-CWYxEIgK.js} +1 -1
  58. package/apps/web/dist/assets/{sas-D8HC__R6.js → sas-BIhkpR95.js} +1 -1
  59. package/apps/web/dist/assets/{scss-dK6Y8mdp.js → scss-CD48eC-9.js} +1 -1
  60. package/apps/web/dist/assets/{shellscript-BrObD5cd.js → shellscript-DQJbmpgk.js} +1 -1
  61. package/apps/web/dist/assets/{shellsession-gwVDz4P8.js → shellsession-BsbY07m-.js} +1 -1
  62. package/apps/web/dist/assets/{soy-BF1mzKmn.js → soy-2Y50DZFi.js} +1 -1
  63. package/apps/web/dist/assets/{sql-rY-oq1Hg.js → sql-nKnfvr4s.js} +1 -1
  64. package/apps/web/dist/assets/{stata-DBh-WI4L.js → stata-Dqj31JrM.js} +1 -1
  65. package/apps/web/dist/assets/{surrealql-DxG1DYQM.js → surrealql-DjKPQgK9.js} +1 -1
  66. package/apps/web/dist/assets/{svelte-IUUmpoTZ.js → svelte-1HUOD4gz.js} +1 -1
  67. package/apps/web/dist/assets/{templ-DSD9ChjE.js → templ-CjuUG0Pz.js} +1 -1
  68. package/apps/web/dist/assets/{tex-ClrklzW1.js → tex-DImgjGWJ.js} +1 -1
  69. package/apps/web/dist/assets/{ts-tags-CiWqjPoE.js → ts-tags-uu3b2KWy.js} +1 -1
  70. package/apps/web/dist/assets/{tsx-BVdH_4RP.js → tsx-DLBT5mtF.js} +1 -1
  71. package/apps/web/dist/assets/{twig-bBlRyqC3.js → twig-CUqAREOD.js} +1 -1
  72. package/apps/web/dist/assets/{typescript-BWvsrxJs.js → typescript-BJx9F5MO.js} +1 -1
  73. package/apps/web/dist/assets/{typst-CoUU-sfH.js → typst-DGjRmWJl.js} +1 -1
  74. package/apps/web/dist/assets/{vue-0_DBul4P.js → vue-D4sa_D6w.js} +1 -1
  75. package/apps/web/dist/assets/{vue-html-feH37mKg.js → vue-html-D9bXwnxX.js} +1 -1
  76. package/apps/web/dist/assets/{vue-vine-xWSVrEzZ.js → vue-vine-CltCK1Bg.js} +1 -1
  77. package/apps/web/dist/assets/{xml-RE-6RA5e.js → xml-BeUIBscE.js} +1 -1
  78. package/apps/web/dist/assets/{xsl-DzQQ-zeR.js → xsl-Dp0u2Kgu.js} +1 -1
  79. package/apps/web/dist/assets/{yaml-BfSzomC9.js → yaml-BSM_mxxD.js} +1 -1
  80. package/apps/web/dist/index.html +2 -2
  81. package/apps/web/dist/sw.js +1 -1
  82. package/package.json +1 -1
  83. package/apps/web/dist/assets/index-6ofGnhX4.css +0 -2
  84. package/apps/web/dist/assets/index-BOrJ0-iI.js +0 -1203
@@ -20,26 +20,9 @@ import { drizzle } from "drizzle-orm/node-sqlite";
20
20
  import { migrate } from "drizzle-orm/node-sqlite/migrator";
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
- import { promisify } from "node:util";
24
23
  import webPush from "web-push";
24
+ import { promisify } from "node:util";
25
25
  import { spawn as spawn$1 } from "@lydell/node-pty";
26
- //#region ../core/src/protocol.ts
27
- function encodeFrame(kind, id, data) {
28
- const frame = new Uint8Array(5 + data.length);
29
- frame[0] = kind;
30
- new DataView(frame.buffer).setUint32(1, id);
31
- frame.set(data, 5);
32
- return frame;
33
- }
34
- function decodeFrame(frame) {
35
- if (frame.length < 5) throw new Error("terminal frame is too short");
36
- return {
37
- kind: frame[0],
38
- id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
39
- data: frame.subarray(5)
40
- };
41
- }
42
- //#endregion
43
26
  //#region src/constants.ts
44
27
  const deprecatedModels = {
45
28
  claude: [
@@ -260,7 +243,8 @@ const privateEntries = [
260
243
  ];
261
244
  const windowNames = /* @__PURE__ */ new Map([[300, "five_hour"], [10080, "seven_day"]]);
262
245
  async function codexAccount(account) {
263
- const linked = isMirror(account.configDir);
246
+ const configDir = account.configDir ?? codexBaseDir;
247
+ const linked = isMirror(configDir);
264
248
  const base = {
265
249
  ...account,
266
250
  email: null,
@@ -269,11 +253,11 @@ async function codexAccount(account) {
269
253
  linked
270
254
  };
271
255
  try {
272
- const server = appServer(account.configDir);
256
+ const server = appServer(configDir);
273
257
  const result = await server.request("account/read", {});
274
258
  if (result.account === null) return {
275
259
  ...base,
276
- error: `Not logged in. Run CODEX_HOME=${account.configDir} codex login in a terminal`,
260
+ error: `Not logged in. Run CODEX_HOME=${configDir} codex login in a terminal`,
277
261
  valid: false
278
262
  };
279
263
  switch (result.account.type) {
@@ -307,7 +291,7 @@ async function codexAccount(account) {
307
291
  }
308
292
  }
309
293
  async function codexCapabilities(account, cwd) {
310
- const server = appServer(account.configDir);
294
+ const server = appServer(account.configDir ?? codexBaseDir);
311
295
  const models = [];
312
296
  let cursor = null;
313
297
  do {
@@ -338,7 +322,7 @@ async function skills(server, cwd) {
338
322
  return (await server.request("skills/list", { cwds: [cwd] })).data.flatMap((entry) => entry.skills.filter((skill) => skill.enabled));
339
323
  }
340
324
  function publishCodexLimits(home, snapshot) {
341
- for (const account of storedAccounts()) if (account.provider === "codex" && account.configDir === home) broadcast({
325
+ for (const account of storedAccounts()) if (account.provider === "codex" && (account.configDir ?? codexBaseDir) === home) broadcast({
342
326
  type: "limits",
343
327
  accountId: account.id,
344
328
  limits: toLimits(snapshot)
@@ -703,7 +687,7 @@ function record$1(value) {
703
687
  //#region src/args.ts
704
688
  let command = "web";
705
689
  let name = null;
706
- 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 pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline");
690
+ 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 pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline").configureHelp({ showGlobalOptions: true });
707
691
  program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
708
692
  program.command("serve").description("Starts only the server, without the web app").action(() => {
709
693
  command = "serve";
@@ -717,6 +701,7 @@ program.command("unpair").description("Picks paired browsers to unpair").action(
717
701
  });
718
702
  program.parse();
719
703
  const options = program.opts();
704
+ if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
720
705
  const args = {
721
706
  ...options,
722
707
  dir: path.resolve(options.dir),
@@ -1063,7 +1048,7 @@ const presets = /* @__PURE__ */ new Map([
1063
1048
  }]
1064
1049
  ]);
1065
1050
  function codexServer(settings) {
1066
- return appServer(storedAccount("codex", settings.accountId).configDir);
1051
+ return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
1067
1052
  }
1068
1053
  var CodexRun = class CodexRun {
1069
1054
  provider = "codex";
@@ -1491,15 +1476,15 @@ async function revertThread(sessionId, messageId) {
1491
1476
  beforeTurnId: turnOf(messageId)
1492
1477
  });
1493
1478
  }
1494
- async function codexCommitMessage(accountId, cwd, model, prompt) {
1495
- const server = appServer(storedAccount("codex", accountId).configDir);
1479
+ async function codexText(accountId, cwd, model, instructions, prompt) {
1480
+ const server = appServer(storedAccount("codex", accountId).configDir ?? codexBaseDir);
1496
1481
  const threadId = (await server.request("thread/start", {
1497
1482
  cwd,
1498
1483
  model,
1499
1484
  ephemeral: true,
1500
1485
  approvalPolicy: "never",
1501
1486
  sandbox: "read-only",
1502
- developerInstructions: "You write concise git commit messages."
1487
+ developerInstructions: instructions
1503
1488
  })).thread.id;
1504
1489
  const reply = Promise.withResolvers();
1505
1490
  let text = "";
@@ -1527,7 +1512,7 @@ async function codexCommitMessage(accountId, cwd, model, prompt) {
1527
1512
  }],
1528
1513
  model
1529
1514
  });
1530
- return { message: await reply.promise };
1515
+ return await reply.promise;
1531
1516
  } finally {
1532
1517
  server.threads.delete(threadId);
1533
1518
  await server.request("thread/unsubscribe", { threadId });
@@ -1812,6 +1797,26 @@ function record(value) {
1812
1797
  return isRecord(value) ? value : {};
1813
1798
  }
1814
1799
  //#endregion
1800
+ //#region src/lib/opencode/requests.ts
1801
+ function permissionRequest(permission, item) {
1802
+ return {
1803
+ id: permission.id,
1804
+ kind: "approval",
1805
+ title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
1806
+ canAlwaysAllow: (permission.save ?? []).length > 0,
1807
+ item: item ?? {
1808
+ id: permission.id,
1809
+ parent: null,
1810
+ time: Date.now(),
1811
+ kind: "tool",
1812
+ name: permission.action,
1813
+ input: { resources: permission.resources },
1814
+ output: "",
1815
+ status: "running"
1816
+ }
1817
+ };
1818
+ }
1819
+ //#endregion
1815
1820
  //#region src/lib/opencode/sessions.ts
1816
1821
  const PAGE = 100;
1817
1822
  const COMMIT_LIMIT = 12e4;
@@ -1927,7 +1932,7 @@ async function rewindOpenCode(request) {
1927
1932
  else await client.session.revert.commit({ sessionID });
1928
1933
  return result;
1929
1934
  }
1930
- async function openCodeCommitMessage(cwd, model, prompt) {
1935
+ async function openCodeText(cwd, model, prompt) {
1931
1936
  const client = await openCode();
1932
1937
  const session = await client.session.create({
1933
1938
  location: { directory: cwd },
@@ -1946,7 +1951,7 @@ async function openCodeCommitMessage(cwd, model, prompt) {
1946
1951
  if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
1947
1952
  if (message.type !== "assistant") continue;
1948
1953
  const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
1949
- if (text !== "") return { message: text };
1954
+ if (text !== "") return text;
1950
1955
  }
1951
1956
  throw new Error("OpenCode did not reply");
1952
1957
  } finally {
@@ -1994,6 +1999,15 @@ function openCodeRun(sessionId) {
1994
1999
  const run = runs.get(sessionId);
1995
2000
  return run?.provider === "opencode" ? run : void 0;
1996
2001
  }
2002
+ function sessionSettings(session, settings) {
2003
+ const variant = session.model?.variant;
2004
+ return {
2005
+ ...settings,
2006
+ model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
2007
+ effort: variant === void 0 || variant === "default" ? null : variant,
2008
+ mode: session.agent ?? null
2009
+ };
2010
+ }
1997
2011
  //#endregion
1998
2012
  //#region src/lib/opencode/run.ts
1999
2013
  const IDLE_LIMIT$1 = 18e5;
@@ -2270,22 +2284,7 @@ var OpenCodeRun = class OpenCodeRun {
2270
2284
  const item = source === void 0 ? void 0 : this.items.get(`${source.messageID}:${source.id}`);
2271
2285
  this.addPending({
2272
2286
  kind: "permission",
2273
- request: {
2274
- id: permission.id,
2275
- kind: "approval",
2276
- title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
2277
- canAlwaysAllow: (permission.save ?? []).length > 0,
2278
- item: item ?? {
2279
- id: permission.id,
2280
- parent: null,
2281
- time: Date.now(),
2282
- kind: "tool",
2283
- name: permission.action,
2284
- input: { resources: permission.resources },
2285
- output: "",
2286
- status: "running"
2287
- }
2288
- }
2287
+ request: permissionRequest(permission, item)
2289
2288
  });
2290
2289
  }
2291
2290
  askForm(form) {
@@ -2367,15 +2366,6 @@ var OpenCodeRun = class OpenCodeRun {
2367
2366
  });
2368
2367
  }
2369
2368
  };
2370
- function sessionSettings(session, settings) {
2371
- const variant = session.model?.variant;
2372
- return {
2373
- ...settings,
2374
- model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
2375
- effort: variant === void 0 || variant === "default" ? null : variant,
2376
- mode: session.agent ?? null
2377
- };
2378
- }
2379
2369
  function files(prompt) {
2380
2370
  return prompt.images.map((image) => ({ uri: `data:${image.mediaType};base64,${image.data}` }));
2381
2371
  }
@@ -2790,7 +2780,7 @@ const listChanges = /* @__PURE__ */ new Set([
2790
2780
  "session.execution.failed",
2791
2781
  "session.execution.interrupted"
2792
2782
  ]);
2793
- const SILENCE_LIMIT$1 = 45e3;
2783
+ const SILENCE_LIMIT$2 = 45e3;
2794
2784
  const RECONNECT_ATTEMPTS = 5;
2795
2785
  const RECONNECT_DELAY = 2e3;
2796
2786
  let connection;
@@ -2833,7 +2823,7 @@ async function connect() {
2833
2823
  async function listen(client) {
2834
2824
  const controller = new AbortController();
2835
2825
  listener = controller;
2836
- const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
2826
+ const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$2);
2837
2827
  try {
2838
2828
  for await (const event of client.event.subscribe({
2839
2829
  signal: controller.signal,
@@ -2842,7 +2832,7 @@ async function listen(client) {
2842
2832
  console.warn("the OpenCode event stream ended");
2843
2833
  } catch (error) {
2844
2834
  if (listener !== controller) return;
2845
- console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
2835
+ console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
2846
2836
  } finally {
2847
2837
  clearTimeout(silence);
2848
2838
  }
@@ -2907,14 +2897,13 @@ function providerName(provider) {
2907
2897
  }
2908
2898
  function storedAccounts() {
2909
2899
  const config = readConfig();
2910
- return [...config.claudeAccounts, ...config.codexAccounts].flatMap((item) => {
2911
- const configDir = item.configDir[platform()];
2912
- if (configDir === null) return [];
2913
- return [{
2914
- ...item,
2915
- configDir
2916
- }];
2917
- });
2900
+ return [...config.claudeAccounts.map((item) => ({
2901
+ ...item,
2902
+ configDir: item.configDir[platform()]
2903
+ })), ...config.codexAccounts.map((item) => ({
2904
+ ...item,
2905
+ configDir: item.configDir[platform()]
2906
+ }))];
2918
2907
  }
2919
2908
  function assertEnabled(provider, accountId) {
2920
2909
  if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
@@ -2935,7 +2924,7 @@ async function stopAccount(account) {
2935
2924
  const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
2936
2925
  for (const run of stopping) run.stop();
2937
2926
  await Promise.all(stopping.map((run) => run.closed));
2938
- if (account.provider === "codex") stopAppServer(account.configDir);
2927
+ if (account.provider === "codex") stopAppServer(account.configDir ?? codexBaseDir);
2939
2928
  if (account.provider === "claude") forgetClaude(account.id);
2940
2929
  }
2941
2930
  function storedAccount(provider, accountId) {
@@ -3063,7 +3052,15 @@ function removeAccount(id) {
3063
3052
  }
3064
3053
  function validAccount(name, configDir) {
3065
3054
  if (name.trim() === "") throw new Error("Give the account a name");
3055
+ if (configDir === null) return {
3056
+ name: name.trim(),
3057
+ configDir: null
3058
+ };
3066
3059
  const dir = configDir.trim();
3060
+ if (dir === "") return {
3061
+ name: name.trim(),
3062
+ configDir: null
3063
+ };
3067
3064
  if (dir === "~" || dir.startsWith("~/")) return {
3068
3065
  name: name.trim(),
3069
3066
  configDir: path.join(os.homedir(), dir.slice(1))
@@ -3075,6 +3072,117 @@ function validAccount(name, configDir) {
3075
3072
  throw new Error("Use an absolute config directory or one starting with ~/");
3076
3073
  }
3077
3074
  //#endregion
3075
+ //#region src/services/push.ts
3076
+ const TITLE_LIMIT = 100;
3077
+ const BODY_LIMIT = 500;
3078
+ const TIME_TO_LIVE = 900;
3079
+ const SEND_TIMEOUT = 1e4;
3080
+ const SILENCE_LIMIT$1 = 9e4;
3081
+ const SUBJECT = "https://github.com/NazmusSayad/offsite";
3082
+ let server$1 = "";
3083
+ let notifier = null;
3084
+ function startPush(address) {
3085
+ server$1 = address;
3086
+ }
3087
+ function pushActive() {
3088
+ return targets().length > 0;
3089
+ }
3090
+ function notifierClient() {
3091
+ return notifier === null ? null : notifier.client;
3092
+ }
3093
+ function notifierConnected() {
3094
+ if (notifier === null || !clients.has(notifier.client)) return false;
3095
+ return Date.now() - notifier.client.heardAt < SILENCE_LIMIT$1;
3096
+ }
3097
+ function claimNotifier(client, endpoint) {
3098
+ notifier = {
3099
+ client,
3100
+ endpoint
3101
+ };
3102
+ for (const item of clients) item.send({
3103
+ type: "notifier",
3104
+ active: item === client
3105
+ });
3106
+ }
3107
+ function subscribe(registration, client) {
3108
+ if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
3109
+ const values = {
3110
+ endpoint: registration.subscription.endpoint,
3111
+ p256dh: registration.subscription.keys.p256dh,
3112
+ auth: registration.subscription.keys.auth,
3113
+ publicKey: registration.publicKey,
3114
+ privateKey: registration.privateKey,
3115
+ machine: registration.machine,
3116
+ server: server$1
3117
+ };
3118
+ db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
3119
+ target: pushSubscriptions.endpoint,
3120
+ set: values
3121
+ }).run();
3122
+ if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
3123
+ console.log("a browser subscribed to push notifications");
3124
+ }
3125
+ function unsubscribe(endpoint) {
3126
+ db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
3127
+ if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
3128
+ console.log("a browser unsubscribed from push notifications");
3129
+ }
3130
+ async function sendPush(message) {
3131
+ const subscriptions = targets();
3132
+ if (subscriptions.length === 0) return false;
3133
+ const trimmed = {
3134
+ ...message,
3135
+ title: [...message.title].slice(0, TITLE_LIMIT).join(""),
3136
+ body: [...message.body].slice(0, BODY_LIMIT).join("")
3137
+ };
3138
+ const gone = [];
3139
+ let sent = false;
3140
+ await Promise.all(subscriptions.map(async (subscription) => {
3141
+ try {
3142
+ await webPush.sendNotification({
3143
+ endpoint: subscription.endpoint,
3144
+ keys: {
3145
+ p256dh: subscription.p256dh,
3146
+ auth: subscription.auth
3147
+ }
3148
+ }, JSON.stringify({
3149
+ ...trimmed,
3150
+ machineId: subscription.machine
3151
+ }), {
3152
+ vapidDetails: {
3153
+ subject: SUBJECT,
3154
+ publicKey: subscription.publicKey,
3155
+ privateKey: subscription.privateKey
3156
+ },
3157
+ TTL: TIME_TO_LIVE,
3158
+ urgency: "high",
3159
+ timeout: SEND_TIMEOUT
3160
+ });
3161
+ sent = true;
3162
+ } catch (error) {
3163
+ const status = error.statusCode;
3164
+ if (status !== void 0 && [
3165
+ 401,
3166
+ 403,
3167
+ 404,
3168
+ 410
3169
+ ].includes(status)) {
3170
+ console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
3171
+ gone.push(subscription.endpoint);
3172
+ return;
3173
+ }
3174
+ console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
3175
+ }
3176
+ }));
3177
+ if (gone.length === 0) return sent;
3178
+ db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
3179
+ return sent;
3180
+ }
3181
+ function targets() {
3182
+ if (notifier === null || notifier.endpoint === null) return [];
3183
+ return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
3184
+ }
3185
+ //#endregion
3078
3186
  //#region src/lib/claude/claude.ts
3079
3187
  const efforts = [
3080
3188
  "low",
@@ -3116,13 +3224,13 @@ function claudeMode(value) {
3116
3224
  return mode;
3117
3225
  }
3118
3226
  function claudeOptions(accountId, cwd) {
3119
- const account = storedAccount("claude", accountId);
3227
+ const configDir = storedAccount("claude", accountId).configDir ?? claudeBaseDir();
3120
3228
  const env = {
3121
3229
  ...process.env,
3122
3230
  CLAUDE_CODE_ENTRYPOINT: "offsite",
3123
- CLAUDE_CONFIG_DIR: account.configDir
3231
+ CLAUDE_CONFIG_DIR: configDir
3124
3232
  };
3125
- if (path.resolve(account.configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
3233
+ if (path.resolve(configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
3126
3234
  return {
3127
3235
  cwd,
3128
3236
  env,
@@ -3135,23 +3243,56 @@ function claudeOptions(accountId, cwd) {
3135
3243
  matcher: "PushNotification",
3136
3244
  hooks: [async (input) => {
3137
3245
  if (input.hook_event_name !== "PostToolUse" || !isRecord(input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
3246
+ const message = input.tool_input.message;
3138
3247
  broadcast({
3139
3248
  type: "notification",
3140
3249
  sessionId: input.session_id,
3141
- message: input.tool_input.message
3250
+ message
3142
3251
  });
3143
- return { hookSpecificOutput: {
3252
+ if (notifierConnected()) return { hookSpecificOutput: {
3144
3253
  hookEventName: "PostToolUse",
3145
3254
  updatedToolOutput: {
3146
- message: input.tool_input.message,
3255
+ message,
3147
3256
  localSent: true,
3148
3257
  disabledReason: "no_transport"
3149
3258
  }
3150
3259
  } };
3260
+ const session = sessions().find((item) => item.id === input.session_id);
3261
+ if (session === void 0) throw new Error(`Session ${input.session_id} is not listed`);
3262
+ const pushSent = await sendPush({
3263
+ kind: "session",
3264
+ title: session.title,
3265
+ body: message,
3266
+ projectId: session.projectId,
3267
+ sessionId: session.id
3268
+ });
3269
+ return { hookSpecificOutput: {
3270
+ hookEventName: "PostToolUse",
3271
+ updatedToolOutput: {
3272
+ message,
3273
+ localSent: false,
3274
+ pushSent,
3275
+ ...pushSent ? {} : { disabledReason: "no_transport" }
3276
+ }
3277
+ } };
3151
3278
  }]
3152
3279
  }] }
3153
3280
  };
3154
3281
  }
3282
+ function runOptions(options) {
3283
+ const settings = options.settings;
3284
+ return {
3285
+ ...claudeOptions(settings.accountId, options.cwd),
3286
+ ...options.resume ? { resume: options.sessionId } : { sessionId: options.sessionId },
3287
+ ...options.resumeAt === null ? {} : { resumeSessionAt: options.resumeAt },
3288
+ enableFileCheckpointing: true,
3289
+ ...settings.mode === null ? {} : { permissionMode: claudeMode(settings.mode) },
3290
+ ...settings.model === null ? {} : { model: settings.model },
3291
+ ...settings.effort === null ? {} : { effort: claudeEffort(settings.effort) },
3292
+ allowDangerouslySkipPermissions: true,
3293
+ includePartialMessages: true
3294
+ };
3295
+ }
3155
3296
  async function claudeCapabilities(account, cwd) {
3156
3297
  const result = await initialization(account.id, cwd);
3157
3298
  return {
@@ -3173,7 +3314,7 @@ async function claudeCapabilities(account, cwd) {
3173
3314
  };
3174
3315
  }
3175
3316
  async function claudeAccount(account) {
3176
- const linked = isLinked(account.configDir);
3317
+ const linked = isLinked(account.configDir ?? claudeBaseDir());
3177
3318
  try {
3178
3319
  const info = (await initialization(account.id, os.homedir())).account;
3179
3320
  return {
@@ -3894,19 +4035,10 @@ var Run = class {
3894
4035
  this.sessionId = options.sessionId;
3895
4036
  this.resumeAt = options.resumeAt;
3896
4037
  this.settings = options.settings;
3897
- const settings = options.settings;
3898
4038
  this.query = query({
3899
4039
  prompt: this.input(),
3900
4040
  options: {
3901
- ...claudeOptions(settings.accountId, options.cwd),
3902
- ...options.resume ? { resume: options.sessionId } : { sessionId: options.sessionId },
3903
- ...options.resumeAt === null ? {} : { resumeSessionAt: options.resumeAt },
3904
- enableFileCheckpointing: true,
3905
- ...settings.mode === null ? {} : { permissionMode: claudeMode(settings.mode) },
3906
- ...settings.model === null ? {} : { model: settings.model },
3907
- ...settings.effort === null ? {} : { effort: claudeEffort(settings.effort) },
3908
- allowDangerouslySkipPermissions: true,
3909
- includePartialMessages: true,
4041
+ ...runOptions(options),
3910
4042
  canUseTool: (toolName, input, context) => this.ask({
3911
4043
  toolName,
3912
4044
  input,
@@ -4252,85 +4384,181 @@ const UNTRACKED_LIMIT = 100;
4252
4384
  const NEW_FILE_LIMIT = 262144;
4253
4385
  const REPO_DEPTH = 3;
4254
4386
  const run$1 = promisify(execFile);
4255
- async function gitStatus(cwd) {
4256
- let output;
4257
- try {
4258
- output = await git(cwd, [
4259
- "status",
4260
- "--porcelain=v2",
4261
- "--branch"
4262
- ]);
4263
- } catch {
4264
- return null;
4265
- }
4266
- const status = {
4267
- branch: null,
4268
- upstream: null,
4269
- ahead: 0,
4270
- behind: 0,
4271
- changed: 0
4272
- };
4273
- for (const line of output.stdout.split("\n").filter(Boolean)) {
4274
- const [marker, key, ...rest] = line.split(" ");
4275
- if (marker !== "#") status.changed++;
4276
- else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
4277
- else if (key === "branch.upstream") status.upstream = rest[0];
4278
- else if (key === "branch.ab") {
4279
- status.ahead = Number(rest[0]);
4280
- status.behind = -Number(rest[1]);
4281
- }
4282
- }
4283
- return status;
4284
- }
4285
- async function commit(cwd, message) {
4286
- if (message.trim() === "") throw new Error("Write a commit message first");
4387
+ const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
4388
+
4389
+ ## TITLE RULES
4390
+
4391
+ - Use the conventional commits format: type(scope): subject
4392
+ - Types: feat, fix, docs, style, refactor, perf, test, chore, ci
4393
+ - Scope is optional; include it when the change targets a clear area
4394
+ - Subject: imperative mood, lowercase, no trailing period
4395
+ - Maximum 72 characters in total
4396
+ - Describe the overall change, not a single commit
4397
+
4398
+ ## BODY RULES
4399
+
4400
+ - Use short Markdown sections with level-2 headings. Choose only sections that fit the change: \`New features\`, \`Improvements\`, \`Fixes\`, \`Before\` / \`After\`, or \`Breaking changes\`. Do not use all of them as a template.
4401
+ - Use bullet points under every section, never paragraphs. Each bullet describes one feature or change in one short sentence.
4402
+ - For a small change, use one section with one bullet. For larger changes, use only the sections needed, with at most three short bullets across the body.
4403
+ - Aim for 20-50 words; fewer is better when enough. Never exceed 80 words.
4404
+ - Describe the outcome, not the implementation walkthrough. Do not enumerate changed files, functions, commits, or incidental cleanup; reviewers can read the diff.
4405
+ - Add a \`Breaking changes\` section only for an actual compatibility break or required migration.
4406
+ - No generic \`Summary\` section, bold labels, checklists, boilerplate openings like "This pull request", or closing summaries.
4407
+
4408
+ ## RULES
4409
+
4410
+ Base every statement on the provided commits and diff; do NOT invent details.
4411
+
4412
+ ## OUTPUT
4413
+
4414
+ Return one Markdown document. The title is the level-1 heading (\`# <title>\`), and everything after it is the body.`;
4415
+ async function gitRepos(cwd) {
4416
+ return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
4417
+ ...await gitStatus(path.join(cwd, repo)),
4418
+ path: repo
4419
+ })));
4420
+ }
4421
+ async function commit(cwd, message, andPush) {
4422
+ const text = message.trim() === "" ? await commitMessage(cwd) : message;
4287
4423
  await git(cwd, ["add", "--all"]);
4288
4424
  await git(cwd, [
4289
4425
  "commit",
4290
4426
  "--message",
4291
- message
4427
+ text
4292
4428
  ]);
4293
- return gitStatus(cwd);
4429
+ if (andPush) try {
4430
+ await push$1(cwd);
4431
+ } catch (error) {
4432
+ throw new Error(`Committed, but push failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4433
+ }
4434
+ return { message: text };
4294
4435
  }
4295
4436
  async function push$1(cwd) {
4296
4437
  const status = await gitStatus(cwd);
4297
- if (status === null) throw new Error(`${cwd} is not a git repository`);
4298
- if (status.upstream === null) await git(cwd, [
4299
- "push",
4300
- "--set-upstream",
4438
+ if (status.branch === null) throw new Error("Cannot push a detached HEAD");
4439
+ try {
4440
+ if (status.upstream === null) await git(cwd, [
4441
+ "push",
4442
+ "--set-upstream",
4443
+ "origin",
4444
+ "HEAD"
4445
+ ]);
4446
+ else await git(cwd, ["push"]);
4447
+ } catch (error) {
4448
+ throw new Error((error instanceof Error ? error.message : String(error)).split("\n").filter((line) => line.trim() !== "" && !line.startsWith("hint:") && !line.startsWith("Command failed:")).join("\n"), { cause: error });
4449
+ }
4450
+ }
4451
+ async function pullRequestBranches(cwd) {
4452
+ const head = (await gitStatus(cwd)).branch;
4453
+ if (head === null) throw new Error("Not on a branch");
4454
+ const base = (await run$1("gh", [
4455
+ "repo",
4456
+ "view",
4457
+ "--json",
4458
+ "defaultBranchRef",
4459
+ "--jq",
4460
+ ".defaultBranchRef.name"
4461
+ ], { cwd })).stdout.trim();
4462
+ if (base === head) throw new Error(`${head} is the default branch. Create a branch first.`);
4463
+ return {
4464
+ head,
4465
+ base,
4466
+ branches: (await git(cwd, [
4467
+ "ls-remote",
4468
+ "--heads",
4469
+ "origin"
4470
+ ])).stdout.split("\n").filter(Boolean).map((line) => line.split("refs/heads/")[1]).filter((branch) => branch !== head)
4471
+ };
4472
+ }
4473
+ async function preparePullRequest(cwd, base, commitChanges) {
4474
+ const head = (await gitStatus(cwd)).branch;
4475
+ if (head === null) throw new Error("Not on a branch");
4476
+ if (commitChanges) await commit(cwd, "", true);
4477
+ const status = await gitStatus(cwd);
4478
+ if (status.upstream === null || status.ahead > 0) await push$1(cwd);
4479
+ const open = (await run$1("gh", [
4480
+ "pr",
4481
+ "list",
4482
+ "--head",
4483
+ head,
4484
+ "--base",
4485
+ base,
4486
+ "--state",
4487
+ "open",
4488
+ "--json",
4489
+ "url",
4490
+ "--jq",
4491
+ ".[0].url // \"\""
4492
+ ], { cwd })).stdout.trim();
4493
+ if (open !== "") return {
4494
+ kind: "open",
4495
+ url: open
4496
+ };
4497
+ await git(cwd, [
4498
+ "fetch",
4301
4499
  "origin",
4302
- "HEAD"
4500
+ base
4303
4501
  ]);
4304
- else await git(cwd, ["push"]);
4305
- return gitStatus(cwd);
4502
+ const commits = (await git(cwd, [
4503
+ "log",
4504
+ "--format=- %B",
4505
+ `origin/${base}..HEAD`
4506
+ ])).stdout.trim();
4507
+ if (commits === "") throw new Error(`No commits between ${base} and ${head}`);
4508
+ const diff = (await git(cwd, [
4509
+ "diff",
4510
+ "--no-color",
4511
+ "--no-ext-diff",
4512
+ `origin/${base}...HEAD`
4513
+ ])).stdout.slice(0, DIFF_LIMIT);
4514
+ const lines = (await generateText(cwd, PULL_REQUEST_INSTRUCTIONS, `Commits between base and head:\n${commits}\n\n${diff}\n\nGenerate the pull request Markdown document from the above.`)).split("\n");
4515
+ const title = lines[0].trim().replace(/^#{1,6}\s*/, "").replace(/^(\*\*|__|\*|_|`)+/, "").replace(/(\*\*|__|\*|_|`)+$/, "").trim();
4516
+ if (title === "") throw new Error("The model returned an empty title");
4517
+ return {
4518
+ kind: "draft",
4519
+ base,
4520
+ head,
4521
+ title,
4522
+ body: lines.slice(1).join("\n").trim()
4523
+ };
4306
4524
  }
4307
- async function createPullRequest(cwd) {
4525
+ async function createPullRequest(cwd, request) {
4308
4526
  const url = (await run$1("gh", [
4309
4527
  "pr",
4310
4528
  "create",
4311
- "--fill"
4529
+ "--title",
4530
+ request.title,
4531
+ "--body",
4532
+ request.body,
4533
+ "--base",
4534
+ request.base,
4535
+ "--head",
4536
+ request.head,
4537
+ "--assignee",
4538
+ "@me"
4312
4539
  ], { cwd })).stdout.trim().split("\n").at(-1);
4313
4540
  if (url === void 0) throw new Error("gh did not return a pull request URL");
4314
4541
  return { url };
4315
4542
  }
4316
- async function suggestCommitMessage(cwd) {
4543
+ async function commitMessage(cwd) {
4544
+ const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
4545
+ if (diff === "") throw new Error("There is nothing to commit");
4546
+ return generateText(cwd, "You write concise git commit messages.", `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}`);
4547
+ }
4548
+ async function generateText(cwd, instructions, prompt) {
4317
4549
  const settings = readConfig().commitMessage;
4318
4550
  const provider = settings.provider;
4319
4551
  const accountId = settings.accountId;
4320
4552
  const model = settings.model;
4321
- if (provider === null || model === null) throw new Error("Choose a commit message model in Settings → Git");
4553
+ if (provider === null || model === null) throw new Error("Choose a model in Settings → Git");
4322
4554
  assertEnabled(provider, accountId);
4323
- if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
4324
- const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
4325
- if (diff === "") throw new Error("There is nothing to commit");
4326
- 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}`;
4327
- if (provider === "codex") return codexCommitMessage(accountId, cwd, model, prompt);
4328
- if (provider === "opencode") return openCodeCommitMessage(cwd, model, prompt);
4555
+ if (provider === "codex") return codexText(accountId, cwd, model, instructions, prompt);
4556
+ if (provider === "opencode") return openCodeText(cwd, model, `${instructions}\n\n${prompt}`);
4329
4557
  const conversation = query({
4330
4558
  prompt,
4331
4559
  options: {
4332
4560
  ...claudeOptions(accountId, cwd),
4333
- systemPrompt: "You write concise git commit messages.",
4561
+ systemPrompt: instructions,
4334
4562
  tools: [],
4335
4563
  model,
4336
4564
  maxTurns: 1,
@@ -4341,15 +4569,14 @@ async function suggestCommitMessage(cwd) {
4341
4569
  if (message.type !== "result") continue;
4342
4570
  if (message.subtype !== "success") throw new Error(message.errors.join("\n"));
4343
4571
  if (message.is_error) throw new Error(message.result);
4344
- return { message: message.result.trim() };
4572
+ return message.result.trim();
4345
4573
  }
4346
4574
  throw new Error("Claude did not reply");
4347
4575
  }
4348
4576
  async function projectDiff(cwd) {
4349
- if (await isRepo(cwd)) return repoDiff(cwd, "");
4350
- const repos = childRepos(cwd, "", REPO_DEPTH);
4577
+ const repos = await projectRepos(cwd);
4351
4578
  if (repos.length === 0) throw new Error(`${cwd} is not a git repository and has none inside it`);
4352
- return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), `${repo}/`)))).join("");
4579
+ return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
4353
4580
  }
4354
4581
  function git(cwd, args) {
4355
4582
  return run$1("git", args, {
@@ -4357,6 +4584,34 @@ function git(cwd, args) {
4357
4584
  maxBuffer: 67108864
4358
4585
  });
4359
4586
  }
4587
+ async function projectRepos(cwd) {
4588
+ return await isRepo(cwd) ? [""] : childRepos(cwd, "", REPO_DEPTH);
4589
+ }
4590
+ async function gitStatus(cwd) {
4591
+ const output = await git(cwd, [
4592
+ "status",
4593
+ "--porcelain=v2",
4594
+ "--branch"
4595
+ ]);
4596
+ const status = {
4597
+ branch: null,
4598
+ upstream: null,
4599
+ ahead: 0,
4600
+ behind: 0,
4601
+ changed: 0
4602
+ };
4603
+ for (const line of output.stdout.split("\n").filter(Boolean)) {
4604
+ const [marker, key, ...rest] = line.split(" ");
4605
+ if (marker !== "#") status.changed++;
4606
+ else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
4607
+ else if (key === "branch.upstream") status.upstream = rest[0];
4608
+ else if (key === "branch.ab") {
4609
+ status.ahead = Number(rest[0]);
4610
+ status.behind = -Number(rest[1]);
4611
+ }
4612
+ }
4613
+ return status;
4614
+ }
4360
4615
  async function isRepo(cwd) {
4361
4616
  try {
4362
4617
  await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
@@ -4538,109 +4793,6 @@ function walk(root) {
4538
4793
  return files;
4539
4794
  }
4540
4795
  //#endregion
4541
- //#region src/services/push.ts
4542
- const TITLE_LIMIT = 100;
4543
- const BODY_LIMIT = 500;
4544
- const TIME_TO_LIVE = 900;
4545
- const SEND_TIMEOUT = 1e4;
4546
- const SUBJECT = "https://github.com/NazmusSayad/offsite";
4547
- let server$1 = "";
4548
- let notifier = null;
4549
- function startPush(address) {
4550
- server$1 = address;
4551
- }
4552
- function pushActive() {
4553
- return targets().length > 0;
4554
- }
4555
- function notifierClient() {
4556
- return notifier === null ? null : notifier.client;
4557
- }
4558
- function claimNotifier(client, endpoint) {
4559
- notifier = {
4560
- client,
4561
- endpoint
4562
- };
4563
- for (const item of clients) item.send({
4564
- type: "notifier",
4565
- active: item === client
4566
- });
4567
- }
4568
- function subscribe(registration, client) {
4569
- if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
4570
- const values = {
4571
- endpoint: registration.subscription.endpoint,
4572
- p256dh: registration.subscription.keys.p256dh,
4573
- auth: registration.subscription.keys.auth,
4574
- publicKey: registration.publicKey,
4575
- privateKey: registration.privateKey,
4576
- machine: registration.machine,
4577
- server: server$1
4578
- };
4579
- db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
4580
- target: pushSubscriptions.endpoint,
4581
- set: values
4582
- }).run();
4583
- if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
4584
- console.log("a browser subscribed to push notifications");
4585
- }
4586
- function unsubscribe(endpoint) {
4587
- db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
4588
- if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
4589
- console.log("a browser unsubscribed from push notifications");
4590
- }
4591
- async function sendPush(message) {
4592
- const subscriptions = targets();
4593
- if (subscriptions.length === 0) return;
4594
- const trimmed = {
4595
- ...message,
4596
- title: [...message.title].slice(0, TITLE_LIMIT).join(""),
4597
- body: [...message.body].slice(0, BODY_LIMIT).join("")
4598
- };
4599
- const gone = [];
4600
- await Promise.all(subscriptions.map(async (subscription) => {
4601
- try {
4602
- await webPush.sendNotification({
4603
- endpoint: subscription.endpoint,
4604
- keys: {
4605
- p256dh: subscription.p256dh,
4606
- auth: subscription.auth
4607
- }
4608
- }, JSON.stringify({
4609
- ...trimmed,
4610
- machineId: subscription.machine
4611
- }), {
4612
- vapidDetails: {
4613
- subject: SUBJECT,
4614
- publicKey: subscription.publicKey,
4615
- privateKey: subscription.privateKey
4616
- },
4617
- TTL: TIME_TO_LIVE,
4618
- urgency: "high",
4619
- timeout: SEND_TIMEOUT
4620
- });
4621
- } catch (error) {
4622
- const status = error.statusCode;
4623
- if (status !== void 0 && [
4624
- 401,
4625
- 403,
4626
- 404,
4627
- 410
4628
- ].includes(status)) {
4629
- console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
4630
- gone.push(subscription.endpoint);
4631
- return;
4632
- }
4633
- console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
4634
- }
4635
- }));
4636
- if (gone.length === 0) return;
4637
- db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
4638
- }
4639
- function targets() {
4640
- if (notifier === null || notifier.endpoint === null) return [];
4641
- return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
4642
- }
4643
- //#endregion
4644
4796
  //#region src/services/runs.ts
4645
4797
  const locks = /* @__PURE__ */ new Map();
4646
4798
  function stopAll() {
@@ -5156,6 +5308,23 @@ function executable(name) {
5156
5308
  return process.platform === "win32" ? `${name}.exe` : name;
5157
5309
  }
5158
5310
  //#endregion
5311
+ //#region ../core/src/terminal.ts
5312
+ function encodeFrame(kind, id, data) {
5313
+ const frame = new Uint8Array(5 + data.length);
5314
+ frame[0] = kind;
5315
+ new DataView(frame.buffer).setUint32(1, id);
5316
+ frame.set(data, 5);
5317
+ return frame;
5318
+ }
5319
+ function decodeFrame(frame) {
5320
+ if (frame.length < 5) throw new Error("terminal frame is too short");
5321
+ return {
5322
+ kind: frame[0],
5323
+ id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
5324
+ data: frame.subarray(5)
5325
+ };
5326
+ }
5327
+ //#endregion
5159
5328
  //#region src/services/terminal/alerts.ts
5160
5329
  const BELL_GAP = 5e3;
5161
5330
  const NOTIFICATION_GAP = 1e3;
@@ -5594,6 +5763,7 @@ function push(id, alert, windowTitle) {
5594
5763
  const tab = findTab(id);
5595
5764
  if (title === "") title = tab?.customName ?? (windowTitle || "Terminal");
5596
5765
  sendPush({
5766
+ kind: "terminal",
5597
5767
  title,
5598
5768
  body,
5599
5769
  projectId: entry[0],
@@ -5838,12 +6008,16 @@ const handlers = {
5838
6008
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
5839
6009
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
5840
6010
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
5841
- "git.status": (params) => gitStatus(projectPath(params.projectId)),
6011
+ "git.status": (params) => gitRepos(projectPath(params.projectId)),
5842
6012
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
5843
- "git.commit": (params) => commit(projectPath(params.projectId), params.message),
5844
- "git.push": (params) => push$1(projectPath(params.projectId)),
5845
- "git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
5846
- "git.message": (params) => suggestCommitMessage(projectPath(params.projectId)),
6013
+ "git.commit": (params) => commit(path.join(projectPath(params.projectId), params.repo), params.message, params.push),
6014
+ "git.push": async (params) => {
6015
+ await push$1(path.join(projectPath(params.projectId), params.repo));
6016
+ return null;
6017
+ },
6018
+ "git.branches": (params) => pullRequestBranches(path.join(projectPath(params.projectId), params.repo)),
6019
+ "git.pullRequest": (params) => preparePullRequest(path.join(projectPath(params.projectId), params.repo), params.base, params.commit),
6020
+ "git.createPullRequest": (params) => createPullRequest(path.join(projectPath(params.projectId), params.repo), params),
5847
6021
  "push.subscribe": (params, client) => {
5848
6022
  subscribe(params, client);
5849
6023
  return null;