offsite 0.0.14 → 0.0.16

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 +455 -256
  2. package/apps/server/dist/main.mjs.map +1 -1
  3. package/apps/web/dist/assets/{angular-html-6NaagU1P.js → angular-html-DS1oV12F.js} +1 -1
  4. package/apps/web/dist/assets/{angular-ts-D8XutLPw.js → angular-ts-DGEt84_g.js} +1 -1
  5. package/apps/web/dist/assets/{apl-BKJEkz6Z.js → apl-bz_iDiNf.js} +1 -1
  6. package/apps/web/dist/assets/{astro-DkrGLOry.js → astro-jXSlLJ7l.js} +1 -1
  7. package/apps/web/dist/assets/{blade-BIc0yPkX.js → blade-2sxYYh5A.js} +1 -1
  8. package/apps/web/dist/assets/{c-Ds8U4gsq.js → c-DapwzVu-.js} +1 -1
  9. package/apps/web/dist/assets/{chapel-COXTL8xW.js → chapel-DUoqr6M4.js} +1 -1
  10. package/apps/web/dist/assets/{cobol-D0VSGQqK.js → cobol-Cn0D9nVy.js} +1 -1
  11. package/apps/web/dist/assets/{coffee-DBrey7sA.js → coffee-C1tCcS8h.js} +1 -1
  12. package/apps/web/dist/assets/{cpp-SKUDYlNR.js → cpp-DxWN2Okg.js} +1 -1
  13. package/apps/web/dist/assets/{crystal-CoTn_b8r.js → crystal-DF-BMake.js} +1 -1
  14. package/apps/web/dist/assets/{css-BErHy7fN.js → css-DO_8FP-m.js} +1 -1
  15. package/apps/web/dist/assets/{edge-CACFpVrz.js → edge-D8r2HbS9.js} +1 -1
  16. package/apps/web/dist/assets/{elixir-aPcGiByD.js → elixir-K6buEKBc.js} +1 -1
  17. package/apps/web/dist/assets/{elm-DN5FfPkL.js → elm-B78wD7Un.js} +1 -1
  18. package/apps/web/dist/assets/{erb-Dj675b5b.js → erb-DuhNLWPu.js} +1 -1
  19. package/apps/web/dist/assets/{git-rebase-DepNW1h5.js → git-rebase-C6n0imm6.js} +1 -1
  20. package/apps/web/dist/assets/{glimmer-js-fCJatm6u.js → glimmer-js-B65Nng8o.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-ts-YwpKoyPh.js → glimmer-ts-Cme47tem.js} +1 -1
  22. package/apps/web/dist/assets/{glsl-PGMqKJzL.js → glsl-DCAs0c4j.js} +1 -1
  23. package/apps/web/dist/assets/{graphql-3IK2PC2Y.js → graphql-CjoRmsAT.js} +1 -1
  24. package/apps/web/dist/assets/{hack-C8CJl4zQ.js → hack-W5aeWZGp.js} +1 -1
  25. package/apps/web/dist/assets/{haml-CiYnV6_a.js → haml-C_s1GJd7.js} +1 -1
  26. package/apps/web/dist/assets/{handlebars-C4m4mY7m.js → handlebars-CYz9msjm.js} +1 -1
  27. package/apps/web/dist/assets/{html-derivative-CwhughXj.js → html-derivative-fPnJ7c-5.js} +1 -1
  28. package/apps/web/dist/assets/{html-D1Ar4Xkj.js → html-rJzVMoNs.js} +1 -1
  29. package/apps/web/dist/assets/{http-pzvpiwNw.js → http-DIR9LW29.js} +1 -1
  30. package/apps/web/dist/assets/{hurl-hkZwo5Ou.js → hurl-73h4sQYH.js} +1 -1
  31. package/apps/web/dist/assets/index-CbDKWecy.js +1206 -0
  32. package/apps/web/dist/assets/index-aoUjY0qi.css +2 -0
  33. package/apps/web/dist/assets/{java-CjDcLNWV.js → java-CC9QxJEZ.js} +1 -1
  34. package/apps/web/dist/assets/{javascript-B1pleYoe.js → javascript-BhEdsWyv.js} +1 -1
  35. package/apps/web/dist/assets/{jinja-erX4wFzj.js → jinja-wBZAPGc9.js} +1 -1
  36. package/apps/web/dist/assets/{jison-CajpWJAS.js → jison-bZuttet2.js} +1 -1
  37. package/apps/web/dist/assets/{json-CwL6i1-C.js → json-BwYVcFZr.js} +1 -1
  38. package/apps/web/dist/assets/{jsx-DmOLIUT0.js → jsx-DZ0EGwxr.js} +1 -1
  39. package/apps/web/dist/assets/{julia-DK5bpj3S.js → julia-Bj20nWQB.js} +1 -1
  40. package/apps/web/dist/assets/{just-BtwHGit4.js → just-pKKGVoyp.js} +1 -1
  41. package/apps/web/dist/assets/{latex-Dnf1mLFf.js → latex-DN5Fi2zg.js} +1 -1
  42. package/apps/web/dist/assets/{liquid-Bv72E2Is.js → liquid-DtPrOPBs.js} +1 -1
  43. package/apps/web/dist/assets/{lua-94ufuXur.js → lua-C1ueigSQ.js} +1 -1
  44. package/apps/web/dist/assets/{marko-BKL0CGzx.js → marko-BAsjT454.js} +1 -1
  45. package/apps/web/dist/assets/{mdc-DNhqDQoU.js → mdc-DwMa0Ya_.js} +1 -1
  46. package/apps/web/dist/assets/{nginx-BcYwGlC2.js → nginx-BlYeGp57.js} +1 -1
  47. package/apps/web/dist/assets/{nim-CRMgcK-K.js → nim-DASYTjR5.js} +1 -1
  48. package/apps/web/dist/assets/{org-DfnsnSld.js → org-DSeAzk6D.js} +1 -1
  49. package/apps/web/dist/assets/{perl-BEwyXv2a.js → perl-BGPYGeVd.js} +1 -1
  50. package/apps/web/dist/assets/{php-CG9qDJ82.js → php-kgT6t38x.js} +1 -1
  51. package/apps/web/dist/assets/{pug-Dg3kkpP7.js → pug-COTHnC13.js} +1 -1
  52. package/apps/web/dist/assets/{qml-Vs1TgYWl.js → qml-DhqsM4c7.js} +1 -1
  53. package/apps/web/dist/assets/{r-CFy73Kus.js → r-D-KnVREb.js} +1 -1
  54. package/apps/web/dist/assets/{razor-Dcng6YrP.js → razor-BGH58TzE.js} +1 -1
  55. package/apps/web/dist/assets/{regexp-Bwbv2WPD.js → regexp-BHxEHonG.js} +1 -1
  56. package/apps/web/dist/assets/{rst-Cul2jxkE.js → rst-UweowKJr.js} +1 -1
  57. package/apps/web/dist/assets/{ruby-TMvLq4yj.js → ruby-DtfMo-6m.js} +1 -1
  58. package/apps/web/dist/assets/{sas-D8HC__R6.js → sas-DkQGY_o2.js} +1 -1
  59. package/apps/web/dist/assets/{scss-dK6Y8mdp.js → scss-QYV6e_ks.js} +1 -1
  60. package/apps/web/dist/assets/{shellscript-BrObD5cd.js → shellscript-CBTXJvxj.js} +1 -1
  61. package/apps/web/dist/assets/{shellsession-gwVDz4P8.js → shellsession-CT0a4IuI.js} +1 -1
  62. package/apps/web/dist/assets/{soy-BF1mzKmn.js → soy-B3ufTOzS.js} +1 -1
  63. package/apps/web/dist/assets/{sql-rY-oq1Hg.js → sql-BhGvXJN_.js} +1 -1
  64. package/apps/web/dist/assets/{stata-DBh-WI4L.js → stata-BYhfUPEu.js} +1 -1
  65. package/apps/web/dist/assets/{surrealql-DxG1DYQM.js → surrealql-UIZa84-V.js} +1 -1
  66. package/apps/web/dist/assets/{svelte-IUUmpoTZ.js → svelte-SkphxuD6.js} +1 -1
  67. package/apps/web/dist/assets/{templ-DSD9ChjE.js → templ-C8T3Zgrx.js} +1 -1
  68. package/apps/web/dist/assets/{tex-ClrklzW1.js → tex-Be-e7W9g.js} +1 -1
  69. package/apps/web/dist/assets/{ts-tags-CiWqjPoE.js → ts-tags-Bk8_Hqg_.js} +1 -1
  70. package/apps/web/dist/assets/{tsx-BVdH_4RP.js → tsx-B_hC-_lk.js} +1 -1
  71. package/apps/web/dist/assets/{twig-bBlRyqC3.js → twig-pKLKVHRj.js} +1 -1
  72. package/apps/web/dist/assets/{typescript-BWvsrxJs.js → typescript-D8MumUKS.js} +1 -1
  73. package/apps/web/dist/assets/{typst-CoUU-sfH.js → typst-DCcUGqaw.js} +1 -1
  74. package/apps/web/dist/assets/{vue-0_DBul4P.js → vue-B_0IsDtY.js} +1 -1
  75. package/apps/web/dist/assets/{vue-html-feH37mKg.js → vue-html-B42dVyQs.js} +1 -1
  76. package/apps/web/dist/assets/{vue-vine-xWSVrEzZ.js → vue-vine-CUms6qs_.js} +1 -1
  77. package/apps/web/dist/assets/{xml-RE-6RA5e.js → xml-Dh5mAVtG.js} +1 -1
  78. package/apps/web/dist/assets/{xsl-DzQQ-zeR.js → xsl-73EQy__K.js} +1 -1
  79. package/apps/web/dist/assets/{yaml-BfSzomC9.js → yaml-C5lWNsLB.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
  }
