offsite 0.0.13 → 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 +438 -252
  2. package/apps/server/dist/main.mjs.map +1 -1
  3. package/apps/web/dist/assets/{angular-html-CtL9u31h.js → angular-html-CG4HtNlx.js} +1 -1
  4. package/apps/web/dist/assets/{angular-ts-D4IFBcAh.js → angular-ts-CsUs7XSt.js} +1 -1
  5. package/apps/web/dist/assets/{apl-CqnJhScR.js → apl-BZNYboe1.js} +1 -1
  6. package/apps/web/dist/assets/{astro-Dn1_avL_.js → astro-BJJJ2TCY.js} +1 -1
  7. package/apps/web/dist/assets/{blade-BKSa6Bs3.js → blade-DPPHNv9A.js} +1 -1
  8. package/apps/web/dist/assets/{c-C8sbMt_8.js → c-DewL-caT.js} +1 -1
  9. package/apps/web/dist/assets/{chapel-BWxXvR2-.js → chapel-BC24MUCE.js} +1 -1
  10. package/apps/web/dist/assets/{cobol-DNoVp--S.js → cobol-CrP0JgFd.js} +1 -1
  11. package/apps/web/dist/assets/{coffee-m1P3e2U1.js → coffee-Do6yzljS.js} +1 -1
  12. package/apps/web/dist/assets/{cpp-CDvshXRJ.js → cpp-BkpYSJP8.js} +1 -1
  13. package/apps/web/dist/assets/{crystal-C4b65AQe.js → crystal-CeGzC-WW.js} +1 -1
  14. package/apps/web/dist/assets/{css-CqQV9eEE.js → css-Dw9LKU-N.js} +1 -1
  15. package/apps/web/dist/assets/{edge-CEdbZQ1U.js → edge-DOlsjMpU.js} +1 -1
  16. package/apps/web/dist/assets/{elixir-CYDX6OgA.js → elixir-Of98ZUP3.js} +1 -1
  17. package/apps/web/dist/assets/{elm-10RuU89D.js → elm-DcO8avoe.js} +1 -1
  18. package/apps/web/dist/assets/{erb-BiMfBUaM.js → erb-DGRCSrUQ.js} +1 -1
  19. package/apps/web/dist/assets/{git-rebase-D4Se-3SO.js → git-rebase-CF43k64m.js} +1 -1
  20. package/apps/web/dist/assets/{glimmer-js-sKOVTqrH.js → glimmer-js-DzTtVcUh.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-ts-DWMGXcHA.js → glimmer-ts-BGpaKkl4.js} +1 -1
  22. package/apps/web/dist/assets/{glsl-6ltM2JGZ.js → glsl-B6O-e93J.js} +1 -1
  23. package/apps/web/dist/assets/{graphql-DwyBbitO.js → graphql-8XdLbcFd.js} +1 -1
  24. package/apps/web/dist/assets/{hack-DKDbxv34.js → hack-DF9S8RPk.js} +1 -1
  25. package/apps/web/dist/assets/{haml-Bsfivw5f.js → haml-D7K9Vy7l.js} +1 -1
  26. package/apps/web/dist/assets/{handlebars-B3HW0h8N.js → handlebars-CodAKXOQ.js} +1 -1
  27. package/apps/web/dist/assets/{html-D9EhGmAq.js → html-DINw2T6o.js} +1 -1
  28. package/apps/web/dist/assets/{html-derivative-Cs1eIi3k.js → html-derivative-Bm8ARfNX.js} +1 -1
  29. package/apps/web/dist/assets/{http-BG4vxnc2.js → http-CzdVlK4g.js} +1 -1
  30. package/apps/web/dist/assets/{hurl-OIF0UEfX.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-2iLSH1xc.js → java-CJrEr5nb.js} +1 -1
  34. package/apps/web/dist/assets/{javascript-Di7CdqTh.js → javascript-BRzSAlK0.js} +1 -1
  35. package/apps/web/dist/assets/{jinja-2mjIAN_z.js → jinja-BCkNTi07.js} +1 -1
  36. package/apps/web/dist/assets/{jison-Ciw1rqtP.js → jison-CuVfSCOU.js} +1 -1
  37. package/apps/web/dist/assets/{json-D7Dp9EFU.js → json-CivFBkRR.js} +1 -1
  38. package/apps/web/dist/assets/{jsx-WEelr3xJ.js → jsx-Bn8oTRCQ.js} +1 -1
  39. package/apps/web/dist/assets/{julia-DL3m48KZ.js → julia-S7B4dPpO.js} +1 -1
  40. package/apps/web/dist/assets/{just-B-5HaJEz.js → just-BkwYofB3.js} +1 -1
  41. package/apps/web/dist/assets/{latex-DKmfapvy.js → latex-CHtrQEQW.js} +1 -1
  42. package/apps/web/dist/assets/{liquid-Bqxp18sH.js → liquid-BiWMrFWE.js} +1 -1
  43. package/apps/web/dist/assets/{lua-BSLGBVbN.js → lua-DH2dy4b5.js} +1 -1
  44. package/apps/web/dist/assets/{marko-yLzCBGNR.js → marko-D1J2FQwH.js} +1 -1
  45. package/apps/web/dist/assets/{mdc-DSGSIXIk.js → mdc-DdU0BPrI.js} +1 -1
  46. package/apps/web/dist/assets/{nginx-BF1hRuVH.js → nginx-aVr0h4x0.js} +1 -1
  47. package/apps/web/dist/assets/{nim-kAH67IJv.js → nim-D3uKiFs9.js} +1 -1
  48. package/apps/web/dist/assets/{org-DCuW6Ito.js → org-Dlc-mmBJ.js} +1 -1
  49. package/apps/web/dist/assets/{perl-ChYkJruC.js → perl-Db2FD7eY.js} +1 -1
  50. package/apps/web/dist/assets/{php-Dy0o5CHL.js → php-BKnY42CF.js} +1 -1
  51. package/apps/web/dist/assets/{pug-C-vFGcyR.js → pug-Dv1XMHZo.js} +1 -1
  52. package/apps/web/dist/assets/{qml-Bbhsm3bz.js → qml-DLrUK6Mn.js} +1 -1
  53. package/apps/web/dist/assets/{r-D6AOSU_n.js → r-YY5yALTA.js} +1 -1
  54. package/apps/web/dist/assets/{razor-CsDTm1QE.js → razor-CHwb0eZk.js} +1 -1
  55. package/apps/web/dist/assets/{regexp-Cxw4Sy2X.js → regexp-BbKQ-CAd.js} +1 -1
  56. package/apps/web/dist/assets/{rst-BdEa7eTr.js → rst-CfgFwgqW.js} +1 -1
  57. package/apps/web/dist/assets/{ruby-CrX6uYJq.js → ruby-CWYxEIgK.js} +1 -1
  58. package/apps/web/dist/assets/{sas-Dq1DiCgf.js → sas-BIhkpR95.js} +1 -1
  59. package/apps/web/dist/assets/{scss-Cq-_Yezx.js → scss-CD48eC-9.js} +1 -1
  60. package/apps/web/dist/assets/{shellscript-N1TdW_Vo.js → shellscript-DQJbmpgk.js} +1 -1
  61. package/apps/web/dist/assets/{shellsession-DGy-5gMa.js → shellsession-BsbY07m-.js} +1 -1
  62. package/apps/web/dist/assets/{soy-BZmv_HHk.js → soy-2Y50DZFi.js} +1 -1
  63. package/apps/web/dist/assets/{sql-B0TfXN6P.js → sql-nKnfvr4s.js} +1 -1
  64. package/apps/web/dist/assets/{stata-D0cL92SE.js → stata-Dqj31JrM.js} +1 -1
  65. package/apps/web/dist/assets/{surrealql-B9RWrsWf.js → surrealql-DjKPQgK9.js} +1 -1
  66. package/apps/web/dist/assets/{svelte-Erfh9F_0.js → svelte-1HUOD4gz.js} +1 -1
  67. package/apps/web/dist/assets/{templ-C1hdk1C3.js → templ-CjuUG0Pz.js} +1 -1
  68. package/apps/web/dist/assets/{tex-B6y5Z0Oe.js → tex-DImgjGWJ.js} +1 -1
  69. package/apps/web/dist/assets/{ts-tags-B8Q3bBJx.js → ts-tags-uu3b2KWy.js} +1 -1
  70. package/apps/web/dist/assets/{tsx-CA2zlxVX.js → tsx-DLBT5mtF.js} +1 -1
  71. package/apps/web/dist/assets/{twig-xr1-p8qn.js → twig-CUqAREOD.js} +1 -1
  72. package/apps/web/dist/assets/{typescript-Bdd7ljtG.js → typescript-BJx9F5MO.js} +1 -1
  73. package/apps/web/dist/assets/{typst-BGU6c2s1.js → typst-DGjRmWJl.js} +1 -1
  74. package/apps/web/dist/assets/{vue-CxnbGcMK.js → vue-D4sa_D6w.js} +1 -1
  75. package/apps/web/dist/assets/{vue-html-D3pkBH4b.js → vue-html-D9bXwnxX.js} +1 -1
  76. package/apps/web/dist/assets/{vue-vine-0adNns8e.js → vue-vine-CltCK1Bg.js} +1 -1
  77. package/apps/web/dist/assets/{xml-Bf5yulh9.js → xml-BeUIBscE.js} +1 -1
  78. package/apps/web/dist/assets/{xsl-DnGCH2C6.js → xsl-Dp0u2Kgu.js} +1 -1
  79. package/apps/web/dist/assets/{yaml-ALFPjwWA.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-CEx2Hw3M.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: [
@@ -56,8 +39,13 @@ const deprecatedModels = {
56
39
  opencode: []
57
40
  };
58
41
  const defaultModels = {
42
+ codex: "gpt-6.1-sol",
59
43
  claude: "claude-opus-5-5",
60
- codex: null,
44
+ opencode: null
45
+ };
46
+ const defaultEfforts = {
47
+ codex: "high",
48
+ claude: "high",
61
49
  opencode: null
62
50
  };
63
51
  //#endregion
@@ -255,7 +243,8 @@ const privateEntries = [
255
243
  ];
256
244
  const windowNames = /* @__PURE__ */ new Map([[300, "five_hour"], [10080, "seven_day"]]);
257
245
  async function codexAccount(account) {
258
- const linked = isMirror(account.configDir);
246
+ const configDir = account.configDir ?? codexBaseDir;
247
+ const linked = isMirror(configDir);
259
248
  const base = {
260
249
  ...account,
261
250
  email: null,
@@ -264,11 +253,11 @@ async function codexAccount(account) {
264
253
  linked
265
254
  };
266
255
  try {
267
- const server = appServer(account.configDir);
256
+ const server = appServer(configDir);
268
257
  const result = await server.request("account/read", {});
269
258
  if (result.account === null) return {
270
259
  ...base,
271
- 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`,
272
261
  valid: false
273
262
  };
274
263
  switch (result.account.type) {
@@ -302,7 +291,7 @@ async function codexAccount(account) {
302
291
  }
303
292
  }
304
293
  async function codexCapabilities(account, cwd) {
305
- const server = appServer(account.configDir);
294
+ const server = appServer(account.configDir ?? codexBaseDir);
306
295
  const models = [];
307
296
  let cursor = null;
308
297
  do {
@@ -333,7 +322,7 @@ async function skills(server, cwd) {
333
322
  return (await server.request("skills/list", { cwds: [cwd] })).data.flatMap((entry) => entry.skills.filter((skill) => skill.enabled));
334
323
  }
335
324
  function publishCodexLimits(home, snapshot) {
336
- 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({
337
326
  type: "limits",
338
327
  accountId: account.id,
339
328
  limits: toLimits(snapshot)
@@ -698,7 +687,7 @@ function record$1(value) {
698
687
  //#region src/args.ts
699
688
  let command = "web";
700
689
  let name = null;
701
- const program = new Command("offsite").description("Starts the server and the web app").option("--host <host>", "Address to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", "47841").option("--dir <dir>", "Folder for settings and accounts", path.join(os.homedir(), ".offsite")).option("--no-auth", "Accept connections without 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 });
702
691
  program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
703
692
  program.command("serve").description("Starts only the server, without the web app").action(() => {
704
693
  command = "serve";
@@ -712,6 +701,7 @@ program.command("unpair").description("Picks paired browsers to unpair").action(
712
701
  });
713
702
  program.parse();
714
703
  const options = program.opts();
704
+ if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
715
705
  const args = {
716
706
  ...options,
717
707
  dir: path.resolve(options.dir),
@@ -776,8 +766,13 @@ function readJson(name) {
776
766
  }
777
767
  }
778
768
  function writeJson(name, value) {
779
- const file = path.join(home, name);
769
+ let file = path.join(home, name);
780
770
  mkdirSync(home, { recursive: true });
771
+ try {
772
+ file = realpathSync(file);
773
+ } catch (error) {
774
+ if (!hasCode(error, "ENOENT")) throw error;
775
+ }
781
776
  writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
782
777
  renameSync(`${file}.tmp`, file);
783
778
  }
@@ -1053,7 +1048,7 @@ const presets = /* @__PURE__ */ new Map([
1053
1048
  }]
1054
1049
  ]);
1055
1050
  function codexServer(settings) {
1056
- return appServer(storedAccount("codex", settings.accountId).configDir);
1051
+ return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
1057
1052
  }
1058
1053
  var CodexRun = class CodexRun {
1059
1054
  provider = "codex";
@@ -1481,15 +1476,15 @@ async function revertThread(sessionId, messageId) {
1481
1476
  beforeTurnId: turnOf(messageId)
1482
1477
  });
1483
1478
  }
1484
- async function codexCommitMessage(accountId, cwd, model, prompt) {
1485
- 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);
1486
1481
  const threadId = (await server.request("thread/start", {
1487
1482
  cwd,
1488
1483
  model,
1489
1484
  ephemeral: true,
1490
1485
  approvalPolicy: "never",
1491
1486
  sandbox: "read-only",
1492
- developerInstructions: "You write concise git commit messages."
1487
+ developerInstructions: instructions
1493
1488
  })).thread.id;
1494
1489
  const reply = Promise.withResolvers();
1495
1490
  let text = "";
@@ -1517,7 +1512,7 @@ async function codexCommitMessage(accountId, cwd, model, prompt) {
1517
1512
  }],
1518
1513
  model
1519
1514
  });
1520
- return { message: await reply.promise };
1515
+ return await reply.promise;
1521
1516
  } finally {
1522
1517
  server.threads.delete(threadId);
1523
1518
  await server.request("thread/unsubscribe", { threadId });
@@ -1802,6 +1797,26 @@ function record(value) {
1802
1797
  return isRecord(value) ? value : {};
1803
1798
  }
1804
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
1805
1820
  //#region src/lib/opencode/sessions.ts
1806
1821
  const PAGE = 100;
1807
1822
  const COMMIT_LIMIT = 12e4;
@@ -1917,7 +1932,7 @@ async function rewindOpenCode(request) {
1917
1932
  else await client.session.revert.commit({ sessionID });
1918
1933
  return result;
1919
1934
  }
1920
- async function openCodeCommitMessage(cwd, model, prompt) {
1935
+ async function openCodeText(cwd, model, prompt) {
1921
1936
  const client = await openCode();
1922
1937
  const session = await client.session.create({
1923
1938
  location: { directory: cwd },
@@ -1936,7 +1951,7 @@ async function openCodeCommitMessage(cwd, model, prompt) {
1936
1951
  if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
1937
1952
  if (message.type !== "assistant") continue;
1938
1953
  const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
1939
- if (text !== "") return { message: text };
1954
+ if (text !== "") return text;
1940
1955
  }
1941
1956
  throw new Error("OpenCode did not reply");
1942
1957
  } finally {
@@ -1984,6 +1999,15 @@ function openCodeRun(sessionId) {
1984
1999
  const run = runs.get(sessionId);
1985
2000
  return run?.provider === "opencode" ? run : void 0;
1986
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
+ }
1987
2011
  //#endregion
1988
2012
  //#region src/lib/opencode/run.ts
1989
2013
  const IDLE_LIMIT$1 = 18e5;
@@ -2260,22 +2284,7 @@ var OpenCodeRun = class OpenCodeRun {
2260
2284
  const item = source === void 0 ? void 0 : this.items.get(`${source.messageID}:${source.id}`);
2261
2285
  this.addPending({
2262
2286
  kind: "permission",
2263
- request: {
2264
- id: permission.id,
2265
- kind: "approval",
2266
- title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
2267
- canAlwaysAllow: (permission.save ?? []).length > 0,
2268
- item: item ?? {
2269
- id: permission.id,
2270
- parent: null,
2271
- time: Date.now(),
2272
- kind: "tool",
2273
- name: permission.action,
2274
- input: { resources: permission.resources },
2275
- output: "",
2276
- status: "running"
2277
- }
2278
- }
2287
+ request: permissionRequest(permission, item)
2279
2288
  });
2280
2289
  }
2281
2290
  askForm(form) {
@@ -2357,15 +2366,6 @@ var OpenCodeRun = class OpenCodeRun {
2357
2366
  });
2358
2367
  }
2359
2368
  };
2360
- function sessionSettings(session, settings) {
2361
- const variant = session.model?.variant;
2362
- return {
2363
- ...settings,
2364
- model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
2365
- effort: variant === void 0 || variant === "default" ? null : variant,
2366
- mode: session.agent ?? null
2367
- };
2368
- }
2369
2369
  function files(prompt) {
2370
2370
  return prompt.images.map((image) => ({ uri: `data:${image.mediaType};base64,${image.data}` }));
2371
2371
  }
@@ -2780,7 +2780,7 @@ const listChanges = /* @__PURE__ */ new Set([
2780
2780
  "session.execution.failed",
2781
2781
  "session.execution.interrupted"
2782
2782
  ]);
2783
- const SILENCE_LIMIT$1 = 45e3;
2783
+ const SILENCE_LIMIT$2 = 45e3;
2784
2784
  const RECONNECT_ATTEMPTS = 5;
2785
2785
  const RECONNECT_DELAY = 2e3;
2786
2786
  let connection;
@@ -2823,7 +2823,7 @@ async function connect() {
2823
2823
  async function listen(client) {
2824
2824
  const controller = new AbortController();
2825
2825
  listener = controller;
2826
- const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
2826
+ const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$2);
2827
2827
  try {
2828
2828
  for await (const event of client.event.subscribe({
2829
2829
  signal: controller.signal,
@@ -2832,7 +2832,7 @@ async function listen(client) {
2832
2832
  console.warn("the OpenCode event stream ended");
2833
2833
  } catch (error) {
2834
2834
  if (listener !== controller) return;
2835
- 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}`);
2836
2836
  } finally {
2837
2837
  clearTimeout(silence);
2838
2838
  }
@@ -2897,14 +2897,13 @@ function providerName(provider) {
2897
2897
  }
2898
2898
  function storedAccounts() {
2899
2899
  const config = readConfig();
2900
- return [...config.claudeAccounts, ...config.codexAccounts].flatMap((item) => {
2901
- const configDir = item.configDir[platform()];
2902
- if (configDir === null) return [];
2903
- return [{
2904
- ...item,
2905
- configDir
2906
- }];
2907
- });
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
+ }))];
2908
2907
  }
