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