@@ -2733,13 +2723,19 @@ async function updateSession(id, patch) {
2733
2723
  case "opencode": await renameOpenCode(id, patch.title);
2734
2724
  }
2735
2725
  if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
2736
- else if (patch.status !== void 0) db.insert(sessions$1).values({
2737
- sessionId: id,
2738
- status: patch.status
2739
- }).onConflictDoUpdate({
2740
- target: sessions$1.sessionId,
2741
- set: { status: patch.status }
2742
- }).run();
2726
+ else if (patch.status !== void 0) {
2727
+ const set = patch.status === "active" ? {
2728
+ status: patch.status,
2729
+ interactedAt: Date.now()
2730
+ } : { status: patch.status };
2731
+ db.insert(sessions$1).values({
2732
+ sessionId: id,
2733
+ ...set
2734
+ }).onConflictDoUpdate({
2735
+ target: sessions$1.sessionId,
2736
+ set
2737
+ }).run();
2738
+ }
2743
2739
  if (patch.title !== void 0) refreshSessions(providerOf(id));
2744
2740
  }
2745
2741
  async function removeSession(id) {
@@ -2790,7 +2786,7 @@ const listChanges = /* @__PURE__ */ new Set([
2790
2786
  "session.execution.failed",
2791
2787
  "session.execution.interrupted"
2792
2788
  ]);
2793
- const SILENCE_LIMIT$1 = 45e3;
2789
+ const SILENCE_LIMIT$2 = 45e3;
2794
2790
  const RECONNECT_ATTEMPTS = 5;
2795
2791
  const RECONNECT_DELAY = 2e3;
2796
2792
  let connection;
@@ -2833,7 +2829,7 @@ async function connect() {
2833
2829
  async function listen(client) {
2834
2830
  const controller = new AbortController();
2835
2831
  listener = controller;
2836
- const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
2832
+ const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$2);
2837
2833
  try {
2838
2834
  for await (const event of client.event.subscribe({
2839
2835
  signal: controller.signal,
@@ -2842,7 +2838,7 @@ async function listen(client) {
2842
2838
  console.warn("the OpenCode event stream ended");
2843
2839
  } catch (error) {
2844
2840
  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}`);
2841
+ console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
2846
2842
  } finally {
2847
2843
  clearTimeout(silence);
2848
2844
  }
@@ -2907,14 +2903,13 @@ function providerName(provider) {
2907
2903
  }
2908
2904
  function storedAccounts() {
2909
2905
  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
- });
2906
+ return [...config.claudeAccounts.map((item) => ({
2907
+ ...item,
2908
+ configDir: item.configDir[platform()]
2909
+ })), ...config.codexAccounts.map((item) => ({
2910
+ ...item,
2911
+ configDir: item.configDir[platform()]
2912
+ }))];
2918
2913
  }
2919
2914
  function assertEnabled(provider, accountId) {
2920
2915
  if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
@@ -2935,7 +2930,7 @@ async function stopAccount(account) {
2935
2930
  const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
2936
2931
  for (const run of stopping) run.stop();
2937
2932
  await Promise.all(stopping.map((run) => run.closed));
2938
- if (account.provider === "codex") stopAppServer(account.configDir);
2933
+ if (account.provider === "codex") stopAppServer(account.configDir ?? codexBaseDir);
2939
2934
  if (account.provider === "claude") forgetClaude(account.id);
2940
2935
  }
2941
2936
  function storedAccount(provider, accountId) {
@@ -3063,7 +3058,15 @@ function removeAccount(id) {
3063
3058
  }
3064
3059
  function validAccount(name, configDir) {
3065
3060
  if (name.trim() === "") throw new Error("Give the account a name");
3061
+ if (configDir === null) return {
3062
+ name: name.trim(),
3063
+ configDir: null
3064
+ };
3066
3065
  const dir = configDir.trim();
3066
+ if (dir === "") return {
3067
+ name: name.trim(),
3068
+ configDir: null
3069
+ };
3067
3070
  if (dir === "~" || dir.startsWith("~/")) return {
3068
3071
  name: name.trim(),
3069
3072
  configDir: path.join(os.homedir(), dir.slice(1))
@@ -3075,6 +3078,117 @@ function validAccount(name, configDir) {
3075
3078
  throw new Error("Use an absolute config directory or one starting with ~/");
3076
3079
  }
3077
3080
  //#endregion
3081
+ //#region src/services/push.ts
3082
+ const TITLE_LIMIT = 100;
3083
+ const BODY_LIMIT = 500;
3084
+ const TIME_TO_LIVE = 900;
3085
+ const SEND_TIMEOUT = 1e4;
3086
+ const SILENCE_LIMIT$1 = 9e4;
3087
+ const SUBJECT = "https://github.com/NazmusSayad/offsite";
3088
+ let server$1 = "";
3089
+ let notifier = null;
3090
+ function startPush(address) {
3091
+ server$1 = address;
3092
+ }
3093
+ function pushActive() {
3094
+ return targets().length > 0;
3095
+ }
3096
+ function notifierClient() {
3097
+ return notifier === null ? null : notifier.client;
3098
+ }
3099
+ function notifierConnected() {
3100
+ if (notifier === null || !clients.has(notifier.client)) return false;
3101
+ return Date.now() - notifier.client.heardAt < SILENCE_LIMIT$1;
3102
+ }
3103
+ function claimNotifier(client, endpoint) {
3104
+ notifier = {
3105
+ client,
3106
+ endpoint
3107
+ };
3108
+ for (const item of clients) item.send({
3109
+ type: "notifier",
3110
+ active: item === client
3111
+ });
3112
+ }
3113
+ function subscribe(registration, client) {
3114
+ if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
3115
+ const values = {
3116
+ endpoint: registration.subscription.endpoint,
3117
+ p256dh: registration.subscription.keys.p256dh,
3118
+ auth: registration.subscription.keys.auth,
3119
+ publicKey: registration.publicKey,
3120
+ privateKey: registration.privateKey,
3121
+ machine: registration.machine,
3122
+ server: server$1
3123
+ };
3124
+ db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
3125
+ target: pushSubscriptions.endpoint,
3126
+ set: values
3127
+ }).run();
3128
+ if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
3129
+ console.log("a browser subscribed to push notifications");
3130
+ }
3131
+ function unsubscribe(endpoint) {
3132
+ db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
3133
+ if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
3134
+ console.log("a browser unsubscribed from push notifications");
3135
+ }
3136
+ async function sendPush(message) {
3137
+ const subscriptions = targets();
3138
+ if (subscriptions.length === 0) return false;
3139
+ const trimmed = {
3140
+ ...message,
3141
+ title: [...message.title].slice(0, TITLE_LIMIT).join(""),
3142
+ body: [...message.body].slice(0, BODY_LIMIT).join("")
3143
+ };
3144
+ const gone = [];
3145
+ let sent = false;
3146
+ await Promise.all(subscriptions.map(async (subscription) => {
3147
+ try {
3148
+ await webPush.sendNotification({
3149
+ endpoint: subscription.endpoint,
3150
+ keys: {
3151
+ p256dh: subscription.p256dh,
3152
+ auth: subscription.auth
3153
+ }
3154
+ }, JSON.stringify({
3155
+ ...trimmed,
3156
+ machineId: subscription.machine
3157
+ }), {
3158
+ vapidDetails: {
3159
+ subject: SUBJECT,
3160
+ publicKey: subscription.publicKey,
3161
+ privateKey: subscription.privateKey
3162
+ },
3163
+ TTL: TIME_TO_LIVE,
3164
+ urgency: "high",
3165
+ timeout: SEND_TIMEOUT
3166
+ });
3167
+ sent = true;
3168
+ } catch (error) {
3169
+ const status = error.statusCode;
3170
+ if (status !== void 0 && [
3171
+ 401,
3172
+ 403,
3173
+ 404,
3174
+ 410
3175
+ ].includes(status)) {
3176
+ console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
3177
+ gone.push(subscription.endpoint);
3178
+ return;
3179
+ }
3180
+ console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
3181
+ }
3182
+ }));
3183
+ if (gone.length === 0) return sent;
3184
+ db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
3185
+ return sent;
3186
+ }
3187
+ function targets() {
3188
+ if (notifier === null || notifier.endpoint === null) return [];
3189
+ return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
3190
+ }
3191
+ //#endregion
3078
3192
  //#region src/lib/claude/claude.ts
3079
3193
  const efforts = [
3080
3194
  "low",
@@ -3116,13 +3230,13 @@ function claudeMode(value) {
3116
3230
  return mode;
3117
3231
  }
3118
3232
  function claudeOptions(accountId, cwd) {
3119
- const account = storedAccount("claude", accountId);
3233
+ const configDir = storedAccount("claude", accountId).configDir ?? claudeBaseDir();
3120
3234
  const env = {
3121
3235
  ...process.env,
3122
3236
  CLAUDE_CODE_ENTRYPOINT: "offsite",
3123
- CLAUDE_CONFIG_DIR: account.configDir
3237
+ CLAUDE_CONFIG_DIR: configDir
3124
3238
  };
3125
- if (path.resolve(account.configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
3239
+ if (path.resolve(configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
3126
3240
  return {
3127
3241
  cwd,
3128
3242
  env,
@@ -3135,23 +3249,56 @@ function claudeOptions(accountId, cwd) {
3135
3249
  matcher: "PushNotification",
3136
3250
  hooks: [async (input) => {
3137
3251
  if (input.hook_event_name !== "PostToolUse" || !isRecord(input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
3252
+ const message = input.tool_input.message;
3138
3253
  broadcast({
3139
3254
  type: "notification",
3140
3255
  sessionId: input.session_id,
3141
- message: input.tool_input.message
3256
+ message
3142
3257
  });
3143
- return { hookSpecificOutput: {
3258
+ if (notifierConnected()) return { hookSpecificOutput: {
3144
3259
  hookEventName: "PostToolUse",
3145
3260
  updatedToolOutput: {
3146
- message: input.tool_input.message,
3261
+ message,
3147
3262
  localSent: true,
3148
3263
  disabledReason: "no_transport"
3149
3264
  }
3150
3265
  } };
3266
+ const session = sessions().find((item) => item.id === input.session_id);
3267
+ if (session === void 0) throw new Error(`Session ${input.session_id} is not listed`);
3268
+ const pushSent = await sendPush({
3269
+ kind: "session",
3270
+ title: session.title,
3271
+ body: message,
3272
+ projectId: session.projectId,
3273
+ sessionId: session.id
3274
+ });
3275
+ return { hookSpecificOutput: {
3276
+ hookEventName: "PostToolUse",
3277
+ updatedToolOutput: {
3278
+ message,
3279
+ localSent: false,
3280
+ pushSent,
3281
+ ...pushSent ? {} : { disabledReason: "no_transport" }
3282
+ }
3283
+ } };
3151
3284
  }]
3152
3285
  }] }
3153
3286
  };
3154
3287
  }
3288
+ function runOptions(options) {
3289
+ const settings = options.settings;
3290
+ return {
3291
+ ...claudeOptions(settings.accountId, options.cwd),
3292
+ ...options.resume ? { resume: options.sessionId } : { sessionId: options.sessionId },
3293
+ ...options.resumeAt === null ? {} : { resumeSessionAt: options.resumeAt },
3294
+ enableFileCheckpointing: true,
3295
+ ...settings.mode === null ? {} : { permissionMode: claudeMode(settings.mode) },
3296
+ ...settings.model === null ? {} : { model: settings.model },
3297
+ ...settings.effort === null ? {} : { effort: claudeEffort(settings.effort) },
3298
+ allowDangerouslySkipPermissions: true,
3299
+ includePartialMessages: true
3300
+ };
3301
+ }
3155
3302
  async function claudeCapabilities(account, cwd) {
3156
3303
  const result = await initialization(account.id, cwd);
3157
3304
  return {
@@ -3173,7 +3320,7 @@ async function claudeCapabilities(account, cwd) {
3173
3320
  };
3174
3321
  }
3175
3322
  async function claudeAccount(account) {
3176
- const linked = isLinked(account.configDir);
3323
+ const linked = isLinked(account.configDir ?? claudeBaseDir());
3177
3324
  try {
3178
3325
  const info = (await initialization(account.id, os.homedir())).account;
3179
3326
  return {
@@ -3894,19 +4041,10 @@ var Run = class {
3894
4041
  this.sessionId = options.sessionId;
3895
4042
  this.resumeAt = options.resumeAt;
3896
4043
  this.settings = options.settings;
3897
- const settings = options.settings;
3898
4044
  this.query = query({
3899
4045
  prompt: this.input(),
3900
4046
  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,
4047
+ ...runOptions(options),
3910
4048
  canUseTool: (toolName, input, context) => this.ask({
3911
4049
  toolName,
3912
4050
  input,
@@ -4252,85 +4390,195 @@ const UNTRACKED_LIMIT = 100;
4252
4390
  const NEW_FILE_LIMIT = 262144;
4253
4391
  const REPO_DEPTH = 3;
4254
4392
  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");
4393
+ const PULL_REQUEST_INSTRUCTIONS = `You write GitHub pull requests as Markdown.
4394
+
4395
+ ## TITLE RULES
4396
+
4397
+ - Use the conventional commits format: type(scope): subject
4398
+ - Types: feat, fix, docs, style, refactor, perf, test, chore, ci
4399
+ - Scope is optional; include it when the change targets a clear area
4400
+ - Subject: imperative mood, lowercase, no trailing period
4401
+ - Maximum 72 characters in total
4402
+ - Describe the overall change, not a single commit
4403
+
4404
+ ## BODY RULES
4405
+
4406
+ - 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.
4407
+ - Use bullet points under every section, never paragraphs. Each bullet describes one feature or change in one short sentence.
4408
+ - 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.
4409
+ - Aim for 20-50 words; fewer is better when enough. Never exceed 80 words.
4410
+ - Describe the outcome, not the implementation walkthrough. Do not enumerate changed files, functions, commits, or incidental cleanup; reviewers can read the diff.
4411
+ - Add a \`Breaking changes\` section only for an actual compatibility break or required migration.
4412
+ - No generic \`Summary\` section, bold labels, checklists, boilerplate openings like "This pull request", or closing summaries.
4413
+
4414
+ ## RULES
4415
+
4416
+ Base every statement on the provided commits and diff; do NOT invent details.
4417
+
4418
+ ## OUTPUT
4419
+
4420
+ Return one Markdown document. The title is the level-1 heading (\`# <title>\`), and everything after it is the body.`;
4421
+ async function gitRepos(cwd) {
4422
+ return Promise.all((await projectRepos(cwd)).map(async (repo) => ({
4423
+ ...await gitStatus(path.join(cwd, repo)),
4424
+ path: repo
4425
+ })));
4426
+ }
4427
+ async function commit(cwd, message, andPush) {
4428
+ const text = message.trim() === "" ? await commitMessage(cwd) : message;
4287
4429
  await git(cwd, ["add", "--all"]);
4288
4430
  await git(cwd, [
4289
4431
  "commit",
4290
4432
  "--message",
4291
- message
4433
+ text
4292
4434
  ]);
4293
- return gitStatus(cwd);
4435
+ if (andPush) try {
4436
+ await push$1(cwd);
4437
+ } catch (error) {
4438
+ throw new Error(`Committed, but push failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
4439
+ }
4440
+ return { message: text };
4294
4441
  }
4295
4442
  async function push$1(cwd) {
4296
4443
  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",
4444
+ if (status.branch === null) throw new Error("Cannot push a detached HEAD");
4445
+ try {
4446
+ if (status.upstream === null) await git(cwd, [
4447
+ "push",
4448
+ "--set-upstream",
4449
+ "origin",
4450
+ "HEAD"
4451
+ ]);
4452
+ else await git(cwd, ["push"]);
4453
+ } catch (error) {
4454
+ 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 });
4455
+ }
4456
+ }
4457
+ async function localBranches(cwd) {
4458
+ return (await git(cwd, [
4459
+ "branch",
4460
+ "--sort=-committerdate",
4461
+ "--format=%(refname:short)"
4462
+ ])).stdout.split("\n").filter(Boolean);
4463
+ }
4464
+ async function switchBranch(cwd, branch, create) {
4465
+ await git(cwd, create ? [
4466
+ "switch",
4467
+ "--create",
4468
+ branch
4469
+ ] : ["switch", branch]);
4470
+ }
4471
+ async function pullRequestBranches(cwd) {
4472
+ const head = (await gitStatus(cwd)).branch;
4473
+ if (head === null) throw new Error("Not on a branch");
4474
+ const base = (await run$1("gh", [
4475
+ "repo",
4476
+ "view",
4477
+ "--json",
4478
+ "defaultBranchRef",
4479
+ "--jq",
4480
+ ".defaultBranchRef.name"
4481
+ ], { cwd })).stdout.trim();
4482
+ if (base === head) throw new Error(`${head} is the default branch. Create a branch first.`);
4483
+ return {
4484
+ head,
4485
+ base,
4486
+ branches: (await git(cwd, [
4487
+ "ls-remote",
4488
+ "--heads",
4489
+ "origin"
4490
+ ])).stdout.split("\n").filter(Boolean).map((line) => line.split("refs/heads/")[1]).filter((branch) => branch !== head)
4491
+ };
4492
+ }
4493
+ async function preparePullRequest(cwd, base, commitChanges) {
4494
+ const head = (await gitStatus(cwd)).branch;
4495
+ if (head === null) throw new Error("Not on a branch");
4496
+ if (commitChanges) await commit(cwd, "", true);
4497
+ const status = await gitStatus(cwd);
4498
+ if (status.upstream === null || status.ahead > 0) await push$1(cwd);
4499
+ const open = (await run$1("gh", [
4500
+ "pr",
4501
+ "list",
4502
+ "--head",
4503
+ head,
4504
+ "--base",
4505
+ base,
4506
+ "--state",
4507
+ "open",
4508
+ "--json",
4509
+ "url",
4510
+ "--jq",
4511
+ ".[0].url // \"\""
4512
+ ], { cwd })).stdout.trim();
4513
+ if (open !== "") return {
4514
+ kind: "open",
4515
+ url: open
4516
+ };
4517
+ await git(cwd, [
4518
+ "fetch",
4301
4519
  "origin",
4302
- "HEAD"
4520
+ base
4303
4521
  ]);
4304
- else await git(cwd, ["push"]);
4305
- return gitStatus(cwd);
4522
+ const commits = (await git(cwd, [
4523
+ "log",
4524
+ "--format=- %B",
4525
+ `origin/${base}..HEAD`
4526
+ ])).stdout.trim();
4527
+ if (commits === "") throw new Error(`No commits between ${base} and ${head}`);
4528
+ const diff = (await git(cwd, [
4529
+ "diff",
4530
+ "--no-color",
4531
+ "--no-ext-diff",
4532
+ `origin/${base}...HEAD`
4533
+ ])).stdout.slice(0, DIFF_LIMIT);
4534
+ 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");
4535
+ const title = lines[0].trim().replace(/^#{1,6}\s*/, "").replace(/^(\*\*|__|\*|_|`)+/, "").replace(/(\*\*|__|\*|_|`)+$/, "").trim();
4536
+ if (title === "") throw new Error("The model returned an empty title");
4537
+ return {
4538
+ kind: "draft",
4539
+ base,
4540
+ head,
4541
+ title,
4542
+ body: lines.slice(1).join("\n").trim()
4543
+ };
4306
4544
  }
4307
- async function createPullRequest(cwd) {
4545
+ async function createPullRequest(cwd, request) {
4308
4546
  const url = (await run$1("gh", [
4309
4547
  "pr",
4310
4548
  "create",
4311
- "--fill"
4549
+ "--title",
4550
+ request.title,
4551
+ "--body",
4552
+ request.body,
4553
+ "--base",
4554
+ request.base,
4555
+ "--head",
4556
+ request.head,
4557
+ "--assignee",
4558
+ "@me"
4312
4559
  ], { cwd })).stdout.trim().split("\n").at(-1);
4313
4560
  if (url === void 0) throw new Error("gh did not return a pull request URL");
4314
4561
  return { url };
4315
4562
  }
4316
- async function suggestCommitMessage(cwd) {
4563
+ async function commitMessage(cwd) {
4564
+ const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
4565
+ if (diff === "") throw new Error("There is nothing to commit");
4566
+ 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}`);
4567
+ }
4568
+ async function generateText(cwd, instructions, prompt) {
4317
4569
  const settings = readConfig().commitMessage;
4318
4570
  const provider = settings.provider;
4319
4571
  const accountId = settings.accountId;
4320
4572
  const model = settings.model;
4321
- if (provider === null || model === null) throw new Error("Choose a commit message model in Settings → Git");
4573
+ if (provider === null || model === null) throw new Error("Choose a model in Settings → Git");
4322
4574
  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);
4575
+ if (provider === "codex") return codexText(accountId, cwd, model, instructions, prompt);
4576
+ if (provider === "opencode") return openCodeText(cwd, model, `${instructions}\n\n${prompt}`);
4329
4577
  const conversation = query({
4330
4578
  prompt,
4331
4579
  options: {
4332
4580
  ...claudeOptions(accountId, cwd),
4333
- systemPrompt: "You write concise git commit messages.",
4581
+ systemPrompt: instructions,
4334
4582
  tools: [],
4335
4583
  model,
4336
4584
  maxTurns: 1,
@@ -4341,15 +4589,14 @@ async function suggestCommitMessage(cwd) {
4341
4589
  if (message.type !== "result") continue;
4342
4590
  if (message.subtype !== "success") throw new Error(message.errors.join("\n"));
4343
4591
  if (message.is_error) throw new Error(message.result);
4344
- return { message: message.result.trim() };
4592
+ return message.result.trim();
4345
4593
  }
4346
4594
  throw new Error("Claude did not reply");
4347
4595
  }
4348
4596
  async function projectDiff(cwd) {
4349
- if (await isRepo(cwd)) return repoDiff(cwd, "");
4350
- const repos = childRepos(cwd, "", REPO_DEPTH);
4597
+ const repos = await projectRepos(cwd);
4351
4598
  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("");
4599
+ return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), repo === "" ? "" : `${repo}/`)))).join("");
4353
4600
  }
4354
4601
  function git(cwd, args) {
4355
4602
  return run$1("git", args, {
@@ -4357,6 +4604,34 @@ function git(cwd, args) {
4357
4604
  maxBuffer: 67108864
4358
4605
  });
4359
4606
  }
4607
+ async function projectRepos(cwd) {
4608
+ return await isRepo(cwd) ? [""] : childRepos(cwd, "", REPO_DEPTH);
4609
+ }
4610
+ async function gitStatus(cwd) {
4611
+ const output = await git(cwd, [
4612
+ "status",
4613
+ "--porcelain=v2",
4614
+ "--branch"
4615
+ ]);
4616
+ const status = {
4617
+ branch: null,
4618
+ upstream: null,
4619
+ ahead: 0,
4620
+ behind: 0,
4621
+ changed: 0
4622
+ };
4623
+ for (const line of output.stdout.split("\n").filter(Boolean)) {
4624
+ const [marker, key, ...rest] = line.split(" ");
4625
+ if (marker !== "#") status.changed++;
4626
+ else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
4627
+ else if (key === "branch.upstream") status.upstream = rest[0];
4628
+ else if (key === "branch.ab") {
4629
+ status.ahead = Number(rest[0]);
4630
+ status.behind = -Number(rest[1]);
4631
+ }
4632
+ }
4633
+ return status;
4634
+ }
4360
4635
  async function isRepo(cwd) {
4361
4636
  try {
4362
4637
  await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
@@ -4538,109 +4813,6 @@ function walk(root) {
4538
4813
  return files;
4539
4814
  }
4540
4815
  //#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
4816
  //#region src/services/runs.ts
4645
4817
  const locks = /* @__PURE__ */ new Map();
4646
4818
  function stopAll() {
@@ -5156,6 +5328,23 @@ function executable(name) {
5156
5328
  return process.platform === "win32" ? `${name}.exe` : name;
5157
5329
  }
5158
5330
  //#endregion
5331
+ //#region ../core/src/terminal.ts
5332
+ function encodeFrame(kind, id, data) {
5333
+ const frame = new Uint8Array(5 + data.length);
5334
+ frame[0] = kind;
5335
+ new DataView(frame.buffer).setUint32(1, id);
5336
+ frame.set(data, 5);
5337
+ return frame;
5338
+ }
5339
+ function decodeFrame(frame) {
5340
+ if (frame.length < 5) throw new Error("terminal frame is too short");
5341
+ return {
5342
+ kind: frame[0],
5343
+ id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
5344
+ data: frame.subarray(5)
5345
+ };
5346
+ }
5347
+ //#endregion
5159
5348
  //#region src/services/terminal/alerts.ts
5160
5349
  const BELL_GAP = 5e3;
5161
5350
  const NOTIFICATION_GAP = 1e3;
@@ -5594,6 +5783,7 @@ function push(id, alert, windowTitle) {
5594
5783
  const tab = findTab(id);
5595
5784
  if (title === "") title = tab?.customName ?? (windowTitle || "Terminal");
5596
5785
  sendPush({
5786
+ kind: "terminal",
5597
5787
  title,
5598
5788
  body,
5599
5789
  projectId: entry[0],
@@ -5838,12 +6028,21 @@ const handlers = {
5838
6028
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
5839
6029
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
5840
6030
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
5841
- "git.status": (params) => gitStatus(projectPath(params.projectId)),
6031
+ "git.status": (params) => gitRepos(projectPath(params.projectId)),
5842
6032
  "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)),
6033
+ "git.commit": (params) => commit(path.join(projectPath(params.projectId), params.repo), params.message, params.push),
6034
+ "git.push": async (params) => {
6035
+ await push$1(path.join(projectPath(params.projectId), params.repo));
6036
+ return null;
6037
+ },
6038
+ "git.branches": (params) => pullRequestBranches(path.join(projectPath(params.projectId), params.repo)),
6039
+ "git.localBranches": (params) => localBranches(path.join(projectPath(params.projectId), params.repo)),
6040
+ "git.switch": async (params) => {
6041
+ await switchBranch(path.join(projectPath(params.projectId), params.repo), params.branch, params.create);
6042
+ return null;
6043
+ },
6044
+ "git.pullRequest": (params) => preparePullRequest(path.join(projectPath(params.projectId), params.repo), params.base, params.commit),
6045
+ "git.createPullRequest": (params) => createPullRequest(path.join(projectPath(params.projectId), params.repo), params),
5847
6046
  "push.subscribe": (params, client) => {
5848
6047
  subscribe(params, client);
5849
6048
  return null;