2909
2908
  function assertEnabled(provider, accountId) {
2910
2909
  if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
@@ -2925,7 +2924,7 @@ async function stopAccount(account) {
2925
2924
  const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
2926
2925
  for (const run of stopping) run.stop();
2927
2926
  await Promise.all(stopping.map((run) => run.closed));
2928
- if (account.provider === "codex") stopAppServer(account.configDir);
2927
+ if (account.provider === "codex") stopAppServer(account.configDir ?? codexBaseDir);
2929
2928
  if (account.provider === "claude") forgetClaude(account.id);
2930
2929
  }
2931
2930
  function storedAccount(provider, accountId) {
@@ -2984,11 +2983,13 @@ async function capabilities(provider, accountId, cwd) {
2984
2983
  hidden: deprecated,
2985
2984
  effort: null
2986
2985
  };
2986
+ const preferred = defaultEfforts[provider];
2987
+ const effort = preferred === null ? null : model.efforts.includes(preferred) ? preferred : model.efforts.at(-1) ?? null;
2987
2988
  return {
2988
2989
  ...model,
2989
2990
  deprecated,
2990
2991
  hidden: overrides[model.value] ?? setting.hidden,
2991
- effort: setting.effort
2992
+ effort: setting.effort ?? effort
2992
2993
  };
2993
2994
  })
2994
2995
  };
@@ -3051,7 +3052,15 @@ function removeAccount(id) {
3051
3052
  }
3052
3053
  function validAccount(name, configDir) {
3053
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
+ };
3054
3059
  const dir = configDir.trim();
3060
+ if (dir === "") return {
3061
+ name: name.trim(),
3062
+ configDir: null
3063
+ };
3055
3064
  if (dir === "~" || dir.startsWith("~/")) return {
3056
3065
  name: name.trim(),
3057
3066
  configDir: path.join(os.homedir(), dir.slice(1))
@@ -3063,6 +3072,117 @@ function validAccount(name, configDir) {
3063
3072
  throw new Error("Use an absolute config directory or one starting with ~/");
3064
3073
  }
3065
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
3066
3186
  //#region src/lib/claude/claude.ts
3067
3187
  const efforts = [
3068
3188
  "low",
@@ -3104,13 +3224,13 @@ function claudeMode(value) {
3104
3224
  return mode;
3105
3225
  }
3106
3226
  function claudeOptions(accountId, cwd) {
3107
- const account = storedAccount("claude", accountId);
3227
+ const configDir = storedAccount("claude", accountId).configDir ?? claudeBaseDir();
3108
3228
  const env = {
3109
3229
  ...process.env,
3110
3230
  CLAUDE_CODE_ENTRYPOINT: "offsite",
3111
- CLAUDE_CONFIG_DIR: account.configDir
3231
+ CLAUDE_CONFIG_DIR: configDir
3112
3232
  };
3113
- 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;
3114
3234
  return {
3115
3235
  cwd,
3116
3236
  env,
@@ -3123,23 +3243,56 @@ function claudeOptions(accountId, cwd) {
3123
3243
  matcher: "PushNotification",
3124
3244
  hooks: [async (input) => {
3125
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;
3126
3247
  broadcast({
3127
3248
  type: "notification",
3128
3249
  sessionId: input.session_id,
3129
- message: input.tool_input.message
3250
+ message
3130
3251
  });
3131
- return { hookSpecificOutput: {
3252
+ if (notifierConnected()) return { hookSpecificOutput: {
3132
3253
  hookEventName: "PostToolUse",
3133
3254
  updatedToolOutput: {
3134
- message: input.tool_input.message,
3255
+ message,
3135
3256
  localSent: true,
3136
3257
  disabledReason: "no_transport"
3137
3258
  }
3138
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
+ } };
3139
3278
  }]
3140
3279
  }] }
3141
3280
  };
3142
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
+ }
3143
3296
  async function claudeCapabilities(account, cwd) {
3144
3297
  const result = await initialization(account.id, cwd);
3145
3298
  return {
@@ -3161,7 +3314,7 @@ async function claudeCapabilities(account, cwd) {
3161
3314
  };
3162
3315
  }
3163
3316
  async function claudeAccount(account) {
3164
- const linked = isLinked(account.configDir);
3317
+ const linked = isLinked(account.configDir ?? claudeBaseDir());
3165
3318
  try {
3166
3319
  const info = (await initialization(account.id, os.homedir())).account;
3167
3320
  return {
@@ -3882,19 +4035,10 @@ var Run = class {
3882
4035
  this.sessionId = options.sessionId;
3883
4036
  this.resumeAt = options.resumeAt;
3884
4037
  this.settings = options.settings;
3885
- const settings = options.settings;
3886
4038
  this.query = query({
3887
4039
  prompt: this.input(),
3888
4040
  options: {
3889
- ...claudeOptions(settings.accountId, options.cwd),
3890
- ...options.resume ? { resume: options.sessionId } : { sessionId: options.sessionId },
3891
- ...options.resumeAt === null ? {} : { resumeSessionAt: options.resumeAt },
3892
- enableFileCheckpointing: true,
3893
- ...settings.mode === null ? {} : { permissionMode: claudeMode(settings.mode) },
3894
- ...settings.model === null ? {} : { model: settings.model },
3895
- ...settings.effort === null ? {} : { effort: claudeEffort(settings.effort) },
3896
- allowDangerouslySkipPermissions: true,
3897
- includePartialMessages: true,
4041
+ ...runOptions(options),
3898
4042
  canUseTool: (toolName, input, context) => this.ask({
3899
4043
  toolName,
3900
4044
  input,
@@ -4240,85 +4384,181 @@ const UNTRACKED_LIMIT = 100;
4240
4384
  const NEW_FILE_LIMIT = 262144;
4241
4385
  const REPO_DEPTH = 3;
4242
4386
  const run$1 = promisify(execFile);
4243
- async function gitStatus(cwd) {
4244
- let output;
4245
- try {
4246
- output = await git(cwd, [
4247
- "status",
4248
- "--porcelain=v2",
4249
- "--branch"
4250
- ]);
4251
- } catch {
4252
- return null;
4253
- }
4254
- const status = {
4255
- branch: null,
4256
- upstream: null,
4257
- ahead: 0,
4258
- behind: 0,
4259
- changed: 0
4260
- };
4261
- for (const line of output.stdout.split("\n").filter(Boolean)) {
4262
- const [marker, key, ...rest] = line.split(" ");
4263
- if (marker !== "#") status.changed++;
4264
- else if (key === "branch.head") status.branch = rest[0] === "(detached)" ? null : rest[0];
4265
- else if (key === "branch.upstream") status.upstream = rest[0];
4266
- else if (key === "branch.ab") {
4267
- status.ahead = Number(rest[0]);
4268
- status.behind = -Number(rest[1]);
4269
- }
4270
- }
4271
- return status;
4272
- }
4273
- async function commit(cwd, message) {
4274
- 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;
4275
4423
  await git(cwd, ["add", "--all"]);
4276
4424
  await git(cwd, [
4277
4425
  "commit",
4278
4426
  "--message",
4279
- message
4427
+ text
4280
4428
  ]);
4281
- 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 };
4282
4435
  }
4283
4436
  async function push$1(cwd) {
4284
4437
  const status = await gitStatus(cwd);
4285
- if (status === null) throw new Error(`${cwd} is not a git repository`);
4286
- if (status.upstream === null) await git(cwd, [
4287
- "push",
4288
- "--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",
4289
4499
  "origin",
4290
- "HEAD"
4500
+ base
4291
4501
  ]);
4292
- else await git(cwd, ["push"]);
4293
- 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
+ };
4294
4524
  }
4295
- async function createPullRequest(cwd) {
4525
+ async function createPullRequest(cwd, request) {
4296
4526
  const url = (await run$1("gh", [
4297
4527
  "pr",
4298
4528
  "create",
4299
- "--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"
4300
4539
  ], { cwd })).stdout.trim().split("\n").at(-1);
4301
4540
  if (url === void 0) throw new Error("gh did not return a pull request URL");
4302
4541
  return { url };
4303
4542
  }
4304
- 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) {
4305
4549
  const settings = readConfig().commitMessage;
4306
4550
  const provider = settings.provider;
4307
4551
  const accountId = settings.accountId;
4308
4552
  const model = settings.model;
4309
- 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");
4310
4554
  assertEnabled(provider, accountId);
4311
- if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
4312
- const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
4313
- if (diff === "") throw new Error("There is nothing to commit");
4314
- 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}`;
4315
- if (provider === "codex") return codexCommitMessage(accountId, cwd, model, prompt);
4316
- 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}`);
4317
4557
  const conversation = query({
4318
4558
  prompt,
4319
4559
  options: {
4320
4560
  ...claudeOptions(accountId, cwd),
4321
- systemPrompt: "You write concise git commit messages.",
4561
+ systemPrompt: instructions,
4322
4562
  tools: [],
4323
4563
  model,
4324
4564
  maxTurns: 1,
@@ -4329,15 +4569,14 @@ async function suggestCommitMessage(cwd) {
4329
4569
  if (message.type !== "result") continue;
4330
4570
  if (message.subtype !== "success") throw new Error(message.errors.join("\n"));
4331
4571
  if (message.is_error) throw new Error(message.result);
4332
- return { message: message.result.trim() };
4572
+ return message.result.trim();
4333
4573
  }
4334
4574
  throw new Error("Claude did not reply");
4335
4575
  }
4336
4576
  async function projectDiff(cwd) {
4337
- if (await isRepo(cwd)) return repoDiff(cwd, "");
4338
- const repos = childRepos(cwd, "", REPO_DEPTH);
4577
+ const repos = await projectRepos(cwd);
4339
4578
  if (repos.length === 0) throw new Error(`${cwd} is not a git repository and has none inside it`);
4340
- 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("");
4341
4580
  }
4342
4581
  function git(cwd, args) {
4343
4582
  return run$1("git", args, {
@@ -4345,6 +4584,34 @@ function git(cwd, args) {
4345
4584
  maxBuffer: 67108864
4346
4585
  });
4347
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
+ }
4348
4615
  async function isRepo(cwd) {
4349
4616
  try {
4350
4617
  await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
@@ -4526,109 +4793,6 @@ function walk(root) {
4526
4793
  return files;
4527
4794
  }
4528
4795
  //#endregion
4529
- //#region src/services/push.ts
4530
- const TITLE_LIMIT = 100;
4531
- const BODY_LIMIT = 500;
4532
- const TIME_TO_LIVE = 900;
4533
- const SEND_TIMEOUT = 1e4;
4534
- const SUBJECT = "https://github.com/NazmusSayad/offsite";
4535
- let server$1 = "";
4536
- let notifier = null;
4537
- function startPush(address) {
4538
- server$1 = address;
4539
- }
4540
- function pushActive() {
4541
- return targets().length > 0;
4542
- }
4543
- function notifierClient() {
4544
- return notifier === null ? null : notifier.client;
4545
- }
4546
- function claimNotifier(client, endpoint) {
4547
- notifier = {
4548
- client,
4549
- endpoint
4550
- };
4551
- for (const item of clients) item.send({
4552
- type: "notifier",
4553
- active: item === client
4554
- });
4555
- }
4556
- function subscribe(registration, client) {
4557
- if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
4558
- const values = {
4559
- endpoint: registration.subscription.endpoint,
4560
- p256dh: registration.subscription.keys.p256dh,
4561
- auth: registration.subscription.keys.auth,
4562
- publicKey: registration.publicKey,
4563
- privateKey: registration.privateKey,
4564
- machine: registration.machine,
4565
- server: server$1
4566
- };
4567
- db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
4568
- target: pushSubscriptions.endpoint,
4569
- set: values
4570
- }).run();
4571
- if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
4572
- console.log("a browser subscribed to push notifications");
4573
- }
4574
- function unsubscribe(endpoint) {
4575
- db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
4576
- if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
4577
- console.log("a browser unsubscribed from push notifications");
4578
- }
4579
- async function sendPush(message) {
4580
- const subscriptions = targets();
4581
- if (subscriptions.length === 0) return;
4582
- const trimmed = {
4583
- ...message,
4584
- title: [...message.title].slice(0, TITLE_LIMIT).join(""),
4585
- body: [...message.body].slice(0, BODY_LIMIT).join("")
4586
- };
4587
- const gone = [];
4588
- await Promise.all(subscriptions.map(async (subscription) => {
4589
- try {
4590
- await webPush.sendNotification({
4591
- endpoint: subscription.endpoint,
4592
- keys: {
4593
- p256dh: subscription.p256dh,
4594
- auth: subscription.auth
4595
- }
4596
- }, JSON.stringify({
4597
- ...trimmed,
4598
- machineId: subscription.machine
4599
- }), {
4600
- vapidDetails: {
4601
- subject: SUBJECT,
4602
- publicKey: subscription.publicKey,
4603
- privateKey: subscription.privateKey
4604
- },
4605
- TTL: TIME_TO_LIVE,
4606
- urgency: "high",
4607
- timeout: SEND_TIMEOUT
4608
- });
4609
- } catch (error) {
4610
- const status = error.statusCode;
4611
- if (status !== void 0 && [
4612
- 401,
4613
- 403,
4614
- 404,
4615
- 410
4616
- ].includes(status)) {
4617
- console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
4618
- gone.push(subscription.endpoint);
4619
- return;
4620
- }
4621
- console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
4622
- }
4623
- }));
4624
- if (gone.length === 0) return;
4625
- db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
4626
- }
4627
- function targets() {
4628
- if (notifier === null || notifier.endpoint === null) return [];
4629
- return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
4630
- }
4631
- //#endregion
4632
4796
  //#region src/services/runs.ts
4633
4797
  const locks = /* @__PURE__ */ new Map();
4634
4798
  function stopAll() {
@@ -5144,6 +5308,23 @@ function executable(name) {
5144
5308
  return process.platform === "win32" ? `${name}.exe` : name;
5145
5309
  }
5146
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
5147
5328
  //#region src/services/terminal/alerts.ts
5148
5329
  const BELL_GAP = 5e3;
5149
5330
  const NOTIFICATION_GAP = 1e3;
@@ -5582,6 +5763,7 @@ function push(id, alert, windowTitle) {
5582
5763
  const tab = findTab(id);
5583
5764
  if (title === "") title = tab?.customName ?? (windowTitle || "Terminal");
5584
5765
  sendPush({
5766
+ kind: "terminal",
5585
5767
  title,
5586
5768
  body,
5587
5769
  projectId: entry[0],
@@ -5826,12 +6008,16 @@ const handlers = {
5826
6008
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
5827
6009
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
5828
6010
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
5829
- "git.status": (params) => gitStatus(projectPath(params.projectId)),
6011
+ "git.status": (params) => gitRepos(projectPath(params.projectId)),
5830
6012
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
5831
- "git.commit": (params) => commit(projectPath(params.projectId), params.message),
5832
- "git.push": (params) => push$1(projectPath(params.projectId)),
5833
- "git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
5834
- "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),
5835
6021
  "push.subscribe": (params, client) => {
5836
6022
  subscribe(params, client);
5837
6023
  return null;