offsite 0.0.7 → 0.0.11
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/README.md +7 -7
- package/apps/server/dist/main.mjs +1855 -1644
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261017041447_init/migration.sql +32 -0
- package/apps/server/drizzle/20261017041447_init/snapshot.json +265 -0
- package/apps/server/drizzle/20261017103559_browser_push_keys/migration.sql +11 -0
- package/apps/server/drizzle/20261017103559_browser_push_keys/snapshot.json +265 -0
- package/apps/server/drizzle/20261017110336_browser_machine_ids/migration.sql +3 -0
- package/apps/server/drizzle/20261017110336_browser_machine_ids/snapshot.json +252 -0
- package/apps/web/dist/assets/{angular-html-CxKqLP98.js → angular-html-DTNmgvth.js} +1 -1
- package/apps/web/dist/assets/{angular-ts-ChaIimso.js → angular-ts-DOOLa99A.js} +1 -1
- package/apps/web/dist/assets/{apl-C79wKZ96.js → apl-CSWmNqJI.js} +1 -1
- package/apps/web/dist/assets/{astro-Bjd0-rUa.js → astro-CK7We5bo.js} +1 -1
- package/apps/web/dist/assets/{blade-CwnKAXvb.js → blade-B9Q94PMc.js} +1 -1
- package/apps/web/dist/assets/{c-Z516m-0_.js → c-D05zRIsd.js} +1 -1
- package/apps/web/dist/assets/{chapel-CjZ9_INP.js → chapel-CJbjuVPR.js} +1 -1
- package/apps/web/dist/assets/{cobol-loY0YFmk.js → cobol-CE0VhU2R.js} +1 -1
- package/apps/web/dist/assets/{coffee-UH3lFVdK.js → coffee-C0i-RL39.js} +1 -1
- package/apps/web/dist/assets/{cpp-DglnXsh6.js → cpp-CBJhfD_V.js} +1 -1
- package/apps/web/dist/assets/{crystal-8bvlXru4.js → crystal-DRdgVTW1.js} +1 -1
- package/apps/web/dist/assets/{css-BR1q7k6_.js → css-DgMip4qW.js} +1 -1
- package/apps/web/dist/assets/{edge-BxokJ4MN.js → edge-BCmeW_3H.js} +1 -1
- package/apps/web/dist/assets/{elixir-BUaqVZQr.js → elixir-BiRSxKnc.js} +1 -1
- package/apps/web/dist/assets/{elm-D_9smeEe.js → elm-zBiqCp9W.js} +1 -1
- package/apps/web/dist/assets/{erb-DOZIqXQH.js → erb-1QviMyT9.js} +1 -1
- package/apps/web/dist/assets/{git-rebase-DN-oOLvo.js → git-rebase-TjFhp_Gn.js} +1 -1
- package/apps/web/dist/assets/{glimmer-js-dSW-hX4N.js → glimmer-js-dThwC_hZ.js} +1 -1
- package/apps/web/dist/assets/{glimmer-ts-BgJbFWVH.js → glimmer-ts-cCThNW17.js} +1 -1
- package/apps/web/dist/assets/{glsl-FkhRNxnG.js → glsl-BxmyIQYG.js} +1 -1
- package/apps/web/dist/assets/{graphql-BKHVf0BT.js → graphql-DygyczJj.js} +1 -1
- package/apps/web/dist/assets/{hack-obS7Vybj.js → hack-Bl1_oIjY.js} +1 -1
- package/apps/web/dist/assets/{haml-8M07SKZy.js → haml-CeOBocB0.js} +1 -1
- package/apps/web/dist/assets/{handlebars-Cyb9PeHi.js → handlebars-Bw1KMTCv.js} +1 -1
- package/apps/web/dist/assets/{html-Dwl7QR96.js → html-BaNo1IFo.js} +1 -1
- package/apps/web/dist/assets/{html-derivative-Dx3lESlT.js → html-derivative-f4LhlsLO.js} +1 -1
- package/apps/web/dist/assets/{http-CKSR21d-.js → http-Be2FOF-U.js} +1 -1
- package/apps/web/dist/assets/{hurl-DGaDIWma.js → hurl-BrtPQQhS.js} +1 -1
- package/apps/web/dist/assets/index-6ofGnhX4.css +2 -0
- package/apps/web/dist/assets/index-NWUHLKte.js +1203 -0
- package/apps/web/dist/assets/{java-BmaAOMbe.js → java-Bum5bcia.js} +1 -1
- package/apps/web/dist/assets/{javascript-Dh-l17xw.js → javascript-C5oYhZKk.js} +1 -1
- package/apps/web/dist/assets/{jinja-C14B9X7r.js → jinja-CNXoe8pn.js} +1 -1
- package/apps/web/dist/assets/{jison-DOLQZnVk.js → jison-g1hzxo4W.js} +1 -1
- package/apps/web/dist/assets/{json-CKQXRUCT.js → json-CiAAuc5j.js} +1 -1
- package/apps/web/dist/assets/{jsx-D3LXQmFh.js → jsx-Z0awY4ip.js} +1 -1
- package/apps/web/dist/assets/{julia-BONGpLSW.js → julia-DX3JjwcM.js} +1 -1
- package/apps/web/dist/assets/{just-1ImCRd19.js → just-DS6eCX0g.js} +1 -1
- package/apps/web/dist/assets/{latex-C6nf8qng.js → latex-CaBic_pJ.js} +1 -1
- package/apps/web/dist/assets/{liquid-OyHcMLT8.js → liquid-DMaOBf3v.js} +1 -1
- package/apps/web/dist/assets/{lua-CFyXSn73.js → lua-DeLz5pfA.js} +1 -1
- package/apps/web/dist/assets/{marko-w4Xaqrn8.js → marko-DqlzpgnR.js} +1 -1
- package/apps/web/dist/assets/{mdc-CipE4q24.js → mdc-CRcSVpSn.js} +1 -1
- package/apps/web/dist/assets/{nginx-CidGWCuA.js → nginx-BvszHPa-.js} +1 -1
- package/apps/web/dist/assets/{nim-DqvuZ6-V.js → nim-B19-Hebo.js} +1 -1
- package/apps/web/dist/assets/{org-C8k2emY1.js → org-Dx7yMTgG.js} +1 -1
- package/apps/web/dist/assets/{perl-D06UK6ou.js → perl-C-dISCJW.js} +1 -1
- package/apps/web/dist/assets/{php-POxwc-vY.js → php-BHBxrwsl.js} +1 -1
- package/apps/web/dist/assets/{pug-BzSRzYD8.js → pug-4a3x4vRs.js} +1 -1
- package/apps/web/dist/assets/{qml-DZF7i2J0.js → qml-CGOhzFqi.js} +1 -1
- package/apps/web/dist/assets/{r-Cem3rJmc.js → r-8jr0rtQL.js} +1 -1
- package/apps/web/dist/assets/{razor-DG8dmRN8.js → razor-BDrURPIA.js} +1 -1
- package/apps/web/dist/assets/{regexp-B4kXooBf.js → regexp-Cg-MfP18.js} +1 -1
- package/apps/web/dist/assets/{rst-DKLtDtVi.js → rst-0OrP55QZ.js} +1 -1
- package/apps/web/dist/assets/{ruby-CamS7fke.js → ruby-dAApWLhj.js} +1 -1
- package/apps/web/dist/assets/{sas-B0QV8BKG.js → sas-3_hKTjL9.js} +1 -1
- package/apps/web/dist/assets/{scss-PDWZBsRp.js → scss-D6ollEQv.js} +1 -1
- package/apps/web/dist/assets/{shellscript-CdzFXVZ3.js → shellscript-V3H5Rwnn.js} +1 -1
- package/apps/web/dist/assets/{shellsession-BTvaJuzj.js → shellsession-TxydKEoU.js} +1 -1
- package/apps/web/dist/assets/{soy-CKjkt_DB.js → soy-t3rHc1tL.js} +1 -1
- package/apps/web/dist/assets/{sql-CvyoBOik.js → sql-B-NkeM8R.js} +1 -1
- package/apps/web/dist/assets/{stata-DKSZtLec.js → stata-BW6neeVL.js} +1 -1
- package/apps/web/dist/assets/{surrealql-DLiWChn8.js → surrealql-CRBKdx6K.js} +1 -1
- package/apps/web/dist/assets/{svelte-BEKUDT0w.js → svelte-DTTmtyn-.js} +1 -1
- package/apps/web/dist/assets/{templ-Cwpj8Ert.js → templ-CCBYQttY.js} +1 -1
- package/apps/web/dist/assets/{tex-eE29FuOr.js → tex-D9XxqR5N.js} +1 -1
- package/apps/web/dist/assets/{ts-tags-CS_oR42m.js → ts-tags-CI4pWHnF.js} +1 -1
- package/apps/web/dist/assets/{tsx-CXh89N3W.js → tsx-DlfK3Vuf.js} +1 -1
- package/apps/web/dist/assets/{twig-DE2-3Vmd.js → twig-D-C2s1f2.js} +1 -1
- package/apps/web/dist/assets/{typescript-xOdvp9de.js → typescript-BnqYTwqM.js} +1 -1
- package/apps/web/dist/assets/{typst-B_JeYRUR.js → typst-DdTyavvE.js} +1 -1
- package/apps/web/dist/assets/{vue-html-UHMeN4EH.js → vue-html-CIXNcZez.js} +1 -1
- package/apps/web/dist/assets/{vue-vine-Cl2xkrIS.js → vue-vine-Czo_W6u3.js} +1 -1
- package/apps/web/dist/assets/{vue-dnoj_d3g.js → vue-zbXTTw4n.js} +1 -1
- package/apps/web/dist/assets/{xml-BgkAAaUc.js → xml-DWv3jkXo.js} +1 -1
- package/apps/web/dist/assets/{xsl-C4H21Kbc.js → xsl-BiggF1J3.js} +1 -1
- package/apps/web/dist/assets/{yaml-Ce1Tbjry.js → yaml-D9oC3m1d.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/sw.js +1 -1
- package/package.json +5 -3
- package/apps/web/dist/assets/index-5vPpgW8A.css +0 -2
- package/apps/web/dist/assets/index-BJCHT3_X.js +0 -1148
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { consola } from "consola";
|
|
5
|
-
import { renderUnicodeCompact } from "uqr";
|
|
6
5
|
import { WebSocketServer } from "ws";
|
|
7
6
|
import os from "node:os";
|
|
8
7
|
import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
9
|
-
import { randomBytes, randomUUID
|
|
8
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
9
|
import { execFile, spawn } from "node:child_process";
|
|
11
10
|
import readline from "node:readline";
|
|
11
|
+
import { z } from "zod";
|
|
12
12
|
import { Command } from "@commander-js/extra-typings";
|
|
13
13
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
14
|
import { OpenCode } from "@opencode/client";
|
|
15
15
|
import { ensure, headers } from "@opencode/client/service";
|
|
16
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
17
|
+
import { sumBy } from "es-toolkit";
|
|
16
18
|
import { DatabaseSync } from "node:sqlite";
|
|
19
|
+
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
20
|
+
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
21
|
+
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
17
22
|
import { lstat, open, readFile, readdir, readlink, stat } from "node:fs/promises";
|
|
18
23
|
import { promisify } from "node:util";
|
|
19
24
|
import webPush from "web-push";
|
|
20
25
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
21
26
|
//#region ../core/src/protocol.ts
|
|
22
|
-
const HOSTED_APP = "https://offsite.sayad.dev";
|
|
23
27
|
function encodeFrame(kind, id, data) {
|
|
24
28
|
const frame = new Uint8Array(5 + data.length);
|
|
25
29
|
frame[0] = kind;
|
|
@@ -36,6 +40,27 @@ function decodeFrame(frame) {
|
|
|
36
40
|
};
|
|
37
41
|
}
|
|
38
42
|
//#endregion
|
|
43
|
+
//#region src/constants.ts
|
|
44
|
+
const deprecatedModels = {
|
|
45
|
+
claude: [
|
|
46
|
+
"claude-sonnet-5",
|
|
47
|
+
"claude-opus-5",
|
|
48
|
+
"claude-fable-5",
|
|
49
|
+
"claude-haiku-4-5-20251001",
|
|
50
|
+
"claude-opus-4-8",
|
|
51
|
+
"claude-opus-4-7",
|
|
52
|
+
"claude-opus-4-6",
|
|
53
|
+
"claude-sonnet-4-6"
|
|
54
|
+
],
|
|
55
|
+
codex: [],
|
|
56
|
+
opencode: []
|
|
57
|
+
};
|
|
58
|
+
const defaultModels = {
|
|
59
|
+
claude: "claude-opus-5-5",
|
|
60
|
+
codex: null,
|
|
61
|
+
opencode: null
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
39
64
|
//#region src/lib/hub.ts
|
|
40
65
|
const clients = /* @__PURE__ */ new Set();
|
|
41
66
|
function publish$2(sessionId, event) {
|
|
@@ -45,305 +70,10 @@ function broadcast(event) {
|
|
|
45
70
|
for (const client of clients) client.send(event);
|
|
46
71
|
}
|
|
47
72
|
//#endregion
|
|
48
|
-
//#region src/args.ts
|
|
49
|
-
let command = "web";
|
|
50
|
-
let name = null;
|
|
51
|
-
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 the pairing token").option("--no-pair", "Don't create a pairing token or print pairing links").option("--fallback <url>", "Address to try when this one is unreachable, e.g. a Cloudflare tunnel");
|
|
52
|
-
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
53
|
-
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
54
|
-
command = "serve";
|
|
55
|
-
});
|
|
56
|
-
program.command("pair").description("Creates a pairing token for another browser and prints its links").option("--name <name>", "Name to recognize this browser by").action((options) => {
|
|
57
|
-
command = "pair";
|
|
58
|
-
name = options.name ?? null;
|
|
59
|
-
});
|
|
60
|
-
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
61
|
-
command = "unpair";
|
|
62
|
-
});
|
|
63
|
-
program.parse();
|
|
64
|
-
const options = program.opts();
|
|
65
|
-
const args = {
|
|
66
|
-
...options,
|
|
67
|
-
dir: path.resolve(options.dir),
|
|
68
|
-
command,
|
|
69
|
-
name
|
|
70
|
-
};
|
|
71
|
-
const serverDir = path.join(import.meta.dirname, "..");
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/storage/storage.ts
|
|
74
|
-
const home = args.dir;
|
|
75
|
-
const lockFile = path.join(home, "server.pid");
|
|
76
|
-
mkdirSync(home, { recursive: true });
|
|
77
|
-
function lockHome() {
|
|
78
|
-
lock();
|
|
79
|
-
process.on("exit", () => unlinkSync(lockFile));
|
|
80
|
-
}
|
|
81
|
-
function lock() {
|
|
82
|
-
try {
|
|
83
|
-
writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
|
|
84
|
-
return;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (!hasCode(error, "EEXIST")) throw error;
|
|
87
|
-
}
|
|
88
|
-
let content;
|
|
89
|
-
let modified;
|
|
90
|
-
try {
|
|
91
|
-
content = readFileSync(lockFile, "utf8");
|
|
92
|
-
modified = statSync(lockFile).mtimeMs;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (hasCode(error, "ENOENT")) return lock();
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
const pid = Number(content);
|
|
98
|
-
const validPid = Number.isInteger(pid) && pid > 0;
|
|
99
|
-
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
100
|
-
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
101
|
-
consola.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
unlinkSync(lockFile);
|
|
105
|
-
lock();
|
|
106
|
-
}
|
|
107
|
-
function isRunning(pid) {
|
|
108
|
-
try {
|
|
109
|
-
process.kill(pid, 0);
|
|
110
|
-
return true;
|
|
111
|
-
} catch (error) {
|
|
112
|
-
if (hasCode(error, "ESRCH")) return false;
|
|
113
|
-
if (hasCode(error, "EPERM")) return true;
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function hasCode(error, code) {
|
|
118
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
119
|
-
}
|
|
120
|
-
function readJson(name) {
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(readFileSync(path.join(home, name), "utf8"));
|
|
123
|
-
} catch (error) {
|
|
124
|
-
if (error.code === "ENOENT") return null;
|
|
125
|
-
throw error;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
function writeJson(name, value) {
|
|
129
|
-
const file = path.join(home, name);
|
|
130
|
-
mkdirSync(home, { recursive: true });
|
|
131
|
-
writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
132
|
-
renameSync(`${file}.tmp`, file);
|
|
133
|
-
}
|
|
134
|
-
//#endregion
|
|
135
|
-
//#region src/storage/config.ts
|
|
136
|
-
const version = "0.0.0";
|
|
137
|
-
const configFile = path.join(home, "config.json");
|
|
138
|
-
const defaults = {
|
|
139
|
-
claudeBaseDir: path.join(os.homedir(), ".claude"),
|
|
140
|
-
composer: {
|
|
141
|
-
provider: "claude",
|
|
142
|
-
claude: {
|
|
143
|
-
accountId: null,
|
|
144
|
-
model: "haiku",
|
|
145
|
-
effort: "low",
|
|
146
|
-
mode: "bypassPermissions",
|
|
147
|
-
plan: false
|
|
148
|
-
},
|
|
149
|
-
codex: {
|
|
150
|
-
accountId: null,
|
|
151
|
-
model: null,
|
|
152
|
-
effort: null,
|
|
153
|
-
mode: "auto",
|
|
154
|
-
plan: false
|
|
155
|
-
},
|
|
156
|
-
opencode: {
|
|
157
|
-
accountId: "opencode",
|
|
158
|
-
model: null,
|
|
159
|
-
effort: null,
|
|
160
|
-
mode: null,
|
|
161
|
-
plan: false
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
commitModels: {
|
|
165
|
-
claude: "haiku",
|
|
166
|
-
codex: "gpt-6-luna",
|
|
167
|
-
opencode: null
|
|
168
|
-
},
|
|
169
|
-
terminal: {
|
|
170
|
-
shell: {
|
|
171
|
-
macos: null,
|
|
172
|
-
linux: null,
|
|
173
|
-
windows: null
|
|
174
|
-
},
|
|
175
|
-
behavior: {
|
|
176
|
-
terminalIdentity: "offsite",
|
|
177
|
-
shellIntegration: true
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
const retired = [
|
|
182
|
-
"theme",
|
|
183
|
-
"diff",
|
|
184
|
-
"terminal.font",
|
|
185
|
-
"terminal.cursor",
|
|
186
|
-
"terminal.padding",
|
|
187
|
-
"terminal.scrollback",
|
|
188
|
-
"terminal.boldIsBright",
|
|
189
|
-
"terminal.behavior.modernizeShiftEnter",
|
|
190
|
-
"terminal.behavior.pasteBrowserImages",
|
|
191
|
-
"terminal.behavior.captureShortcuts",
|
|
192
|
-
"terminal.behavior.bellFlash",
|
|
193
|
-
"terminal.behavior.bellSound",
|
|
194
|
-
"terminal.behavior.clickToMoveCursor",
|
|
195
|
-
"terminal.behavior.optionAsMeta",
|
|
196
|
-
"terminal.behavior.confirmCloseBusyTab",
|
|
197
|
-
"composer.accountId",
|
|
198
|
-
"composer.model",
|
|
199
|
-
"composer.effort",
|
|
200
|
-
"composer.permissionMode"
|
|
201
|
-
];
|
|
202
|
-
const choices = {
|
|
203
|
-
"composer.provider": [
|
|
204
|
-
"claude",
|
|
205
|
-
"codex",
|
|
206
|
-
"opencode"
|
|
207
|
-
],
|
|
208
|
-
"composer.claude.effort": [
|
|
209
|
-
"low",
|
|
210
|
-
"medium",
|
|
211
|
-
"high",
|
|
212
|
-
"xhigh",
|
|
213
|
-
"max"
|
|
214
|
-
],
|
|
215
|
-
"composer.claude.mode": [
|
|
216
|
-
"default",
|
|
217
|
-
"acceptEdits",
|
|
218
|
-
"bypassPermissions",
|
|
219
|
-
"plan",
|
|
220
|
-
"dontAsk",
|
|
221
|
-
"auto"
|
|
222
|
-
],
|
|
223
|
-
"composer.codex.mode": [
|
|
224
|
-
"read-only",
|
|
225
|
-
"auto",
|
|
226
|
-
"full-access"
|
|
227
|
-
],
|
|
228
|
-
"terminal.behavior.terminalIdentity": [
|
|
229
|
-
"offsite",
|
|
230
|
-
"ghostty",
|
|
231
|
-
"iterm2"
|
|
232
|
-
]
|
|
233
|
-
};
|
|
234
|
-
const nullable = [
|
|
235
|
-
"composer.claude.accountId",
|
|
236
|
-
"composer.claude.model",
|
|
237
|
-
"composer.claude.effort",
|
|
238
|
-
"composer.claude.mode",
|
|
239
|
-
"composer.codex.accountId",
|
|
240
|
-
"composer.codex.model",
|
|
241
|
-
"composer.codex.effort",
|
|
242
|
-
"composer.codex.mode",
|
|
243
|
-
"composer.opencode.accountId",
|
|
244
|
-
"composer.opencode.model",
|
|
245
|
-
"composer.opencode.effort",
|
|
246
|
-
"composer.opencode.mode",
|
|
247
|
-
"commitModels.claude",
|
|
248
|
-
"commitModels.codex",
|
|
249
|
-
"commitModels.opencode",
|
|
250
|
-
"terminal.shell.macos",
|
|
251
|
-
"terminal.shell.linux",
|
|
252
|
-
"terminal.shell.windows"
|
|
253
|
-
];
|
|
254
|
-
function platform() {
|
|
255
|
-
if (process.platform === "darwin") return "macos";
|
|
256
|
-
if (process.platform === "linux") return "linux";
|
|
257
|
-
if (process.platform === "win32") return "windows";
|
|
258
|
-
throw new Error(`unsupported platform ${process.platform}`);
|
|
259
|
-
}
|
|
260
|
-
function defaultShell() {
|
|
261
|
-
if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
|
|
262
|
-
return os.userInfo().shell ?? "/bin/sh";
|
|
263
|
-
}
|
|
264
|
-
function configInfo() {
|
|
265
|
-
return {
|
|
266
|
-
config: readConfig(),
|
|
267
|
-
defaults,
|
|
268
|
-
platform: platform(),
|
|
269
|
-
version,
|
|
270
|
-
defaultShell: defaultShell()
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
function readConfig() {
|
|
274
|
-
let user;
|
|
275
|
-
try {
|
|
276
|
-
user = readJson("config.json");
|
|
277
|
-
} catch (error) {
|
|
278
|
-
console.warn(`invalid ${configFile}, using the default config: ${error}`);
|
|
279
|
-
return structuredClone(defaults);
|
|
280
|
-
}
|
|
281
|
-
if (user === null) return structuredClone(defaults);
|
|
282
|
-
if (!isObject(user)) {
|
|
283
|
-
console.warn(`${configFile} is not an object, using the default config`);
|
|
284
|
-
return structuredClone(defaults);
|
|
285
|
-
}
|
|
286
|
-
let config = structuredClone(defaults);
|
|
287
|
-
for (const [key, value] of Object.entries(user)) config = merge(config, [key], value);
|
|
288
|
-
return config;
|
|
289
|
-
}
|
|
290
|
-
function saveConfig(value) {
|
|
291
|
-
const error = invalid(value, defaults, []);
|
|
292
|
-
if (error !== null) throw new Error(error);
|
|
293
|
-
writeJson("config.json", value);
|
|
294
|
-
process.env.CLAUDE_CONFIG_DIR = value.claudeBaseDir;
|
|
295
|
-
broadcast({
|
|
296
|
-
type: "config",
|
|
297
|
-
config: value
|
|
298
|
-
});
|
|
299
|
-
return value;
|
|
300
|
-
}
|
|
301
|
-
function merge(config, keys, user) {
|
|
302
|
-
if (retired.includes(keys.join("."))) return config;
|
|
303
|
-
if (isObject(keys.reduce((value, key) => isObject(value) ? value[key] : void 0, config)) && isObject(user)) {
|
|
304
|
-
let next = config;
|
|
305
|
-
for (const [key, value] of Object.entries(user)) next = merge(next, [...keys, key], value);
|
|
306
|
-
return next;
|
|
307
|
-
}
|
|
308
|
-
const candidate = structuredClone(config);
|
|
309
|
-
const parent = keys.slice(0, -1).reduce((value, key) => value[key], candidate);
|
|
310
|
-
parent[keys[keys.length - 1]] = user;
|
|
311
|
-
const error = invalid(candidate, defaults, []);
|
|
312
|
-
if (error === null) return candidate;
|
|
313
|
-
console.warn(`invalid ${keys.join(".")} in ${configFile}, using its default: ${error}`);
|
|
314
|
-
return config;
|
|
315
|
-
}
|
|
316
|
-
function invalid(value, template, keys) {
|
|
317
|
-
const name = keys.join(".");
|
|
318
|
-
if (isObject(template)) {
|
|
319
|
-
if (!isObject(value)) return `${name || "config"} must be an object`;
|
|
320
|
-
for (const key of Object.keys(value)) if (!(key in template)) return `unknown setting ${[...keys, key].join(".")}`;
|
|
321
|
-
for (const key of Object.keys(template)) {
|
|
322
|
-
if (!(key in value)) return `missing setting ${[...keys, key].join(".")}`;
|
|
323
|
-
const error = invalid(value[key], template[key], [...keys, key]);
|
|
324
|
-
if (error !== null) return error;
|
|
325
|
-
}
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
if (nullable.includes(name) && value === null) return null;
|
|
329
|
-
if (name === "claudeBaseDir") return typeof value === "string" && path.isAbsolute(value) ? null : "claudeBaseDir must be an absolute path";
|
|
330
|
-
if (name in choices) {
|
|
331
|
-
if (choices[name].includes(value)) return null;
|
|
332
|
-
return `${name} must be one of ${choices[name].join(", ")}`;
|
|
333
|
-
}
|
|
334
|
-
if (template === null) {
|
|
335
|
-
if (typeof value === "string") return null;
|
|
336
|
-
return `${name} must be text or null`;
|
|
337
|
-
}
|
|
338
|
-
if (typeof value !== typeof template) return `${name} must be a ${typeof template}`;
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
function isObject(value) {
|
|
342
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
343
|
-
}
|
|
344
|
-
//#endregion
|
|
345
73
|
//#region src/lib/codex/app-server.ts
|
|
346
74
|
const IDLE_LIMIT$3 = 18e5;
|
|
75
|
+
const START_TIMEOUT = 15e3;
|
|
76
|
+
const REQUEST_TIMEOUT = 3e4;
|
|
347
77
|
const codexBaseDir = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
348
78
|
const servers = /* @__PURE__ */ new Map();
|
|
349
79
|
function appServer(home) {
|
|
@@ -356,6 +86,9 @@ function appServer(home) {
|
|
|
356
86
|
function stopAppServers() {
|
|
357
87
|
for (const server of servers.values()) server.stop();
|
|
358
88
|
}
|
|
89
|
+
function stopAppServer(home) {
|
|
90
|
+
servers.get(home)?.stop();
|
|
91
|
+
}
|
|
359
92
|
function findCodex() {
|
|
360
93
|
const name = process.platform === "win32" ? "codex.exe" : "codex";
|
|
361
94
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
@@ -395,19 +128,22 @@ var AppServer = class {
|
|
|
395
128
|
clientInfo: {
|
|
396
129
|
name: "offsite",
|
|
397
130
|
title: "Offsite",
|
|
398
|
-
version
|
|
131
|
+
version: "0.0.0"
|
|
399
132
|
},
|
|
400
133
|
capabilities: {
|
|
401
134
|
experimentalApi: true,
|
|
402
135
|
requestAttestation: false
|
|
403
136
|
}
|
|
404
|
-
}).then(() => this.write({ method: "initialized" }))
|
|
137
|
+
}, START_TIMEOUT).then(() => this.write({ method: "initialized" }), (error) => {
|
|
138
|
+
this.child.kill();
|
|
139
|
+
throw error;
|
|
140
|
+
});
|
|
405
141
|
this.armIdleTimer();
|
|
406
142
|
}
|
|
407
143
|
async request(method, params) {
|
|
408
144
|
await this.ready;
|
|
409
145
|
this.armIdleTimer();
|
|
410
|
-
return await this.call(method, params);
|
|
146
|
+
return await this.call(method, params, REQUEST_TIMEOUT);
|
|
411
147
|
}
|
|
412
148
|
reply(id, result) {
|
|
413
149
|
this.write({
|
|
@@ -429,14 +165,26 @@ var AppServer = class {
|
|
|
429
165
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
430
166
|
this.child.stdin.end();
|
|
431
167
|
}
|
|
432
|
-
call(method, params) {
|
|
168
|
+
call(method, params, timeout) {
|
|
433
169
|
if (this.exited !== null) return Promise.reject(this.exited);
|
|
434
170
|
this.next += 1;
|
|
435
171
|
const id = this.next;
|
|
436
|
-
const result = new Promise((resolve, reject) =>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
172
|
+
const result = new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
this.waiting.delete(id);
|
|
175
|
+
reject(/* @__PURE__ */ new Error(`Codex did not answer ${method} within ${timeout / 1e3} seconds`));
|
|
176
|
+
}, timeout);
|
|
177
|
+
this.waiting.set(id, {
|
|
178
|
+
resolve: (value) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve(value);
|
|
181
|
+
},
|
|
182
|
+
reject: (error) => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
reject(error);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
440
188
|
this.write({
|
|
441
189
|
id,
|
|
442
190
|
method,
|
|
@@ -617,79 +365,207 @@ function isMirror(dir) {
|
|
|
617
365
|
}
|
|
618
366
|
}
|
|
619
367
|
//#endregion
|
|
620
|
-
//#region src/lib/
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
368
|
+
//#region src/lib/claude/content.ts
|
|
369
|
+
const imageTypes = [
|
|
370
|
+
"image/png",
|
|
371
|
+
"image/jpeg",
|
|
372
|
+
"image/gif",
|
|
373
|
+
"image/webp"
|
|
374
|
+
];
|
|
375
|
+
function toMessage(type, item, time) {
|
|
376
|
+
return {
|
|
377
|
+
type,
|
|
378
|
+
uuid: item.uuid,
|
|
379
|
+
time,
|
|
380
|
+
content: isRecord(item.message) ? toBlocks(item.message.content, answersOf(item.tool_use_result)) : [],
|
|
381
|
+
parent: item.parent_tool_use_id
|
|
382
|
+
};
|
|
633
383
|
}
|
|
634
|
-
function
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
384
|
+
function toUserInput(prompt, uuid, priority) {
|
|
385
|
+
for (const image of prompt.images) if (!imageTypes.includes(image.mediaType)) throw new Error(`${image.mediaType} images are not supported`);
|
|
386
|
+
return {
|
|
387
|
+
type: "user",
|
|
388
|
+
message: {
|
|
389
|
+
role: "user",
|
|
390
|
+
content: [...prompt.images.map((image) => ({
|
|
391
|
+
type: "image",
|
|
392
|
+
source: {
|
|
393
|
+
type: "base64",
|
|
394
|
+
media_type: image.mediaType,
|
|
395
|
+
data: image.data
|
|
396
|
+
}
|
|
397
|
+
})), ...prompt.text === "" ? [] : [{
|
|
398
|
+
type: "text",
|
|
399
|
+
text: prompt.text
|
|
400
|
+
}]]
|
|
401
|
+
},
|
|
402
|
+
parent_tool_use_id: null,
|
|
403
|
+
uuid,
|
|
404
|
+
...priority === void 0 ? {} : { priority }
|
|
405
|
+
};
|
|
638
406
|
}
|
|
639
|
-
function
|
|
407
|
+
function promptMessage(prompt, uuid) {
|
|
640
408
|
return {
|
|
641
|
-
|
|
642
|
-
|
|
409
|
+
type: "user",
|
|
410
|
+
uuid,
|
|
643
411
|
time: Date.now(),
|
|
644
|
-
|
|
645
|
-
|
|
412
|
+
content: [...prompt.images.map((image) => ({
|
|
413
|
+
type: "image",
|
|
414
|
+
...image
|
|
415
|
+
})), ...prompt.text === "" ? [] : [{
|
|
416
|
+
type: "text",
|
|
417
|
+
text: prompt.text
|
|
418
|
+
}]],
|
|
419
|
+
parent: null
|
|
646
420
|
};
|
|
647
421
|
}
|
|
648
|
-
function
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
time
|
|
422
|
+
function notice(text) {
|
|
423
|
+
return {
|
|
424
|
+
type: "notice",
|
|
425
|
+
uuid: randomUUID(),
|
|
426
|
+
time: Date.now(),
|
|
427
|
+
text,
|
|
428
|
+
isError: true
|
|
653
429
|
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
430
|
+
}
|
|
431
|
+
function contextTokens(message) {
|
|
432
|
+
if (!isRecord(message) || !isRecord(message.usage)) return null;
|
|
433
|
+
const usage = message.usage;
|
|
434
|
+
return [
|
|
435
|
+
usage.input_tokens,
|
|
436
|
+
usage.cache_creation_input_tokens,
|
|
437
|
+
usage.cache_read_input_tokens,
|
|
438
|
+
usage.output_tokens
|
|
439
|
+
].reduce((total, value) => total + (typeof value === "number" ? value : 0), 0);
|
|
440
|
+
}
|
|
441
|
+
function answersOf(output) {
|
|
442
|
+
if (!isRecord(output) || !isRecord(output.answers)) return null;
|
|
443
|
+
const answers = {};
|
|
444
|
+
for (const [question, answer] of Object.entries(output.answers)) {
|
|
445
|
+
if (typeof answer !== "string") return null;
|
|
446
|
+
answers[question] = answer;
|
|
447
|
+
}
|
|
448
|
+
return answers;
|
|
449
|
+
}
|
|
450
|
+
function toBlocks(content, answers) {
|
|
451
|
+
if (typeof content === "string") return [{
|
|
452
|
+
type: "text",
|
|
453
|
+
text: content
|
|
454
|
+
}];
|
|
455
|
+
if (!Array.isArray(content)) return [];
|
|
456
|
+
return content.flatMap((block) => {
|
|
457
|
+
if (!isRecord(block)) return [];
|
|
458
|
+
switch (block.type) {
|
|
459
|
+
case "text": return typeof block.text === "string" && !block.text.startsWith("<system-reminder>") && !block.text.startsWith("<local-command-caveat>") ? [{
|
|
460
|
+
type: "text",
|
|
461
|
+
text: block.text
|
|
462
|
+
}] : [];
|
|
463
|
+
case "thinking": return typeof block.thinking === "string" && block.thinking !== "" ? [{
|
|
464
|
+
type: "thinking",
|
|
465
|
+
thinking: block.thinking
|
|
466
|
+
}] : [];
|
|
467
|
+
case "image": {
|
|
468
|
+
const source = block.source;
|
|
469
|
+
return isRecord(source) && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string" ? [{
|
|
470
|
+
type: "image",
|
|
471
|
+
mediaType: source.media_type,
|
|
472
|
+
data: source.data
|
|
473
|
+
}] : [];
|
|
474
|
+
}
|
|
475
|
+
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && isRecord(block.input) ? [{
|
|
476
|
+
type: "tool_use",
|
|
477
|
+
id: block.id,
|
|
478
|
+
name: block.name,
|
|
479
|
+
input: block.input
|
|
480
|
+
}] : [];
|
|
481
|
+
case "tool_result": return typeof block.tool_use_id === "string" ? [{
|
|
482
|
+
type: "tool_result",
|
|
483
|
+
toolUseId: block.tool_use_id,
|
|
484
|
+
content: toBlocks(block.content, null).filter((item) => item.type === "text"),
|
|
485
|
+
isError: block.is_error === true,
|
|
486
|
+
answers
|
|
487
|
+
}] : [];
|
|
488
|
+
default: return [];
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function isRecord(value) {
|
|
493
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/lib/codex/items.ts
|
|
497
|
+
const statuses$1 = {
|
|
498
|
+
inProgress: "running",
|
|
499
|
+
completed: "completed",
|
|
500
|
+
failed: "failed",
|
|
501
|
+
declined: "stopped",
|
|
502
|
+
interrupted: "stopped"
|
|
503
|
+
};
|
|
504
|
+
function turnItems(turns, parent) {
|
|
505
|
+
return turns.flatMap((turn) => [...turn.items.flatMap((item) => toItems$1(turn.id, item, parent, turn.startedAt === null ? null : turn.startedAt * 1e3)), ...turn.status === "failed" && turn.error !== null ? [errorItem(`${turn.id}:error`, turn.error.message)] : []]);
|
|
506
|
+
}
|
|
507
|
+
function itemId(turnId, id) {
|
|
508
|
+
return `${turnId}:${id}`;
|
|
509
|
+
}
|
|
510
|
+
function turnOf(itemId) {
|
|
511
|
+
const index = itemId.indexOf(":");
|
|
512
|
+
if (index < 0) throw new Error(`${itemId} is not a Codex item`);
|
|
513
|
+
return itemId.slice(0, index);
|
|
514
|
+
}
|
|
515
|
+
function errorItem(id, text) {
|
|
516
|
+
return {
|
|
517
|
+
id,
|
|
518
|
+
parent: null,
|
|
519
|
+
time: Date.now(),
|
|
520
|
+
kind: "error",
|
|
521
|
+
text
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function toItems$1(turnId, item, parent, time) {
|
|
525
|
+
const base = {
|
|
526
|
+
id: itemId(turnId, item.id),
|
|
527
|
+
parent,
|
|
528
|
+
time
|
|
529
|
+
};
|
|
530
|
+
switch (item.type) {
|
|
531
|
+
case "userMessage": return [{
|
|
532
|
+
...base,
|
|
533
|
+
kind: "user",
|
|
534
|
+
text: item.content.flatMap(userText$1).join("\n").trim(),
|
|
535
|
+
images: item.content.flatMap(userImages),
|
|
536
|
+
format: "text"
|
|
537
|
+
}];
|
|
538
|
+
case "agentMessage": return item.text.trim() === "" ? [] : [{
|
|
539
|
+
...base,
|
|
540
|
+
kind: "assistant",
|
|
541
|
+
text: item.text,
|
|
542
|
+
message: base.id
|
|
543
|
+
}];
|
|
544
|
+
case "reasoning": {
|
|
545
|
+
const text = [...item.summary, ...item.content].join("\n\n").trim();
|
|
546
|
+
return text === "" ? [] : [{
|
|
547
|
+
...base,
|
|
548
|
+
kind: "reasoning",
|
|
549
|
+
text
|
|
550
|
+
}];
|
|
551
|
+
}
|
|
552
|
+
case "plan": return [{
|
|
553
|
+
...base,
|
|
554
|
+
kind: "plan",
|
|
555
|
+
plan: item.text
|
|
556
|
+
}];
|
|
557
|
+
case "commandExecution": return [{
|
|
558
|
+
...base,
|
|
559
|
+
kind: "command",
|
|
560
|
+
command: item.command,
|
|
561
|
+
description: "",
|
|
562
|
+
output: item.aggregatedOutput ?? "",
|
|
563
|
+
status: statuses$1[item.status],
|
|
564
|
+
background: null
|
|
565
|
+
}];
|
|
566
|
+
case "fileChange": return item.changes.map((change, index) => ({
|
|
567
|
+
...base,
|
|
568
|
+
id: `${base.id}:${index}`,
|
|
693
569
|
kind: "file_change",
|
|
694
570
|
path: change.path,
|
|
695
571
|
diffs: change.kind.type === "add" ? [{
|
|
@@ -700,7 +576,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
700
576
|
after: ""
|
|
701
577
|
}] : hunks(change.diff),
|
|
702
578
|
output: "",
|
|
703
|
-
status: statuses[item.status]
|
|
579
|
+
status: statuses$1[item.status]
|
|
704
580
|
}));
|
|
705
581
|
case "mcpToolCall": return [{
|
|
706
582
|
...base,
|
|
@@ -708,7 +584,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
708
584
|
name: `${item.server}.${item.tool}`,
|
|
709
585
|
input: record$1(item.arguments),
|
|
710
586
|
output: item.error?.message ?? (item.result === null ? "" : contentText$1(item.result.content)),
|
|
711
|
-
status: statuses[item.status]
|
|
587
|
+
status: statuses$1[item.status]
|
|
712
588
|
}];
|
|
713
589
|
case "dynamicToolCall": return [{
|
|
714
590
|
...base,
|
|
@@ -716,7 +592,7 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
716
592
|
name: item.tool,
|
|
717
593
|
input: record$1(item.arguments),
|
|
718
594
|
output: contentText$1(item.contentItems ?? []),
|
|
719
|
-
status: statuses[item.status]
|
|
595
|
+
status: statuses$1[item.status]
|
|
720
596
|
}];
|
|
721
597
|
case "collabAgentToolCall":
|
|
722
598
|
if (item.tool === "spawnAgent") return [{
|
|
@@ -725,14 +601,14 @@ function toItems$1(turnId, item, parent, time) {
|
|
|
725
601
|
agent: item.model ?? "Agent",
|
|
726
602
|
description: item.prompt ?? "",
|
|
727
603
|
agentId: item.receiverThreadIds[0] ?? null,
|
|
728
|
-
status: statuses[item.status]
|
|
604
|
+
status: statuses$1[item.status]
|
|
729
605
|
}];
|
|
730
606
|
return [{
|
|
731
607
|
...base,
|
|
732
608
|
kind: "agent_message",
|
|
733
609
|
target: item.receiverThreadIds[0] ?? null,
|
|
734
610
|
summary: item.prompt ?? item.tool,
|
|
735
|
-
status: statuses[item.status]
|
|
611
|
+
status: statuses$1[item.status]
|
|
736
612
|
}];
|
|
737
613
|
case "webSearch": return [{
|
|
738
614
|
...base,
|
|
@@ -813,53 +689,296 @@ function hunks(diff) {
|
|
|
813
689
|
return result;
|
|
814
690
|
}
|
|
815
691
|
function contentText$1(content) {
|
|
816
|
-
return content.map((block) =>
|
|
692
|
+
return content.map((block) => isRecord(block) && typeof block.text === "string" ? block.text : JSON.stringify(block)).join("\n");
|
|
817
693
|
}
|
|
818
694
|
function record$1(value) {
|
|
819
|
-
|
|
820
|
-
return { value };
|
|
695
|
+
return isRecord(value) ? value : { value };
|
|
821
696
|
}
|
|
822
697
|
//#endregion
|
|
823
|
-
//#region src/
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
698
|
+
//#region src/args.ts
|
|
699
|
+
let command = "web";
|
|
700
|
+
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 the pairing token").option("--no-pair", "Don't create a pairing token or print pairing links").option("--fallback <url>", "Address to try when this one is unreachable, e.g. a Cloudflare tunnel");
|
|
702
|
+
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
703
|
+
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
704
|
+
command = "serve";
|
|
705
|
+
});
|
|
706
|
+
program.command("pair").description("Creates a pairing token for another browser and prints its links").option("--name <name>", "Name to recognize this browser by").action((options) => {
|
|
707
|
+
command = "pair";
|
|
708
|
+
name = options.name ?? null;
|
|
709
|
+
});
|
|
710
|
+
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
711
|
+
command = "unpair";
|
|
712
|
+
});
|
|
713
|
+
program.parse();
|
|
714
|
+
const options = program.opts();
|
|
715
|
+
const args = {
|
|
716
|
+
...options,
|
|
717
|
+
dir: path.resolve(options.dir),
|
|
718
|
+
command,
|
|
719
|
+
name
|
|
720
|
+
};
|
|
721
|
+
const serverDir = path.join(import.meta.dirname, "..");
|
|
722
|
+
//#endregion
|
|
723
|
+
//#region src/storage/storage.ts
|
|
724
|
+
const home = args.dir;
|
|
725
|
+
const lockFile = path.join(home, "server.pid");
|
|
726
|
+
mkdirSync(home, { recursive: true });
|
|
727
|
+
function lockHome() {
|
|
728
|
+
lock();
|
|
729
|
+
process.on("exit", () => unlinkSync(lockFile));
|
|
834
730
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
})
|
|
731
|
+
function lock() {
|
|
732
|
+
try {
|
|
733
|
+
writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
|
|
734
|
+
return;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
737
|
+
}
|
|
738
|
+
let content;
|
|
739
|
+
let modified;
|
|
740
|
+
try {
|
|
741
|
+
content = readFileSync(lockFile, "utf8");
|
|
742
|
+
modified = statSync(lockFile).mtimeMs;
|
|
743
|
+
} catch (error) {
|
|
744
|
+
if (hasCode(error, "ENOENT")) return lock();
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
const pid = Number(content);
|
|
748
|
+
const validPid = Number.isInteger(pid) && pid > 0;
|
|
749
|
+
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
750
|
+
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
751
|
+
consola.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
752
|
+
process.exit(1);
|
|
753
|
+
}
|
|
754
|
+
unlinkSync(lockFile);
|
|
755
|
+
lock();
|
|
756
|
+
}
|
|
757
|
+
function isRunning(pid) {
|
|
758
|
+
try {
|
|
759
|
+
process.kill(pid, 0);
|
|
760
|
+
return true;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (hasCode(error, "ESRCH")) return false;
|
|
763
|
+
if (hasCode(error, "EPERM")) return true;
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function hasCode(error, code) {
|
|
768
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
769
|
+
}
|
|
770
|
+
function readJson(name) {
|
|
771
|
+
try {
|
|
772
|
+
return JSON.parse(readFileSync(path.join(home, name), "utf8"));
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (error.code === "ENOENT") return null;
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function writeJson(name, value) {
|
|
779
|
+
const file = path.join(home, name);
|
|
780
|
+
mkdirSync(home, { recursive: true });
|
|
781
|
+
writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
782
|
+
renameSync(`${file}.tmp`, file);
|
|
840
783
|
}
|
|
841
784
|
//#endregion
|
|
842
|
-
//#region src/
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
785
|
+
//#region src/storage/config.ts
|
|
786
|
+
const configFile = path.join(home, "config.json");
|
|
787
|
+
const modelSetting = z.strictObject({
|
|
788
|
+
hidden: z.boolean(),
|
|
789
|
+
effort: z.string().nullable()
|
|
790
|
+
});
|
|
791
|
+
const configSchema = z.strictObject({
|
|
792
|
+
claudeBaseDir: z.string().refine((value) => path.isAbsolute(value), "must be an absolute path").default(path.join(os.homedir(), ".claude")),
|
|
793
|
+
enabledProviders: z.strictObject({
|
|
794
|
+
claude: z.boolean().default(true),
|
|
795
|
+
codex: z.boolean().default(true),
|
|
796
|
+
opencode: z.boolean().default(true)
|
|
797
|
+
}).prefault({}),
|
|
798
|
+
composer: z.strictObject({
|
|
799
|
+
provider: z.enum([
|
|
800
|
+
"claude",
|
|
801
|
+
"codex",
|
|
802
|
+
"opencode"
|
|
803
|
+
]).default("claude"),
|
|
804
|
+
claude: z.strictObject({
|
|
805
|
+
accountId: z.string().nullable().default(null),
|
|
806
|
+
model: z.string().nullable().default(null),
|
|
807
|
+
effort: z.enum([
|
|
808
|
+
"low",
|
|
809
|
+
"medium",
|
|
810
|
+
"high",
|
|
811
|
+
"xhigh",
|
|
812
|
+
"max"
|
|
813
|
+
]).nullable().default(null),
|
|
814
|
+
mode: z.enum([
|
|
815
|
+
"default",
|
|
816
|
+
"acceptEdits",
|
|
817
|
+
"bypassPermissions",
|
|
818
|
+
"plan",
|
|
819
|
+
"dontAsk",
|
|
820
|
+
"auto"
|
|
821
|
+
]).nullable().default("bypassPermissions"),
|
|
822
|
+
plan: z.boolean().default(false)
|
|
823
|
+
}).prefault({}),
|
|
824
|
+
codex: z.strictObject({
|
|
825
|
+
accountId: z.string().nullable().default(null),
|
|
826
|
+
model: z.string().nullable().default(null),
|
|
827
|
+
effort: z.string().nullable().default(null),
|
|
828
|
+
mode: z.enum([
|
|
829
|
+
"read-only",
|
|
830
|
+
"auto",
|
|
831
|
+
"full-access"
|
|
832
|
+
]).nullable().default("auto"),
|
|
833
|
+
plan: z.boolean().default(false)
|
|
834
|
+
}).prefault({}),
|
|
835
|
+
opencode: z.strictObject({
|
|
836
|
+
accountId: z.null().default(null),
|
|
837
|
+
model: z.string().nullable().default(null),
|
|
838
|
+
effort: z.string().nullable().default(null),
|
|
839
|
+
mode: z.string().nullable().default(null),
|
|
840
|
+
plan: z.boolean().default(false)
|
|
841
|
+
}).prefault({})
|
|
842
|
+
}).prefault({}),
|
|
843
|
+
commitMessage: z.strictObject({
|
|
844
|
+
provider: z.enum([
|
|
845
|
+
"claude",
|
|
846
|
+
"codex",
|
|
847
|
+
"opencode"
|
|
848
|
+
]).nullable().default(null),
|
|
849
|
+
accountId: z.string().nullable().default(null),
|
|
850
|
+
model: z.string().nullable().default(null)
|
|
851
|
+
}).prefault({}),
|
|
852
|
+
models: z.strictObject({
|
|
853
|
+
claude: z.record(z.string(), modelSetting).default({}),
|
|
854
|
+
codex: z.record(z.string(), modelSetting).default({}),
|
|
855
|
+
opencode: z.record(z.string(), modelSetting).default({})
|
|
856
|
+
}).prefault({}),
|
|
857
|
+
delivery: z.enum(["queue", "steer"]).default("steer"),
|
|
858
|
+
autoResolve: z.strictObject({
|
|
859
|
+
questions: z.number().positive().nullable().default(3),
|
|
860
|
+
permissions: z.number().positive().nullable().default(1),
|
|
861
|
+
permissionDecision: z.enum(["allow", "deny"]).default("allow")
|
|
862
|
+
}).prefault({}),
|
|
863
|
+
terminal: z.strictObject({
|
|
864
|
+
shell: z.strictObject({
|
|
865
|
+
macos: z.string().nullable().default(null),
|
|
866
|
+
linux: z.string().nullable().default(null),
|
|
867
|
+
windows: z.string().nullable().default(null)
|
|
868
|
+
}).prefault({}),
|
|
869
|
+
behavior: z.strictObject({
|
|
870
|
+
terminalIdentity: z.enum([
|
|
871
|
+
"offsite",
|
|
872
|
+
"ghostty",
|
|
873
|
+
"iterm2"
|
|
874
|
+
]).default("offsite"),
|
|
875
|
+
shellIntegration: z.boolean().default(true)
|
|
876
|
+
}).prefault({})
|
|
877
|
+
}).prefault({})
|
|
878
|
+
});
|
|
879
|
+
const defaults = configSchema.parse({});
|
|
880
|
+
function platform() {
|
|
881
|
+
if (process.platform === "darwin") return "macos";
|
|
882
|
+
if (process.platform === "linux") return "linux";
|
|
883
|
+
if (process.platform === "win32") return "windows";
|
|
884
|
+
throw new Error(`unsupported platform ${process.platform}`);
|
|
885
|
+
}
|
|
886
|
+
function defaultShell() {
|
|
887
|
+
if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
|
|
888
|
+
return os.userInfo().shell ?? "/bin/sh";
|
|
889
|
+
}
|
|
890
|
+
function configInfo() {
|
|
891
|
+
return {
|
|
892
|
+
config: readConfig(),
|
|
893
|
+
defaults,
|
|
894
|
+
platform: platform(),
|
|
895
|
+
defaultShell: defaultShell()
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function readConfig() {
|
|
899
|
+
let user;
|
|
900
|
+
try {
|
|
901
|
+
user = readJson("config.json");
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.warn(`invalid ${configFile}, using the default config: ${error}`);
|
|
904
|
+
return structuredClone(defaults);
|
|
905
|
+
}
|
|
906
|
+
if (user === null) return structuredClone(defaults);
|
|
907
|
+
const result = configSchema.safeParse(user);
|
|
908
|
+
if (!result.success) {
|
|
909
|
+
console.warn(`invalid ${configFile}, using the default config:\n${z.prettifyError(result.error)}`);
|
|
910
|
+
return structuredClone(defaults);
|
|
911
|
+
}
|
|
912
|
+
return result.data;
|
|
913
|
+
}
|
|
914
|
+
function saveConfig(value) {
|
|
915
|
+
const result = configSchema.safeParse(value);
|
|
916
|
+
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
917
|
+
writeJson("config.json", result.data);
|
|
918
|
+
process.env.CLAUDE_CONFIG_DIR = result.data.claudeBaseDir;
|
|
919
|
+
broadcast({
|
|
920
|
+
type: "config",
|
|
921
|
+
config: result.data
|
|
922
|
+
});
|
|
923
|
+
return result.data;
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/lib/live.ts
|
|
927
|
+
const runs = /* @__PURE__ */ new Map();
|
|
928
|
+
function activity() {
|
|
929
|
+
const result = {};
|
|
930
|
+
for (const run of runs.values()) result[run.sessionId] = {
|
|
931
|
+
running: run.running,
|
|
932
|
+
waiting: run.pending.size > 0,
|
|
933
|
+
prompted: run.prompted,
|
|
934
|
+
tasks: run.tasks
|
|
935
|
+
};
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
function broadcastActivity() {
|
|
939
|
+
broadcast({
|
|
940
|
+
type: "activity",
|
|
941
|
+
activity: activity()
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function scheduleAutoResolve(sessionId, request) {
|
|
945
|
+
if (request.kind === "plan") return;
|
|
946
|
+
const settings = readConfig().autoResolve;
|
|
947
|
+
const minutes = request.kind === "question" ? settings.questions : settings.permissions;
|
|
948
|
+
if (minutes === null) return;
|
|
949
|
+
setTimeout(async () => {
|
|
950
|
+
const run = runs.get(sessionId);
|
|
951
|
+
if (run === void 0) return;
|
|
952
|
+
try {
|
|
953
|
+
if (request.kind === "question") await run.respond(request.id, "allow", {});
|
|
954
|
+
else await run.respond(request.id, readConfig().autoResolve.permissionDecision, null);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.warn(`could not auto-resolve ${request.id}: ${error}`);
|
|
957
|
+
}
|
|
958
|
+
}, minutes * 6e4);
|
|
959
|
+
}
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/lib/codex/requests.ts
|
|
962
|
+
function toRequest$1(requestId, method, params, items) {
|
|
963
|
+
const turnId = params.turnId;
|
|
964
|
+
switch (method) {
|
|
965
|
+
case "item/commandExecution/requestApproval": return {
|
|
966
|
+
id: requestId,
|
|
967
|
+
kind: "approval",
|
|
968
|
+
title: params.reason ?? "Run this command?",
|
|
969
|
+
canAlwaysAllow: true,
|
|
970
|
+
item: items.get(itemId(turnId, params.itemId)) ?? {
|
|
971
|
+
id: itemId(turnId, params.itemId),
|
|
972
|
+
parent: null,
|
|
973
|
+
time: Date.now(),
|
|
974
|
+
kind: "command",
|
|
975
|
+
command: params.command ?? "",
|
|
976
|
+
description: "",
|
|
977
|
+
output: "",
|
|
978
|
+
status: "running",
|
|
979
|
+
background: null
|
|
980
|
+
}
|
|
981
|
+
};
|
|
863
982
|
case "item/fileChange/requestApproval": return {
|
|
864
983
|
id: requestId,
|
|
865
984
|
kind: "approval",
|
|
@@ -975,6 +1094,7 @@ var CodexRun = class CodexRun {
|
|
|
975
1094
|
prompted = false;
|
|
976
1095
|
backgroundTasks = 0;
|
|
977
1096
|
tasks = {};
|
|
1097
|
+
activeAt = Date.now();
|
|
978
1098
|
model;
|
|
979
1099
|
close;
|
|
980
1100
|
turnId = null;
|
|
@@ -1079,11 +1199,12 @@ var CodexRun = class CodexRun {
|
|
|
1079
1199
|
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => console.warn(`could not unload Codex thread: ${error}`)).finally(() => this.end());
|
|
1080
1200
|
}
|
|
1081
1201
|
exited() {
|
|
1082
|
-
this.put(errorItem(randomUUID(), "Codex stopped"));
|
|
1202
|
+
this.put(errorItem(randomUUID(), this.pending.size === 0 ? "Codex stopped" : "Codex stopped, its open requests expired"));
|
|
1083
1203
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1084
1204
|
this.end();
|
|
1085
1205
|
}
|
|
1086
1206
|
notify(method, params) {
|
|
1207
|
+
this.activeAt = Date.now();
|
|
1087
1208
|
switch (method) {
|
|
1088
1209
|
case "turn/started": {
|
|
1089
1210
|
const turn = params.turn;
|
|
@@ -1181,6 +1302,7 @@ var CodexRun = class CodexRun {
|
|
|
1181
1302
|
request
|
|
1182
1303
|
});
|
|
1183
1304
|
broadcastActivity();
|
|
1305
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
1184
1306
|
}
|
|
1185
1307
|
async startTurn(prompt) {
|
|
1186
1308
|
clearTimeout(this.idleTimer);
|
|
@@ -1438,445 +1560,120 @@ async function readThread(threadId) {
|
|
|
1438
1560
|
})).thread;
|
|
1439
1561
|
}
|
|
1440
1562
|
//#endregion
|
|
1441
|
-
//#region src/
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1563
|
+
//#region src/lib/opencode/items.ts
|
|
1564
|
+
const toolStatuses = {
|
|
1565
|
+
streaming: "running",
|
|
1566
|
+
running: "running",
|
|
1567
|
+
completed: "completed",
|
|
1568
|
+
error: "failed"
|
|
1569
|
+
};
|
|
1570
|
+
const todoStatuses$1 = {
|
|
1571
|
+
pending: "pending",
|
|
1572
|
+
in_progress: "running",
|
|
1573
|
+
completed: "completed",
|
|
1574
|
+
cancelled: "completed"
|
|
1575
|
+
};
|
|
1576
|
+
function messageItems(message, parent) {
|
|
1577
|
+
const base = {
|
|
1578
|
+
id: message.id,
|
|
1579
|
+
parent,
|
|
1580
|
+
time: message.time.created
|
|
1581
|
+
};
|
|
1582
|
+
switch (message.type) {
|
|
1583
|
+
case "user": return [{
|
|
1584
|
+
...base,
|
|
1585
|
+
kind: "user",
|
|
1586
|
+
text: message.text,
|
|
1587
|
+
images: (message.files ?? []).filter((file) => file.mime.startsWith("image/")).map((file) => ({
|
|
1588
|
+
mediaType: file.mime,
|
|
1589
|
+
data: file.data
|
|
1590
|
+
})),
|
|
1591
|
+
format: "text"
|
|
1592
|
+
}];
|
|
1593
|
+
case "skill": return [{
|
|
1594
|
+
...base,
|
|
1595
|
+
kind: "skill",
|
|
1596
|
+
skill: message.name,
|
|
1597
|
+
output: "",
|
|
1598
|
+
status: "completed"
|
|
1599
|
+
}];
|
|
1600
|
+
case "shell": return [{
|
|
1601
|
+
...base,
|
|
1602
|
+
kind: "command",
|
|
1603
|
+
command: message.command,
|
|
1604
|
+
description: "",
|
|
1605
|
+
output: message.output?.output ?? "",
|
|
1606
|
+
status: message.status === "running" ? "running" : message.status === "exited" ? message.exit === 0 ? "completed" : "failed" : "stopped",
|
|
1607
|
+
background: null
|
|
1608
|
+
}];
|
|
1609
|
+
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1610
|
+
case "compaction": return message.status === "failed" ? [{
|
|
1611
|
+
...errorItem(message.id, message.error.message),
|
|
1612
|
+
parent
|
|
1613
|
+
}] : [{
|
|
1614
|
+
...base,
|
|
1615
|
+
kind: "compaction"
|
|
1616
|
+
}];
|
|
1617
|
+
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
1618
|
+
...errorItem(message.id, message.error.message),
|
|
1619
|
+
parent
|
|
1620
|
+
}] : [];
|
|
1621
|
+
case "agent-switched":
|
|
1622
|
+
case "model-switched":
|
|
1623
|
+
case "location-switched":
|
|
1624
|
+
case "synthetic":
|
|
1625
|
+
case "system": return [];
|
|
1491
1626
|
}
|
|
1492
|
-
publish$1();
|
|
1493
|
-
return null;
|
|
1494
1627
|
}
|
|
1495
|
-
function
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
const parent = path.dirname(folder);
|
|
1503
|
-
return {
|
|
1504
|
-
path: folder,
|
|
1505
|
-
parent: parent === folder ? null : parent,
|
|
1506
|
-
directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
1507
|
-
name: entry.name,
|
|
1508
|
-
path: path.join(folder, entry.name)
|
|
1509
|
-
})).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
|
1510
|
-
};
|
|
1628
|
+
function formQuestions(form) {
|
|
1629
|
+
return form.fields.flatMap((field) => field.type === "external" ? [] : [{
|
|
1630
|
+
question: questionText(field),
|
|
1631
|
+
header: field.title ?? "",
|
|
1632
|
+
multiSelect: field.type === "multiselect",
|
|
1633
|
+
options: fieldOptions(field)
|
|
1634
|
+
}]);
|
|
1511
1635
|
}
|
|
1512
|
-
function
|
|
1513
|
-
|
|
1514
|
-
type
|
|
1515
|
-
|
|
1516
|
-
|
|
1636
|
+
function formAnswer(form, answers) {
|
|
1637
|
+
return Object.fromEntries(form.fields.flatMap((field) => {
|
|
1638
|
+
if (field.type === "external") return [];
|
|
1639
|
+
const answer = answers[questionText(field)];
|
|
1640
|
+
if (answer === void 0 || answer === "") return [];
|
|
1641
|
+
switch (field.type) {
|
|
1642
|
+
case "string": return [[field.key, optionValue(field.options ?? [], answer)]];
|
|
1643
|
+
case "multiselect": return [[field.key, answer.split(", ").map((label) => optionValue(field.options, label))]];
|
|
1644
|
+
case "boolean":
|
|
1645
|
+
if (answer === "Yes") return [[field.key, true]];
|
|
1646
|
+
if (answer === "No") return [[field.key, false]];
|
|
1647
|
+
throw new Error(`${field.key} needs Yes or No`);
|
|
1648
|
+
case "number":
|
|
1649
|
+
case "integer": return [[field.key, Number(answer)]];
|
|
1650
|
+
}
|
|
1651
|
+
}));
|
|
1517
1652
|
}
|
|
1518
|
-
function
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
logo: row.logo
|
|
1653
|
+
function partItems(message, part, index, parent) {
|
|
1654
|
+
const base = {
|
|
1655
|
+
id: `${message.id}:${index}`,
|
|
1656
|
+
parent,
|
|
1657
|
+
time: message.time.created
|
|
1524
1658
|
};
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
const records = readRecords();
|
|
1544
|
-
const paths = projects$1().map((project) => project.path);
|
|
1545
|
-
const [claude, codex, opencode] = await Promise.all([
|
|
1546
|
-
Promise.all(paths.map(async (cwd) => (await listSessions({
|
|
1547
|
-
dir: cwd,
|
|
1548
|
-
includeWorktrees: false,
|
|
1549
|
-
includeProgrammatic: false
|
|
1550
|
-
})).map((item) => ({
|
|
1551
|
-
id: item.sessionId,
|
|
1552
|
-
provider: "claude",
|
|
1553
|
-
title: item.summary,
|
|
1554
|
-
cwd,
|
|
1555
|
-
branch: item.gitBranch ?? null,
|
|
1556
|
-
updatedAt: item.lastModified
|
|
1557
|
-
})))),
|
|
1558
|
-
codexThreads(paths).catch((error) => {
|
|
1559
|
-
console.warn(`could not list Codex threads: ${error}`);
|
|
1560
|
-
return [];
|
|
1561
|
-
}),
|
|
1562
|
-
openCodeSessions(paths).catch((error) => {
|
|
1563
|
-
console.warn(`could not list OpenCode sessions: ${error}`);
|
|
1564
|
-
return [];
|
|
1565
|
-
})
|
|
1566
|
-
]);
|
|
1567
|
-
return [
|
|
1568
|
-
...claude.flat(),
|
|
1569
|
-
...codex.map((thread) => ({
|
|
1570
|
-
id: thread.id,
|
|
1571
|
-
provider: "codex",
|
|
1572
|
-
title: thread.name ?? thread.preview,
|
|
1573
|
-
cwd: thread.cwd,
|
|
1574
|
-
branch: thread.gitInfo?.branch ?? null,
|
|
1575
|
-
updatedAt: thread.updatedAt * 1e3
|
|
1576
|
-
})),
|
|
1577
|
-
...opencode.map((session) => ({
|
|
1578
|
-
id: session.id,
|
|
1579
|
-
provider: "opencode",
|
|
1580
|
-
title: session.title ?? "New session",
|
|
1581
|
-
cwd: session.location.directory,
|
|
1582
|
-
branch: null,
|
|
1583
|
-
updatedAt: session.time.updated
|
|
1584
|
-
}))
|
|
1585
|
-
].map((item) => {
|
|
1586
|
-
providers.set(item.id, item.provider);
|
|
1587
|
-
const record = records.get(item.id);
|
|
1588
|
-
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
1589
|
-
return {
|
|
1590
|
-
...item,
|
|
1591
|
-
updatedAt,
|
|
1592
|
-
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
1593
|
-
};
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
function providerOf(sessionId) {
|
|
1597
|
-
const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
1598
|
-
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
1599
|
-
return provider;
|
|
1600
|
-
}
|
|
1601
|
-
function recordInteraction(id) {
|
|
1602
|
-
db.prepare("INSERT INTO sessions (session_id, interacted_at, status) VALUES (?, ?, 'active') ON CONFLICT (session_id) DO UPDATE SET interacted_at = excluded.interacted_at, status = CASE status WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END").run(id, Date.now());
|
|
1603
|
-
}
|
|
1604
|
-
async function publishSessions() {
|
|
1605
|
-
try {
|
|
1606
|
-
broadcast({
|
|
1607
|
-
type: "sessions",
|
|
1608
|
-
sessions: await sessions()
|
|
1609
|
-
});
|
|
1610
|
-
} catch (error) {
|
|
1611
|
-
console.warn(`could not list threads: ${error}`);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
function watchSessions() {
|
|
1615
|
-
for (const watcher of watchers) watcher.close();
|
|
1616
|
-
watchers = [path.join(readConfig().claudeBaseDir, "projects"), path.join(codexBaseDir, "sessions")].flatMap((dir) => {
|
|
1617
|
-
try {
|
|
1618
|
-
const watcher = watch(dir, { recursive: true }, refreshSessions);
|
|
1619
|
-
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
1620
|
-
return [watcher];
|
|
1621
|
-
} catch (error) {
|
|
1622
|
-
if (error.code !== "ENOENT") throw error;
|
|
1623
|
-
return [];
|
|
1624
|
-
}
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
function refreshSessions() {
|
|
1628
|
-
timer ??= setTimeout(() => {
|
|
1629
|
-
timer = void 0;
|
|
1630
|
-
publishSessions();
|
|
1631
|
-
}, 1e3);
|
|
1632
|
-
}
|
|
1633
|
-
async function updateSession(id, patch) {
|
|
1634
|
-
if (patch.title !== void 0) switch (providerOf(id)) {
|
|
1635
|
-
case "claude":
|
|
1636
|
-
await renameSession(id, patch.title);
|
|
1637
|
-
break;
|
|
1638
|
-
case "codex":
|
|
1639
|
-
await renameThread(id, patch.title);
|
|
1640
|
-
break;
|
|
1641
|
-
case "opencode": await renameOpenCode(id, patch.title);
|
|
1642
|
-
}
|
|
1643
|
-
if (patch.status === "inactive") db.prepare("DELETE FROM sessions WHERE session_id = ?").run(id);
|
|
1644
|
-
else if (patch.status !== void 0) db.prepare("INSERT INTO sessions (session_id, status) VALUES (?, ?) ON CONFLICT (session_id) DO UPDATE SET status = excluded.status").run(id, patch.status);
|
|
1645
|
-
}
|
|
1646
|
-
async function removeSession(id) {
|
|
1647
|
-
const provider = providerOf(id);
|
|
1648
|
-
const run = runs.get(id);
|
|
1649
|
-
if (run !== void 0) {
|
|
1650
|
-
run.stop();
|
|
1651
|
-
await run.closed;
|
|
1652
|
-
}
|
|
1653
|
-
switch (provider) {
|
|
1654
|
-
case "claude":
|
|
1655
|
-
await deleteSession(id);
|
|
1656
|
-
break;
|
|
1657
|
-
case "codex":
|
|
1658
|
-
await deleteThread(id);
|
|
1659
|
-
break;
|
|
1660
|
-
case "opencode": await removeOpenCode(id);
|
|
1661
|
-
}
|
|
1662
|
-
db.prepare("DELETE FROM sessions WHERE session_id = ?").run(id);
|
|
1663
|
-
}
|
|
1664
|
-
async function fork(sessionId, cwd, messageId) {
|
|
1665
|
-
const provider = providerOf(sessionId);
|
|
1666
|
-
const result = await forkWith(provider, sessionId, cwd, messageId);
|
|
1667
|
-
providers.set(result.sessionId, provider);
|
|
1668
|
-
recordInteraction(result.sessionId);
|
|
1669
|
-
return result;
|
|
1670
|
-
}
|
|
1671
|
-
async function forkWith(provider, sessionId, cwd, messageId) {
|
|
1672
|
-
if (provider === "codex") return forkThread(sessionId, messageId);
|
|
1673
|
-
if (provider === "opencode") return forkOpenCode(sessionId, messageId);
|
|
1674
|
-
if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
|
|
1675
|
-
return { sessionId: (await forkSession(sessionId, {
|
|
1676
|
-
dir: cwd,
|
|
1677
|
-
upToMessageId: messageId
|
|
1678
|
-
})).sessionId };
|
|
1679
|
-
}
|
|
1680
|
-
function readRecords() {
|
|
1681
|
-
const records = /* @__PURE__ */ new Map();
|
|
1682
|
-
for (const row of db.prepare("SELECT session_id, interacted_at, status FROM sessions").all()) {
|
|
1683
|
-
if (typeof row.session_id !== "string" || typeof row.interacted_at !== "number" && row.interacted_at !== null || row.status !== "active" && row.status !== "pinned" && row.status !== "minimized") throw new Error("The sessions table has an unexpected row");
|
|
1684
|
-
records.set(row.session_id, {
|
|
1685
|
-
interactedAt: row.interacted_at,
|
|
1686
|
-
status: row.status
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
return records;
|
|
1690
|
-
}
|
|
1691
|
-
//#endregion
|
|
1692
|
-
//#region src/lib/opencode/client.ts
|
|
1693
|
-
const listChanges = /* @__PURE__ */ new Set([
|
|
1694
|
-
"session.created",
|
|
1695
|
-
"session.renamed",
|
|
1696
|
-
"session.moved",
|
|
1697
|
-
"session.forked",
|
|
1698
|
-
"session.deleted",
|
|
1699
|
-
"session.execution.succeeded",
|
|
1700
|
-
"session.execution.failed",
|
|
1701
|
-
"session.execution.interrupted"
|
|
1702
|
-
]);
|
|
1703
|
-
let connection;
|
|
1704
|
-
function findOpenCode() {
|
|
1705
|
-
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
1706
|
-
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
1707
|
-
const candidate = path.join(dir, name);
|
|
1708
|
-
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
1709
|
-
}
|
|
1710
|
-
return null;
|
|
1711
|
-
}
|
|
1712
|
-
function openCode() {
|
|
1713
|
-
connection ??= connect();
|
|
1714
|
-
return connection;
|
|
1715
|
-
}
|
|
1716
|
-
async function connect() {
|
|
1717
|
-
try {
|
|
1718
|
-
const endpoint = await ensure();
|
|
1719
|
-
const client = OpenCode.make({
|
|
1720
|
-
baseUrl: endpoint.url,
|
|
1721
|
-
headers: headers(endpoint)
|
|
1722
|
-
});
|
|
1723
|
-
listen(client);
|
|
1724
|
-
return client;
|
|
1725
|
-
} catch (error) {
|
|
1726
|
-
connection = void 0;
|
|
1727
|
-
throw error;
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
async function listen(client) {
|
|
1731
|
-
try {
|
|
1732
|
-
for await (const event of client.event.subscribe()) route(event);
|
|
1733
|
-
console.warn("the OpenCode event stream ended");
|
|
1734
|
-
} catch (error) {
|
|
1735
|
-
console.warn(`lost the OpenCode event stream: ${error}`);
|
|
1736
|
-
}
|
|
1737
|
-
connection = void 0;
|
|
1738
|
-
if (openCodeRuns().length === 0) return;
|
|
1739
|
-
await setTimeout$1(1e3);
|
|
1740
|
-
try {
|
|
1741
|
-
const active = await (await openCode()).session.active();
|
|
1742
|
-
for (const run of openCodeRuns()) run.resync(run.sessionId in active);
|
|
1743
|
-
refreshSessions();
|
|
1744
|
-
} catch (error) {
|
|
1745
|
-
console.warn(`could not reconnect to OpenCode: ${error}`);
|
|
1746
|
-
for (const run of openCodeRuns()) run.exited();
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
function route(event) {
|
|
1750
|
-
if (listChanges.has(event.type)) refreshSessions();
|
|
1751
|
-
const sessionId = sessionOf(event);
|
|
1752
|
-
if (sessionId === null) return;
|
|
1753
|
-
const run = runs.get(sessionId);
|
|
1754
|
-
if (run?.provider === "opencode") run.receive(event);
|
|
1755
|
-
}
|
|
1756
|
-
function sessionOf(event) {
|
|
1757
|
-
if (event.type === "form.created") return event.data.form.sessionID;
|
|
1758
|
-
const data = "data" in event ? event.data : null;
|
|
1759
|
-
if (typeof data === "object" && data !== null && "sessionID" in data && typeof data.sessionID === "string") return data.sessionID;
|
|
1760
|
-
return null;
|
|
1761
|
-
}
|
|
1762
|
-
function openCodeRuns() {
|
|
1763
|
-
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
1764
|
-
}
|
|
1765
|
-
//#endregion
|
|
1766
|
-
//#region src/lib/opencode/items.ts
|
|
1767
|
-
const toolStatuses = {
|
|
1768
|
-
streaming: "running",
|
|
1769
|
-
running: "running",
|
|
1770
|
-
completed: "completed",
|
|
1771
|
-
error: "failed"
|
|
1772
|
-
};
|
|
1773
|
-
const todoStatuses$1 = {
|
|
1774
|
-
pending: "pending",
|
|
1775
|
-
in_progress: "running",
|
|
1776
|
-
completed: "completed",
|
|
1777
|
-
cancelled: "completed"
|
|
1778
|
-
};
|
|
1779
|
-
function messageItems(message, parent) {
|
|
1780
|
-
const base = {
|
|
1781
|
-
id: message.id,
|
|
1782
|
-
parent,
|
|
1783
|
-
time: message.time.created
|
|
1784
|
-
};
|
|
1785
|
-
switch (message.type) {
|
|
1786
|
-
case "user": return [{
|
|
1787
|
-
...base,
|
|
1788
|
-
kind: "user",
|
|
1789
|
-
text: message.text,
|
|
1790
|
-
images: (message.files ?? []).filter((file) => file.mime.startsWith("image/")).map((file) => ({
|
|
1791
|
-
mediaType: file.mime,
|
|
1792
|
-
data: file.data
|
|
1793
|
-
})),
|
|
1794
|
-
format: "text"
|
|
1795
|
-
}];
|
|
1796
|
-
case "skill": return [{
|
|
1797
|
-
...base,
|
|
1798
|
-
kind: "skill",
|
|
1799
|
-
skill: message.name,
|
|
1800
|
-
output: "",
|
|
1801
|
-
status: "completed"
|
|
1802
|
-
}];
|
|
1803
|
-
case "shell": return [{
|
|
1804
|
-
...base,
|
|
1805
|
-
kind: "command",
|
|
1806
|
-
command: message.command,
|
|
1807
|
-
description: "",
|
|
1808
|
-
output: message.output?.output ?? "",
|
|
1809
|
-
status: message.status === "running" ? "running" : message.status === "exited" ? message.exit === 0 ? "completed" : "failed" : "stopped",
|
|
1810
|
-
background: null
|
|
1811
|
-
}];
|
|
1812
|
-
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1813
|
-
case "compaction": return message.status === "failed" ? [{
|
|
1814
|
-
...errorItem(message.id, message.error.message),
|
|
1815
|
-
parent
|
|
1816
|
-
}] : [{
|
|
1817
|
-
...base,
|
|
1818
|
-
kind: "compaction"
|
|
1819
|
-
}];
|
|
1820
|
-
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
1821
|
-
...errorItem(message.id, message.error.message),
|
|
1822
|
-
parent
|
|
1823
|
-
}] : [];
|
|
1824
|
-
case "agent-switched":
|
|
1825
|
-
case "model-switched":
|
|
1826
|
-
case "location-switched":
|
|
1827
|
-
case "synthetic":
|
|
1828
|
-
case "system": return [];
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
function formQuestions(form) {
|
|
1832
|
-
return form.fields.flatMap((field) => field.type === "external" ? [] : [{
|
|
1833
|
-
question: questionText(field),
|
|
1834
|
-
header: field.title ?? "",
|
|
1835
|
-
multiSelect: field.type === "multiselect",
|
|
1836
|
-
options: fieldOptions(field)
|
|
1837
|
-
}]);
|
|
1838
|
-
}
|
|
1839
|
-
function formAnswer(form, answers) {
|
|
1840
|
-
return Object.fromEntries(form.fields.flatMap((field) => {
|
|
1841
|
-
if (field.type === "external") return [];
|
|
1842
|
-
const answer = answers[questionText(field)];
|
|
1843
|
-
if (answer === void 0 || answer === "") return [];
|
|
1844
|
-
switch (field.type) {
|
|
1845
|
-
case "string": return [[field.key, optionValue(field.options ?? [], answer)]];
|
|
1846
|
-
case "multiselect": return [[field.key, answer.split(", ").map((label) => optionValue(field.options, label))]];
|
|
1847
|
-
case "boolean":
|
|
1848
|
-
if (answer === "Yes") return [[field.key, true]];
|
|
1849
|
-
if (answer === "No") return [[field.key, false]];
|
|
1850
|
-
throw new Error(`${field.key} needs Yes or No`);
|
|
1851
|
-
case "number":
|
|
1852
|
-
case "integer": return [[field.key, Number(answer)]];
|
|
1853
|
-
}
|
|
1854
|
-
}));
|
|
1855
|
-
}
|
|
1856
|
-
function partItems(message, part, index, parent) {
|
|
1857
|
-
const base = {
|
|
1858
|
-
id: `${message.id}:${index}`,
|
|
1859
|
-
parent,
|
|
1860
|
-
time: message.time.created
|
|
1861
|
-
};
|
|
1862
|
-
switch (part.type) {
|
|
1863
|
-
case "text": return part.text.trim() === "" ? [] : [{
|
|
1864
|
-
...base,
|
|
1865
|
-
kind: "assistant",
|
|
1866
|
-
text: part.text,
|
|
1867
|
-
message: message.id
|
|
1868
|
-
}];
|
|
1869
|
-
case "reasoning": return part.text.trim() === "" ? [] : [{
|
|
1870
|
-
...base,
|
|
1871
|
-
kind: "reasoning",
|
|
1872
|
-
text: part.text.trim()
|
|
1873
|
-
}];
|
|
1874
|
-
case "tool": return toolItems({
|
|
1875
|
-
id: `${message.id}:${part.id}`,
|
|
1876
|
-
parent,
|
|
1877
|
-
time: part.time.created
|
|
1878
|
-
}, part);
|
|
1879
|
-
}
|
|
1659
|
+
switch (part.type) {
|
|
1660
|
+
case "text": return part.text.trim() === "" ? [] : [{
|
|
1661
|
+
...base,
|
|
1662
|
+
kind: "assistant",
|
|
1663
|
+
text: part.text,
|
|
1664
|
+
message: message.id
|
|
1665
|
+
}];
|
|
1666
|
+
case "reasoning": return part.text.trim() === "" ? [] : [{
|
|
1667
|
+
...base,
|
|
1668
|
+
kind: "reasoning",
|
|
1669
|
+
text: part.text.trim()
|
|
1670
|
+
}];
|
|
1671
|
+
case "tool": return toolItems({
|
|
1672
|
+
id: `${message.id}:${part.id}`,
|
|
1673
|
+
parent,
|
|
1674
|
+
time: part.time.created
|
|
1675
|
+
}, part);
|
|
1676
|
+
}
|
|
1880
1677
|
}
|
|
1881
1678
|
function toolItems(base, tool) {
|
|
1882
1679
|
const state = tool.state;
|
|
@@ -1888,8 +1685,8 @@ function toolItems(base, tool) {
|
|
|
1888
1685
|
case "shell": return [{
|
|
1889
1686
|
...base,
|
|
1890
1687
|
kind: "command",
|
|
1891
|
-
command: text$
|
|
1892
|
-
description: text$
|
|
1688
|
+
command: text$2(input.command),
|
|
1689
|
+
description: text$2(input.description),
|
|
1893
1690
|
output,
|
|
1894
1691
|
status,
|
|
1895
1692
|
background: null
|
|
@@ -1897,10 +1694,10 @@ function toolItems(base, tool) {
|
|
|
1897
1694
|
case "edit": return [{
|
|
1898
1695
|
...base,
|
|
1899
1696
|
kind: "file_change",
|
|
1900
|
-
path: text$
|
|
1697
|
+
path: text$2(input.path ?? input.filePath),
|
|
1901
1698
|
diffs: [{
|
|
1902
|
-
before: text$
|
|
1903
|
-
after: text$
|
|
1699
|
+
before: text$2(input.oldString),
|
|
1700
|
+
after: text$2(input.newString)
|
|
1904
1701
|
}],
|
|
1905
1702
|
output,
|
|
1906
1703
|
status
|
|
@@ -1908,10 +1705,10 @@ function toolItems(base, tool) {
|
|
|
1908
1705
|
case "write": return [{
|
|
1909
1706
|
...base,
|
|
1910
1707
|
kind: "file_change",
|
|
1911
|
-
path: text$
|
|
1708
|
+
path: text$2(input.path ?? input.filePath),
|
|
1912
1709
|
diffs: [{
|
|
1913
1710
|
before: "",
|
|
1914
|
-
after: text$
|
|
1711
|
+
after: text$2(input.content)
|
|
1915
1712
|
}],
|
|
1916
1713
|
output,
|
|
1917
1714
|
status
|
|
@@ -1923,25 +1720,25 @@ function toolItems(base, tool) {
|
|
|
1923
1720
|
...base,
|
|
1924
1721
|
id: `${base.id}:${index}`,
|
|
1925
1722
|
kind: "file_change",
|
|
1926
|
-
path: text$
|
|
1927
|
-
diffs: hunks(text$
|
|
1723
|
+
path: text$2(record(file).file),
|
|
1724
|
+
diffs: hunks(text$2(record(file).patch)),
|
|
1928
1725
|
output,
|
|
1929
1726
|
status
|
|
1930
1727
|
}));
|
|
1931
1728
|
case "todowrite": return [{
|
|
1932
1729
|
...base,
|
|
1933
1730
|
kind: "todo_list",
|
|
1934
|
-
steps: list(input.todos).map((todo) => ({
|
|
1935
|
-
text: text$
|
|
1936
|
-
status: todoStatuses$1[text$
|
|
1731
|
+
steps: list$1(input.todos).map((todo) => ({
|
|
1732
|
+
text: text$2(record(todo).content),
|
|
1733
|
+
status: todoStatuses$1[text$2(record(todo).status)] ?? "pending"
|
|
1937
1734
|
}))
|
|
1938
1735
|
}];
|
|
1939
1736
|
case "subagent":
|
|
1940
1737
|
case "task": return [{
|
|
1941
1738
|
...base,
|
|
1942
1739
|
kind: "subagent",
|
|
1943
|
-
agent: text$
|
|
1944
|
-
description: text$
|
|
1740
|
+
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
1741
|
+
description: text$2(input.description),
|
|
1945
1742
|
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
1946
1743
|
status
|
|
1947
1744
|
}];
|
|
@@ -1949,20 +1746,20 @@ function toolItems(base, tool) {
|
|
|
1949
1746
|
case "websearch": return [{
|
|
1950
1747
|
...base,
|
|
1951
1748
|
kind: "web_search",
|
|
1952
|
-
query: tool.name === "websearch" ? text$
|
|
1953
|
-
url: tool.name === "webfetch" ? text$
|
|
1749
|
+
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
1750
|
+
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
1954
1751
|
results: [],
|
|
1955
1752
|
output,
|
|
1956
1753
|
status
|
|
1957
1754
|
}];
|
|
1958
1755
|
case "question": {
|
|
1959
|
-
const questions = list(input.questions).map((item) => ({
|
|
1960
|
-
question: text$
|
|
1961
|
-
header: text$
|
|
1756
|
+
const questions = list$1(input.questions).map((item) => ({
|
|
1757
|
+
question: text$2(record(item).question),
|
|
1758
|
+
header: text$2(record(item).header),
|
|
1962
1759
|
multiSelect: record(item).multiple === true,
|
|
1963
|
-
options: list(record(item).options).map((option) => ({
|
|
1964
|
-
label: text$
|
|
1965
|
-
description: text$
|
|
1760
|
+
options: list$1(record(item).options).map((option) => ({
|
|
1761
|
+
label: text$2(record(option).label),
|
|
1762
|
+
description: text$2(record(option).description)
|
|
1966
1763
|
}))
|
|
1967
1764
|
}));
|
|
1968
1765
|
const answers = metadata.answers;
|
|
@@ -1970,14 +1767,14 @@ function toolItems(base, tool) {
|
|
|
1970
1767
|
...base,
|
|
1971
1768
|
kind: "question",
|
|
1972
1769
|
questions,
|
|
1973
|
-
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list(answers[index]).map(text$
|
|
1770
|
+
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list$1(answers[index]).map(text$2).join(", ")])) : null,
|
|
1974
1771
|
status
|
|
1975
1772
|
}];
|
|
1976
1773
|
}
|
|
1977
1774
|
case "skill": return [{
|
|
1978
1775
|
...base,
|
|
1979
1776
|
kind: "skill",
|
|
1980
|
-
skill: text$
|
|
1777
|
+
skill: text$2(input.name),
|
|
1981
1778
|
output,
|
|
1982
1779
|
status
|
|
1983
1780
|
}];
|
|
@@ -2018,34 +1815,20 @@ function optionValue(options, label) {
|
|
|
2018
1815
|
function contentText(content) {
|
|
2019
1816
|
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
2020
1817
|
}
|
|
2021
|
-
function text$
|
|
1818
|
+
function text$2(value) {
|
|
2022
1819
|
return typeof value === "string" ? value : "";
|
|
2023
1820
|
}
|
|
2024
|
-
function list(value) {
|
|
2025
|
-
return Array.isArray(value) ? value : [];
|
|
2026
|
-
}
|
|
2027
|
-
function record(value) {
|
|
2028
|
-
return
|
|
2029
|
-
}
|
|
2030
|
-
//#endregion
|
|
2031
|
-
//#region src/lib/opencode/sessions.ts
|
|
2032
|
-
const PAGE = 100;
|
|
2033
|
-
const COMMIT_LIMIT = 12e4;
|
|
2034
|
-
const temporary = /* @__PURE__ */ new Set();
|
|
2035
|
-
function openCodeAccount() {
|
|
2036
|
-
return {
|
|
2037
|
-
id: "opencode",
|
|
2038
|
-
provider: "opencode",
|
|
2039
|
-
name: "OpenCode",
|
|
2040
|
-
configDir: "",
|
|
2041
|
-
email: null,
|
|
2042
|
-
plan: null,
|
|
2043
|
-
error: null,
|
|
2044
|
-
limits: [],
|
|
2045
|
-
linked: true,
|
|
2046
|
-
valid: true
|
|
2047
|
-
};
|
|
1821
|
+
function list$1(value) {
|
|
1822
|
+
return Array.isArray(value) ? value : [];
|
|
1823
|
+
}
|
|
1824
|
+
function record(value) {
|
|
1825
|
+
return isRecord(value) ? value : {};
|
|
2048
1826
|
}
|
|
1827
|
+
//#endregion
|
|
1828
|
+
//#region src/lib/opencode/sessions.ts
|
|
1829
|
+
const PAGE = 100;
|
|
1830
|
+
const COMMIT_LIMIT = 12e4;
|
|
1831
|
+
const temporary = /* @__PURE__ */ new Set();
|
|
2049
1832
|
function openCodeInstalled() {
|
|
2050
1833
|
return findOpenCode() !== null;
|
|
2051
1834
|
}
|
|
@@ -2065,23 +1848,15 @@ async function openCodeCapabilities(cwd) {
|
|
|
2065
1848
|
description: model.providerID,
|
|
2066
1849
|
efforts: model.variants.map((variant) => variant.id)
|
|
2067
1850
|
})),
|
|
2068
|
-
commands: [
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
hint: ""
|
|
2078
|
-
})),
|
|
2079
|
-
...skills.data.map((skill) => ({
|
|
2080
|
-
name: skill.name,
|
|
2081
|
-
description: skill.description ?? "",
|
|
2082
|
-
hint: ""
|
|
2083
|
-
}))
|
|
2084
|
-
],
|
|
1851
|
+
commands: [{
|
|
1852
|
+
name: "compact",
|
|
1853
|
+
description: "Summarize the conversation to free up context",
|
|
1854
|
+
hint: ""
|
|
1855
|
+
}, ...[...commands.data, ...skills.data].map((command) => ({
|
|
1856
|
+
name: command.name,
|
|
1857
|
+
description: command.description ?? "",
|
|
1858
|
+
hint: ""
|
|
1859
|
+
}))],
|
|
2085
1860
|
agents: agents.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden).map((agent) => ({
|
|
2086
1861
|
id: agent.id,
|
|
2087
1862
|
name: agent.name,
|
|
@@ -2158,8 +1933,8 @@ async function rewindOpenCode(request) {
|
|
|
2158
1933
|
})).files ?? [];
|
|
2159
1934
|
const result = request.code ? {
|
|
2160
1935
|
files: files.map((file) => file.file),
|
|
2161
|
-
insertions: files
|
|
2162
|
-
deletions: files
|
|
1936
|
+
insertions: sumBy(files, (file) => file.additions),
|
|
1937
|
+
deletions: sumBy(files, (file) => file.deletions)
|
|
2163
1938
|
} : null;
|
|
2164
1939
|
if (request.dryRun) await client.session.revert.clear({ sessionID });
|
|
2165
1940
|
else await client.session.revert.commit({ sessionID });
|
|
@@ -2256,28 +2031,24 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2256
2031
|
prompted = false;
|
|
2257
2032
|
backgroundTasks = 0;
|
|
2258
2033
|
tasks = {};
|
|
2259
|
-
client;
|
|
2260
2034
|
close;
|
|
2261
2035
|
applied;
|
|
2262
2036
|
loading = Promise.resolve();
|
|
2263
2037
|
refreshTimer;
|
|
2264
2038
|
idleTimer;
|
|
2265
2039
|
static async start(cwd, settings) {
|
|
2266
|
-
const
|
|
2267
|
-
const session = await client.session.create({
|
|
2040
|
+
const session = await (await openCode()).session.create({
|
|
2268
2041
|
location: { directory: cwd },
|
|
2269
2042
|
...settings.mode === null ? {} : { agent: settings.mode },
|
|
2270
2043
|
...settings.model === null ? {} : { model: modelRef(settings.model, settings.effort) }
|
|
2271
2044
|
});
|
|
2272
|
-
return new OpenCodeRun(
|
|
2045
|
+
return new OpenCodeRun(session.id, cwd, settings, settings);
|
|
2273
2046
|
}
|
|
2274
2047
|
static async resume(sessionId, cwd, settings) {
|
|
2275
|
-
const
|
|
2276
|
-
|
|
2277
|
-
return new OpenCodeRun(client, sessionId, cwd, settings, sessionSettings(session, settings));
|
|
2048
|
+
const session = await (await openCode()).session.get({ sessionID: sessionId });
|
|
2049
|
+
return new OpenCodeRun(sessionId, cwd, settings, sessionSettings(session, settings));
|
|
2278
2050
|
}
|
|
2279
|
-
constructor(
|
|
2280
|
-
this.client = client;
|
|
2051
|
+
constructor(sessionId, cwd, settings, applied) {
|
|
2281
2052
|
this.sessionId = sessionId;
|
|
2282
2053
|
this.cwd = cwd;
|
|
2283
2054
|
this.settings = settings;
|
|
@@ -2293,322 +2064,843 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2293
2064
|
await this.startTurn(prompt);
|
|
2294
2065
|
return;
|
|
2295
2066
|
}
|
|
2296
|
-
switch (delivery) {
|
|
2297
|
-
case "steer":
|
|
2298
|
-
await
|
|
2299
|
-
sessionID: this.sessionId,
|
|
2300
|
-
text: prompt.text,
|
|
2301
|
-
files: files(prompt),
|
|
2302
|
-
delivery: "steer"
|
|
2303
|
-
});
|
|
2304
|
-
return;
|
|
2305
|
-
case "queue":
|
|
2306
|
-
this.queue = [...this.queue, {
|
|
2307
|
-
id: randomUUID(),
|
|
2308
|
-
prompt
|
|
2309
|
-
}];
|
|
2310
|
-
this.publishQueue();
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
unqueue(id) {
|
|
2314
|
-
this.queue = this.queue.filter((item) => item.id !== id);
|
|
2315
|
-
this.publishQueue();
|
|
2067
|
+
switch (delivery) {
|
|
2068
|
+
case "steer":
|
|
2069
|
+
await (await openCode()).session.prompt({
|
|
2070
|
+
sessionID: this.sessionId,
|
|
2071
|
+
text: prompt.text,
|
|
2072
|
+
files: files(prompt),
|
|
2073
|
+
delivery: "steer"
|
|
2074
|
+
});
|
|
2075
|
+
return;
|
|
2076
|
+
case "queue":
|
|
2077
|
+
this.queue = [...this.queue, {
|
|
2078
|
+
id: randomUUID(),
|
|
2079
|
+
prompt
|
|
2080
|
+
}];
|
|
2081
|
+
this.publishQueue();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
unqueue(id) {
|
|
2085
|
+
this.queue = this.queue.filter((item) => item.id !== id);
|
|
2086
|
+
this.publishQueue();
|
|
2087
|
+
}
|
|
2088
|
+
steer(id) {
|
|
2089
|
+
const item = this.queue.find((queued) => queued.id === id);
|
|
2090
|
+
if (item === void 0) return;
|
|
2091
|
+
this.unqueue(id);
|
|
2092
|
+
this.submit(item.prompt, "steer").catch((error) => this.fail(error));
|
|
2093
|
+
}
|
|
2094
|
+
configure(settings) {
|
|
2095
|
+
this.settings = {
|
|
2096
|
+
...settings,
|
|
2097
|
+
accountId: this.settings.accountId
|
|
2098
|
+
};
|
|
2099
|
+
publish$2(this.sessionId, {
|
|
2100
|
+
type: "settings",
|
|
2101
|
+
sessionId: this.sessionId,
|
|
2102
|
+
settings: this.settings
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
async interrupt() {
|
|
2106
|
+
await (await openCode()).session.interrupt({ sessionID: this.sessionId });
|
|
2107
|
+
}
|
|
2108
|
+
stopTask() {
|
|
2109
|
+
throw new Error("OpenCode has no background tasks to stop");
|
|
2110
|
+
}
|
|
2111
|
+
async respond(requestId, decision, answers) {
|
|
2112
|
+
const pending = this.pending.get(requestId);
|
|
2113
|
+
if (pending === void 0) return;
|
|
2114
|
+
const sessionID = this.sessionId;
|
|
2115
|
+
const client = await openCode();
|
|
2116
|
+
if (pending.kind === "permission") await client.permission.reply({
|
|
2117
|
+
sessionID,
|
|
2118
|
+
requestID: requestId,
|
|
2119
|
+
decision: replies[decision]
|
|
2120
|
+
});
|
|
2121
|
+
else if (decision === "deny") await client.session.form.cancel({
|
|
2122
|
+
sessionID,
|
|
2123
|
+
formID: requestId
|
|
2124
|
+
});
|
|
2125
|
+
else await client.session.form.reply({
|
|
2126
|
+
sessionID,
|
|
2127
|
+
formID: requestId,
|
|
2128
|
+
answer: formAnswer(pending.form, answers ?? {})
|
|
2129
|
+
});
|
|
2130
|
+
this.settle(requestId);
|
|
2131
|
+
}
|
|
2132
|
+
stop() {
|
|
2133
|
+
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
2134
|
+
this.end();
|
|
2135
|
+
}
|
|
2136
|
+
exited() {
|
|
2137
|
+
this.put([errorItem(randomUUID(), this.pending.size === 0 ? "Lost the connection to OpenCode" : "Lost the connection to OpenCode, its open requests expired")]);
|
|
2138
|
+
this.stop();
|
|
2139
|
+
}
|
|
2140
|
+
reconcile(client, active, permissions, forms) {
|
|
2141
|
+
this.refresh(client);
|
|
2142
|
+
const open = /* @__PURE__ */ new Set([...permissions.map((permission) => permission.id), ...forms.map((form) => form.id)]);
|
|
2143
|
+
for (const requestId of [...this.pending.keys()]) if (!open.has(requestId)) this.settle(requestId);
|
|
2144
|
+
for (const permission of permissions) if (!this.pending.has(permission.id)) this.ask(permission);
|
|
2145
|
+
for (const form of forms) if (!this.pending.has(form.id)) this.askForm(form);
|
|
2146
|
+
if (this.running && !active) this.finishTurn();
|
|
2147
|
+
}
|
|
2148
|
+
receive(event) {
|
|
2149
|
+
switch (event.type) {
|
|
2150
|
+
case "session.reasoning.delta":
|
|
2151
|
+
case "session.tool.input.delta":
|
|
2152
|
+
case "session.compaction.delta": return;
|
|
2153
|
+
case "session.execution.started":
|
|
2154
|
+
clearTimeout(this.idleTimer);
|
|
2155
|
+
if (!this.running) this.prompted = false;
|
|
2156
|
+
this.running = true;
|
|
2157
|
+
broadcastActivity();
|
|
2158
|
+
return;
|
|
2159
|
+
case "session.execution.succeeded":
|
|
2160
|
+
case "session.execution.failed":
|
|
2161
|
+
case "session.execution.interrupted":
|
|
2162
|
+
this.refresh();
|
|
2163
|
+
this.loading.then(() => this.finishTurn());
|
|
2164
|
+
return;
|
|
2165
|
+
case "session.text.delta":
|
|
2166
|
+
this.partial += event.data.delta;
|
|
2167
|
+
publish$2(this.sessionId, {
|
|
2168
|
+
type: "delta",
|
|
2169
|
+
sessionId: this.sessionId,
|
|
2170
|
+
text: event.data.delta
|
|
2171
|
+
});
|
|
2172
|
+
return;
|
|
2173
|
+
case "permission.asked":
|
|
2174
|
+
this.refresh();
|
|
2175
|
+
this.loading.then(() => this.ask(event.data));
|
|
2176
|
+
return;
|
|
2177
|
+
case "permission.replied":
|
|
2178
|
+
this.settle(event.data.requestID);
|
|
2179
|
+
return;
|
|
2180
|
+
case "form.created":
|
|
2181
|
+
this.askForm(event.data.form);
|
|
2182
|
+
return;
|
|
2183
|
+
case "form.replied":
|
|
2184
|
+
case "form.cancelled":
|
|
2185
|
+
this.settle(event.data.id);
|
|
2186
|
+
return;
|
|
2187
|
+
default:
|
|
2188
|
+
clearTimeout(this.refreshTimer);
|
|
2189
|
+
this.refreshTimer = setTimeout(() => this.refresh(), 100);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
refresh(client) {
|
|
2193
|
+
clearTimeout(this.refreshTimer);
|
|
2194
|
+
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => console.warn(`could not load OpenCode messages: ${error}`));
|
|
2195
|
+
}
|
|
2196
|
+
async load(client) {
|
|
2197
|
+
const messages = (await client.message.list({
|
|
2198
|
+
sessionID: this.sessionId,
|
|
2199
|
+
limit: RECENT,
|
|
2200
|
+
order: "desc"
|
|
2201
|
+
})).data.toReversed();
|
|
2202
|
+
this.put(messages.flatMap((message) => messageItems(message, null)).filter((item) => JSON.stringify(this.items.get(item.id)) !== JSON.stringify(item)));
|
|
2203
|
+
const context = await contextOf(client, this.cwd, messages);
|
|
2204
|
+
if (context === null || context.tokens === this.context?.tokens) return;
|
|
2205
|
+
this.context = context;
|
|
2206
|
+
publish$2(this.sessionId, {
|
|
2207
|
+
type: "context",
|
|
2208
|
+
sessionId: this.sessionId,
|
|
2209
|
+
context
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
async startTurn(prompt) {
|
|
2213
|
+
clearTimeout(this.idleTimer);
|
|
2214
|
+
this.running = true;
|
|
2215
|
+
this.prompted = true;
|
|
2216
|
+
broadcastActivity();
|
|
2217
|
+
try {
|
|
2218
|
+
await this.apply();
|
|
2219
|
+
await this.send(prompt);
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
this.running = false;
|
|
2222
|
+
broadcastActivity();
|
|
2223
|
+
this.armIdleTimer();
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
async send(prompt) {
|
|
2228
|
+
const client = await openCode();
|
|
2229
|
+
const sessionID = this.sessionId;
|
|
2230
|
+
const location = { directory: this.cwd };
|
|
2231
|
+
const match = /^\/(\S+)\s*([\s\S]*)$/.exec(prompt.text);
|
|
2232
|
+
if (match !== null) {
|
|
2233
|
+
if (match[1] === "compact" && prompt.images.length === 0) {
|
|
2234
|
+
await client.session.compact({ sessionID });
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
if ((await client.command.list({ location })).data.some((command) => command.name === match[1])) {
|
|
2238
|
+
await client.session.command({
|
|
2239
|
+
sessionID,
|
|
2240
|
+
name: match[1],
|
|
2241
|
+
text: match[2],
|
|
2242
|
+
files: files(prompt)
|
|
2243
|
+
});
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const skill = (await client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
2247
|
+
if (skill !== void 0) {
|
|
2248
|
+
await client.session.prompt({
|
|
2249
|
+
sessionID,
|
|
2250
|
+
text: match[2],
|
|
2251
|
+
files: files(prompt),
|
|
2252
|
+
skills: [{ id: skill.id }]
|
|
2253
|
+
});
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
await client.session.prompt({
|
|
2258
|
+
sessionID,
|
|
2259
|
+
text: prompt.text,
|
|
2260
|
+
files: files(prompt)
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
async apply() {
|
|
2264
|
+
const client = await openCode();
|
|
2265
|
+
const settings = this.settings;
|
|
2266
|
+
const model = settings.model ?? this.applied.model;
|
|
2267
|
+
if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await client.session.switchModel({
|
|
2268
|
+
sessionID: this.sessionId,
|
|
2269
|
+
model: modelRef(model, settings.effort)
|
|
2270
|
+
});
|
|
2271
|
+
if (settings.mode !== null && settings.mode !== this.applied.mode) await client.session.switchAgent({
|
|
2272
|
+
sessionID: this.sessionId,
|
|
2273
|
+
agent: settings.mode
|
|
2274
|
+
});
|
|
2275
|
+
this.applied = {
|
|
2276
|
+
...settings,
|
|
2277
|
+
model,
|
|
2278
|
+
mode: settings.mode ?? this.applied.mode
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
ask(permission) {
|
|
2282
|
+
const source = permission.source;
|
|
2283
|
+
const item = source === void 0 ? void 0 : this.items.get(`${source.messageID}:${source.id}`);
|
|
2284
|
+
this.addPending({
|
|
2285
|
+
kind: "permission",
|
|
2286
|
+
request: {
|
|
2287
|
+
id: permission.id,
|
|
2288
|
+
kind: "approval",
|
|
2289
|
+
title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
|
|
2290
|
+
canAlwaysAllow: (permission.save ?? []).length > 0,
|
|
2291
|
+
item: item ?? {
|
|
2292
|
+
id: permission.id,
|
|
2293
|
+
parent: null,
|
|
2294
|
+
time: Date.now(),
|
|
2295
|
+
kind: "tool",
|
|
2296
|
+
name: permission.action,
|
|
2297
|
+
input: { resources: permission.resources },
|
|
2298
|
+
output: "",
|
|
2299
|
+
status: "running"
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
askForm(form) {
|
|
2305
|
+
this.addPending({
|
|
2306
|
+
kind: "form",
|
|
2307
|
+
request: {
|
|
2308
|
+
id: form.id,
|
|
2309
|
+
kind: "question",
|
|
2310
|
+
questions: formQuestions(form)
|
|
2311
|
+
},
|
|
2312
|
+
form
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
addPending(pending) {
|
|
2316
|
+
this.pending.set(pending.request.id, pending);
|
|
2317
|
+
publish$2(this.sessionId, {
|
|
2318
|
+
type: "request",
|
|
2319
|
+
sessionId: this.sessionId,
|
|
2320
|
+
request: pending.request
|
|
2321
|
+
});
|
|
2322
|
+
broadcastActivity();
|
|
2323
|
+
scheduleAutoResolve(this.sessionId, pending.request);
|
|
2324
|
+
}
|
|
2325
|
+
finishTurn() {
|
|
2326
|
+
this.partial = "";
|
|
2327
|
+
const [next, ...rest] = this.queue;
|
|
2328
|
+
if (next !== void 0) {
|
|
2329
|
+
this.queue = rest;
|
|
2330
|
+
this.publishQueue();
|
|
2331
|
+
this.startTurn(next.prompt).catch((error) => this.fail(error));
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
this.running = false;
|
|
2335
|
+
broadcastActivity();
|
|
2336
|
+
this.armIdleTimer();
|
|
2316
2337
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2338
|
+
settle(requestId) {
|
|
2339
|
+
if (!this.pending.delete(requestId)) return;
|
|
2340
|
+
publish$2(this.sessionId, {
|
|
2341
|
+
type: "requestDone",
|
|
2342
|
+
sessionId: this.sessionId,
|
|
2343
|
+
requestId
|
|
2344
|
+
});
|
|
2345
|
+
broadcastActivity();
|
|
2322
2346
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
};
|
|
2347
|
+
put(items) {
|
|
2348
|
+
if (items.length === 0) return;
|
|
2349
|
+
for (const item of items) this.items.set(item.id, item);
|
|
2350
|
+
if (items.some((item) => item.kind === "assistant")) this.partial = "";
|
|
2328
2351
|
publish$2(this.sessionId, {
|
|
2329
|
-
type: "
|
|
2352
|
+
type: "items",
|
|
2330
2353
|
sessionId: this.sessionId,
|
|
2331
|
-
|
|
2354
|
+
items
|
|
2332
2355
|
});
|
|
2333
2356
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2357
|
+
fail(error) {
|
|
2358
|
+
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2336
2359
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2360
|
+
armIdleTimer() {
|
|
2361
|
+
clearTimeout(this.idleTimer);
|
|
2362
|
+
if (this.running) return;
|
|
2363
|
+
this.idleTimer = setTimeout(() => {
|
|
2364
|
+
if (!this.running && this.pending.size === 0) this.stop();
|
|
2365
|
+
}, IDLE_LIMIT$1);
|
|
2339
2366
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
const
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
else await this.client.session.form.reply({
|
|
2354
|
-
sessionID,
|
|
2355
|
-
formID: requestId,
|
|
2356
|
-
answer: formAnswer(pending.form, answers ?? {})
|
|
2367
|
+
end() {
|
|
2368
|
+
clearTimeout(this.idleTimer);
|
|
2369
|
+
clearTimeout(this.refreshTimer);
|
|
2370
|
+
for (const requestId of [...this.pending.keys()]) this.settle(requestId);
|
|
2371
|
+
this.running = false;
|
|
2372
|
+
broadcastActivity();
|
|
2373
|
+
this.close();
|
|
2374
|
+
}
|
|
2375
|
+
publishQueue() {
|
|
2376
|
+
publish$2(this.sessionId, {
|
|
2377
|
+
type: "queue",
|
|
2378
|
+
sessionId: this.sessionId,
|
|
2379
|
+
queue: this.queue
|
|
2357
2380
|
});
|
|
2358
|
-
this.settle(requestId);
|
|
2359
2381
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2382
|
+
};
|
|
2383
|
+
function sessionSettings(session, settings) {
|
|
2384
|
+
const variant = session.model?.variant;
|
|
2385
|
+
return {
|
|
2386
|
+
...settings,
|
|
2387
|
+
model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
|
|
2388
|
+
effort: variant === void 0 || variant === "default" ? null : variant,
|
|
2389
|
+
mode: session.agent ?? null
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
function files(prompt) {
|
|
2393
|
+
return prompt.images.map((image) => ({ uri: `data:${image.mediaType};base64,${image.data}` }));
|
|
2394
|
+
}
|
|
2395
|
+
//#endregion
|
|
2396
|
+
//#region src/storage/db.ts
|
|
2397
|
+
const file = path.join(home, "offsite.db");
|
|
2398
|
+
closeSync(openSync(file, "a", 384));
|
|
2399
|
+
chmodSync(file, 384);
|
|
2400
|
+
const client = new DatabaseSync(file);
|
|
2401
|
+
client.exec(`
|
|
2402
|
+
PRAGMA journal_mode = WAL;
|
|
2403
|
+
PRAGMA busy_timeout = 5000;
|
|
2404
|
+
PRAGMA synchronous = NORMAL;
|
|
2405
|
+
PRAGMA foreign_keys = ON;
|
|
2406
|
+
`);
|
|
2407
|
+
const db = drizzle({ client });
|
|
2408
|
+
migrate(db, { migrationsFolder: path.join(serverDir, "drizzle") });
|
|
2409
|
+
//#endregion
|
|
2410
|
+
//#region src/storage/schema.ts
|
|
2411
|
+
const projects$2 = snakeCase.table("projects", {
|
|
2412
|
+
id: text().primaryKey(),
|
|
2413
|
+
path: text().notNull().unique(),
|
|
2414
|
+
name: text().notNull(),
|
|
2415
|
+
logo: text()
|
|
2416
|
+
});
|
|
2417
|
+
const sessions$1 = snakeCase.table("sessions", {
|
|
2418
|
+
sessionId: text().primaryKey(),
|
|
2419
|
+
interactedAt: integer(),
|
|
2420
|
+
status: text({ enum: [
|
|
2421
|
+
"active",
|
|
2422
|
+
"pinned",
|
|
2423
|
+
"minimized"
|
|
2424
|
+
] }).notNull()
|
|
2425
|
+
});
|
|
2426
|
+
const tokens = snakeCase.table("tokens", {
|
|
2427
|
+
hash: text().primaryKey(),
|
|
2428
|
+
name: text(),
|
|
2429
|
+
createdAt: integer().notNull(),
|
|
2430
|
+
lastUsedAt: integer()
|
|
2431
|
+
});
|
|
2432
|
+
const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
2433
|
+
endpoint: text().primaryKey(),
|
|
2434
|
+
p256dh: text().notNull(),
|
|
2435
|
+
auth: text().notNull(),
|
|
2436
|
+
publicKey: text().notNull(),
|
|
2437
|
+
privateKey: text().notNull(),
|
|
2438
|
+
machine: text().notNull(),
|
|
2439
|
+
server: text().notNull()
|
|
2440
|
+
});
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/services/projects.ts
|
|
2443
|
+
const LOGO_TYPES = [
|
|
2444
|
+
"image/png",
|
|
2445
|
+
"image/jpeg",
|
|
2446
|
+
"image/gif",
|
|
2447
|
+
"image/webp",
|
|
2448
|
+
"image/svg+xml",
|
|
2449
|
+
"image/x-icon",
|
|
2450
|
+
"image/vnd.microsoft.icon"
|
|
2451
|
+
];
|
|
2452
|
+
const LOGO_LIMIT = 524288;
|
|
2453
|
+
function projects$1() {
|
|
2454
|
+
return db.select().from(projects$2).all();
|
|
2455
|
+
}
|
|
2456
|
+
function projectPath(id) {
|
|
2457
|
+
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
2458
|
+
if (project === void 0) throw new Error("That project was removed");
|
|
2459
|
+
return project.path;
|
|
2460
|
+
}
|
|
2461
|
+
async function addProject(input) {
|
|
2462
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2463
|
+
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
2464
|
+
db.insert(projects$2).values({
|
|
2465
|
+
id: randomUUID(),
|
|
2466
|
+
path: folder,
|
|
2467
|
+
name: path.basename(folder) || folder,
|
|
2468
|
+
logo: null
|
|
2469
|
+
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
2470
|
+
publish$1();
|
|
2471
|
+
const project = projects$1().find((item) => item.path === folder);
|
|
2472
|
+
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
2473
|
+
return project;
|
|
2474
|
+
}
|
|
2475
|
+
function updateProject(id, patch) {
|
|
2476
|
+
if (patch.name !== void 0) {
|
|
2477
|
+
const name = patch.name.trim();
|
|
2478
|
+
if (name === "") throw new Error("Project name cannot be empty");
|
|
2479
|
+
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
2363
2480
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2481
|
+
if (patch.logo !== void 0) {
|
|
2482
|
+
if (patch.logo !== null) checkLogo(patch.logo);
|
|
2483
|
+
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
2367
2484
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2485
|
+
publish$1();
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
function removeProject(id) {
|
|
2489
|
+
db.delete(projects$2).where(eq(projects$2.id, id)).run();
|
|
2490
|
+
publish$1();
|
|
2491
|
+
}
|
|
2492
|
+
async function directories(input) {
|
|
2493
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2494
|
+
const entries = await readdir(folder, { withFileTypes: true });
|
|
2495
|
+
const parent = path.dirname(folder);
|
|
2496
|
+
return {
|
|
2497
|
+
path: folder,
|
|
2498
|
+
parent: parent === folder ? null : parent,
|
|
2499
|
+
directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
2500
|
+
name: entry.name,
|
|
2501
|
+
path: path.join(folder, entry.name)
|
|
2502
|
+
})).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function publish$1() {
|
|
2506
|
+
broadcast({
|
|
2507
|
+
type: "projects",
|
|
2508
|
+
projects: projects$1()
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
function checkLogo(logo) {
|
|
2512
|
+
const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
|
|
2513
|
+
if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
|
|
2514
|
+
if (logo.length > LOGO_LIMIT) throw new Error("Use an image smaller than 384 KB");
|
|
2515
|
+
}
|
|
2516
|
+
function expandHome(input) {
|
|
2517
|
+
if (input === "~") return os.homedir();
|
|
2518
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
2519
|
+
return input;
|
|
2520
|
+
}
|
|
2521
|
+
//#endregion
|
|
2522
|
+
//#region src/services/sessions.ts
|
|
2523
|
+
const day = 864e5;
|
|
2524
|
+
const LIST_TIMEOUT = 1e4;
|
|
2525
|
+
const REFRESH_DELAY = 1e3;
|
|
2526
|
+
const allProviders = [
|
|
2527
|
+
"claude",
|
|
2528
|
+
"codex",
|
|
2529
|
+
"opencode"
|
|
2530
|
+
];
|
|
2531
|
+
const listed = {
|
|
2532
|
+
claude: [],
|
|
2533
|
+
codex: [],
|
|
2534
|
+
opencode: []
|
|
2535
|
+
};
|
|
2536
|
+
const statuses = {
|
|
2537
|
+
claude: {
|
|
2538
|
+
provider: "claude",
|
|
2539
|
+
state: "loading",
|
|
2540
|
+
error: null
|
|
2541
|
+
},
|
|
2542
|
+
codex: {
|
|
2543
|
+
provider: "codex",
|
|
2544
|
+
state: "loading",
|
|
2545
|
+
error: null
|
|
2546
|
+
},
|
|
2547
|
+
opencode: {
|
|
2548
|
+
provider: "opencode",
|
|
2549
|
+
state: "loading",
|
|
2550
|
+
error: null
|
|
2371
2551
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
},
|
|
2412
|
-
form: event.data.form
|
|
2413
|
-
});
|
|
2414
|
-
return;
|
|
2415
|
-
case "form.replied":
|
|
2416
|
-
case "form.cancelled":
|
|
2417
|
-
this.settle(event.data.id);
|
|
2418
|
-
return;
|
|
2419
|
-
default:
|
|
2420
|
-
clearTimeout(this.refreshTimer);
|
|
2421
|
-
this.refreshTimer = setTimeout(() => this.refresh(), 100);
|
|
2552
|
+
};
|
|
2553
|
+
const refreshing = /* @__PURE__ */ new Map();
|
|
2554
|
+
const timers = /* @__PURE__ */ new Map();
|
|
2555
|
+
const providers = /* @__PURE__ */ new Map();
|
|
2556
|
+
let watchers = [];
|
|
2557
|
+
function sessions() {
|
|
2558
|
+
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
2559
|
+
const known = new Set(projects$1().map((project) => project.id));
|
|
2560
|
+
return allProviders.flatMap((provider) => listed[provider].filter((item) => known.has(item.projectId)).map((item) => {
|
|
2561
|
+
const record = records.get(item.id);
|
|
2562
|
+
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
2563
|
+
return {
|
|
2564
|
+
...item,
|
|
2565
|
+
updatedAt,
|
|
2566
|
+
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
2567
|
+
};
|
|
2568
|
+
}));
|
|
2569
|
+
}
|
|
2570
|
+
function providerStatuses() {
|
|
2571
|
+
return allProviders.map((provider) => statuses[provider]);
|
|
2572
|
+
}
|
|
2573
|
+
function listedUpdatedAt(provider, sessionId) {
|
|
2574
|
+
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
2575
|
+
}
|
|
2576
|
+
function providerOf(sessionId) {
|
|
2577
|
+
const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
2578
|
+
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
2579
|
+
return provider;
|
|
2580
|
+
}
|
|
2581
|
+
function recordInteraction(id) {
|
|
2582
|
+
db.insert(sessions$1).values({
|
|
2583
|
+
sessionId: id,
|
|
2584
|
+
interactedAt: Date.now(),
|
|
2585
|
+
status: "active"
|
|
2586
|
+
}).onConflictDoUpdate({
|
|
2587
|
+
target: sessions$1.sessionId,
|
|
2588
|
+
set: {
|
|
2589
|
+
interactedAt: Date.now(),
|
|
2590
|
+
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
2422
2591
|
}
|
|
2592
|
+
}).run();
|
|
2593
|
+
}
|
|
2594
|
+
function publishSessions() {
|
|
2595
|
+
broadcast({
|
|
2596
|
+
type: "sessions",
|
|
2597
|
+
sessions: sessions()
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
function refreshAll() {
|
|
2601
|
+
for (const provider of allProviders) refresh(provider);
|
|
2602
|
+
}
|
|
2603
|
+
function refreshSessions(provider) {
|
|
2604
|
+
if (timers.has(provider)) return;
|
|
2605
|
+
timers.set(provider, setTimeout(() => {
|
|
2606
|
+
timers.delete(provider);
|
|
2607
|
+
refresh(provider);
|
|
2608
|
+
}, REFRESH_DELAY));
|
|
2609
|
+
}
|
|
2610
|
+
async function refresh(provider) {
|
|
2611
|
+
const current = refreshing.get(provider);
|
|
2612
|
+
if (current !== void 0) {
|
|
2613
|
+
current.again = true;
|
|
2614
|
+
return;
|
|
2423
2615
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
const messages = (await this.client.message.list({
|
|
2430
|
-
sessionID: this.sessionId,
|
|
2431
|
-
limit: RECENT,
|
|
2432
|
-
order: "desc"
|
|
2433
|
-
})).data.toReversed();
|
|
2434
|
-
this.put(messages.flatMap((message) => messageItems(message, null)).filter((item) => JSON.stringify(this.items.get(item.id)) !== JSON.stringify(item)));
|
|
2435
|
-
const context = await contextOf(this.client, this.cwd, messages);
|
|
2436
|
-
if (context === null || context.tokens === this.context?.tokens) return;
|
|
2437
|
-
this.context = context;
|
|
2438
|
-
publish$2(this.sessionId, {
|
|
2439
|
-
type: "context",
|
|
2440
|
-
sessionId: this.sessionId,
|
|
2441
|
-
context
|
|
2442
|
-
});
|
|
2616
|
+
const entry = { again: true };
|
|
2617
|
+
refreshing.set(provider, entry);
|
|
2618
|
+
while (entry.again) {
|
|
2619
|
+
entry.again = false;
|
|
2620
|
+
await list(provider);
|
|
2443
2621
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2622
|
+
refreshing.delete(provider);
|
|
2623
|
+
}
|
|
2624
|
+
async function list(provider) {
|
|
2625
|
+
const paths = new Map(projects$1().map((project) => [project.path, project.id]));
|
|
2626
|
+
const before = statuses[provider];
|
|
2627
|
+
let timer;
|
|
2628
|
+
try {
|
|
2629
|
+
if (!readConfig().enabledProviders[provider]) {
|
|
2630
|
+
listed[provider] = [];
|
|
2631
|
+
statuses[provider] = {
|
|
2632
|
+
provider,
|
|
2633
|
+
state: "disabled",
|
|
2634
|
+
error: null
|
|
2635
|
+
};
|
|
2636
|
+
return;
|
|
2457
2637
|
}
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
}
|
|
2477
|
-
const skill = (await this.client.skill.list({ location })).data.find((item) => item.name === match[1]);
|
|
2478
|
-
if (skill !== void 0) {
|
|
2479
|
-
await this.client.session.prompt({
|
|
2480
|
-
sessionID,
|
|
2481
|
-
text: match[2],
|
|
2482
|
-
files: files(prompt),
|
|
2483
|
-
skills: [{ id: skill.id }]
|
|
2484
|
-
});
|
|
2485
|
-
return;
|
|
2486
|
-
}
|
|
2638
|
+
const found = await Promise.race([find(provider, paths), new Promise((_, reject) => {
|
|
2639
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
|
|
2640
|
+
})]);
|
|
2641
|
+
if (found === null) {
|
|
2642
|
+
listed[provider] = [];
|
|
2643
|
+
statuses[provider] = {
|
|
2644
|
+
provider,
|
|
2645
|
+
state: "missing",
|
|
2646
|
+
error: null
|
|
2647
|
+
};
|
|
2648
|
+
} else {
|
|
2649
|
+
listed[provider] = found;
|
|
2650
|
+
statuses[provider] = {
|
|
2651
|
+
provider,
|
|
2652
|
+
state: "ready",
|
|
2653
|
+
error: null
|
|
2654
|
+
};
|
|
2655
|
+
for (const item of found) providers.set(item.id, provider);
|
|
2487
2656
|
}
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
const settings = this.settings;
|
|
2496
|
-
const model = settings.model ?? this.applied.model;
|
|
2497
|
-
if (model !== null && (model !== this.applied.model || settings.effort !== this.applied.effort)) await this.client.session.switchModel({
|
|
2498
|
-
sessionID: this.sessionId,
|
|
2499
|
-
model: modelRef(model, settings.effort)
|
|
2500
|
-
});
|
|
2501
|
-
if (settings.mode !== null && settings.mode !== this.applied.mode) await this.client.session.switchAgent({
|
|
2502
|
-
sessionID: this.sessionId,
|
|
2503
|
-
agent: settings.mode
|
|
2504
|
-
});
|
|
2505
|
-
this.applied = {
|
|
2506
|
-
...settings,
|
|
2507
|
-
model,
|
|
2508
|
-
mode: settings.mode ?? this.applied.mode
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2659
|
+
console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
2660
|
+
statuses[provider] = {
|
|
2661
|
+
provider,
|
|
2662
|
+
state: "error",
|
|
2663
|
+
error: message
|
|
2509
2664
|
};
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
request: {
|
|
2517
|
-
id: permission.id,
|
|
2518
|
-
kind: "approval",
|
|
2519
|
-
title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
|
|
2520
|
-
canAlwaysAllow: (permission.save ?? []).length > 0,
|
|
2521
|
-
item: item ?? {
|
|
2522
|
-
id: permission.id,
|
|
2523
|
-
parent: null,
|
|
2524
|
-
time: Date.now(),
|
|
2525
|
-
kind: "tool",
|
|
2526
|
-
name: permission.action,
|
|
2527
|
-
input: { resources: permission.resources },
|
|
2528
|
-
output: "",
|
|
2529
|
-
status: "running"
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2665
|
+
} finally {
|
|
2666
|
+
clearTimeout(timer);
|
|
2667
|
+
publishSessions();
|
|
2668
|
+
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
2669
|
+
type: "providers",
|
|
2670
|
+
providers: providerStatuses()
|
|
2532
2671
|
});
|
|
2533
2672
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2673
|
+
}
|
|
2674
|
+
async function find(provider, paths) {
|
|
2675
|
+
switch (provider) {
|
|
2676
|
+
case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => (await listSessions({
|
|
2677
|
+
dir: cwd,
|
|
2678
|
+
includeWorktrees: false,
|
|
2679
|
+
includeProgrammatic: false
|
|
2680
|
+
})).map((item) => ({
|
|
2681
|
+
id: item.sessionId,
|
|
2682
|
+
provider: "claude",
|
|
2683
|
+
title: item.summary,
|
|
2684
|
+
projectId,
|
|
2685
|
+
branch: item.gitBranch ?? null,
|
|
2686
|
+
updatedAt: item.lastModified
|
|
2687
|
+
}))))).flat();
|
|
2688
|
+
case "codex":
|
|
2689
|
+
if (!codexInstalled()) return null;
|
|
2690
|
+
return (await codexThreads([...paths.keys()])).flatMap((thread) => {
|
|
2691
|
+
const projectId = paths.get(thread.cwd);
|
|
2692
|
+
if (projectId === void 0) return [];
|
|
2693
|
+
return [{
|
|
2694
|
+
id: thread.id,
|
|
2695
|
+
provider: "codex",
|
|
2696
|
+
title: thread.name ?? thread.preview,
|
|
2697
|
+
projectId,
|
|
2698
|
+
branch: thread.gitInfo?.branch ?? null,
|
|
2699
|
+
updatedAt: thread.updatedAt * 1e3
|
|
2700
|
+
}];
|
|
2701
|
+
});
|
|
2702
|
+
case "opencode":
|
|
2703
|
+
if (!openCodeInstalled()) return null;
|
|
2704
|
+
return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
|
|
2705
|
+
const projectId = paths.get(session.location.directory);
|
|
2706
|
+
if (projectId === void 0) return [];
|
|
2707
|
+
return [{
|
|
2708
|
+
id: session.id,
|
|
2709
|
+
provider: "opencode",
|
|
2710
|
+
title: session.title ?? "New session",
|
|
2711
|
+
projectId,
|
|
2712
|
+
branch: null,
|
|
2713
|
+
updatedAt: session.time.updated
|
|
2714
|
+
}];
|
|
2715
|
+
});
|
|
2542
2716
|
}
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2717
|
+
}
|
|
2718
|
+
function watchSessions() {
|
|
2719
|
+
for (const watcher of watchers) watcher.close();
|
|
2720
|
+
watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
2721
|
+
if (!readConfig().enabledProviders[provider]) return [];
|
|
2722
|
+
try {
|
|
2723
|
+
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
2724
|
+
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
2725
|
+
return [watcher];
|
|
2726
|
+
} catch (error) {
|
|
2727
|
+
if (error.code !== "ENOENT") throw error;
|
|
2728
|
+
return [];
|
|
2551
2729
|
}
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
async function updateSession(id, patch) {
|
|
2733
|
+
if (patch.title !== void 0) switch (providerOf(id)) {
|
|
2734
|
+
case "claude":
|
|
2735
|
+
await renameSession(id, patch.title);
|
|
2736
|
+
break;
|
|
2737
|
+
case "codex":
|
|
2738
|
+
await renameThread(id, patch.title);
|
|
2739
|
+
break;
|
|
2740
|
+
case "opencode": await renameOpenCode(id, patch.title);
|
|
2555
2741
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2742
|
+
if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2743
|
+
else if (patch.status !== void 0) db.insert(sessions$1).values({
|
|
2744
|
+
sessionId: id,
|
|
2745
|
+
status: patch.status
|
|
2746
|
+
}).onConflictDoUpdate({
|
|
2747
|
+
target: sessions$1.sessionId,
|
|
2748
|
+
set: { status: patch.status }
|
|
2749
|
+
}).run();
|
|
2750
|
+
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
2751
|
+
}
|
|
2752
|
+
async function removeSession(id) {
|
|
2753
|
+
const provider = providerOf(id);
|
|
2754
|
+
const run = runs.get(id);
|
|
2755
|
+
if (run !== void 0) {
|
|
2756
|
+
run.stop();
|
|
2757
|
+
await run.closed;
|
|
2564
2758
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
});
|
|
2759
|
+
switch (provider) {
|
|
2760
|
+
case "claude":
|
|
2761
|
+
await deleteSession(id);
|
|
2762
|
+
break;
|
|
2763
|
+
case "codex":
|
|
2764
|
+
await deleteThread(id);
|
|
2765
|
+
break;
|
|
2766
|
+
case "opencode": await removeOpenCode(id);
|
|
2574
2767
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2768
|
+
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2769
|
+
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
2770
|
+
}
|
|
2771
|
+
async function fork(sessionId, projectId, messageId) {
|
|
2772
|
+
const provider = providerOf(sessionId);
|
|
2773
|
+
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
2774
|
+
providers.set(result.sessionId, provider);
|
|
2775
|
+
recordInteraction(result.sessionId);
|
|
2776
|
+
refreshSessions(provider);
|
|
2777
|
+
return result;
|
|
2778
|
+
}
|
|
2779
|
+
async function forkWith(provider, sessionId, cwd, messageId) {
|
|
2780
|
+
if (provider === "codex") return forkThread(sessionId, messageId);
|
|
2781
|
+
if (provider === "opencode") return forkOpenCode(sessionId, messageId);
|
|
2782
|
+
if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
|
|
2783
|
+
return { sessionId: (await forkSession(sessionId, {
|
|
2784
|
+
dir: cwd,
|
|
2785
|
+
upToMessageId: messageId
|
|
2786
|
+
})).sessionId };
|
|
2787
|
+
}
|
|
2788
|
+
//#endregion
|
|
2789
|
+
//#region src/lib/opencode/client.ts
|
|
2790
|
+
const listChanges = /* @__PURE__ */ new Set([
|
|
2791
|
+
"session.created",
|
|
2792
|
+
"session.renamed",
|
|
2793
|
+
"session.moved",
|
|
2794
|
+
"session.forked",
|
|
2795
|
+
"session.deleted",
|
|
2796
|
+
"session.execution.succeeded",
|
|
2797
|
+
"session.execution.failed",
|
|
2798
|
+
"session.execution.interrupted"
|
|
2799
|
+
]);
|
|
2800
|
+
const SILENCE_LIMIT$1 = 45e3;
|
|
2801
|
+
const RECONNECT_ATTEMPTS = 5;
|
|
2802
|
+
const RECONNECT_DELAY = 2e3;
|
|
2803
|
+
let connection;
|
|
2804
|
+
let listener;
|
|
2805
|
+
function findOpenCode() {
|
|
2806
|
+
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
2807
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
2808
|
+
const candidate = path.join(dir, name);
|
|
2809
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
2577
2810
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
function openCode() {
|
|
2814
|
+
connection ??= start();
|
|
2815
|
+
return connection;
|
|
2816
|
+
}
|
|
2817
|
+
function disconnectOpenCode() {
|
|
2818
|
+
const controller = listener;
|
|
2819
|
+
listener = void 0;
|
|
2820
|
+
connection = void 0;
|
|
2821
|
+
controller?.abort();
|
|
2822
|
+
}
|
|
2823
|
+
async function start() {
|
|
2824
|
+
try {
|
|
2825
|
+
const client = await connect();
|
|
2826
|
+
listen(client);
|
|
2827
|
+
return client;
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
connection = void 0;
|
|
2830
|
+
throw error;
|
|
2584
2831
|
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2832
|
+
}
|
|
2833
|
+
async function connect() {
|
|
2834
|
+
const endpoint = await ensure();
|
|
2835
|
+
return OpenCode.make({
|
|
2836
|
+
baseUrl: endpoint.url,
|
|
2837
|
+
headers: headers(endpoint)
|
|
2838
|
+
});
|
|
2839
|
+
}
|
|
2840
|
+
async function listen(client) {
|
|
2841
|
+
const controller = new AbortController();
|
|
2842
|
+
listener = controller;
|
|
2843
|
+
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
2844
|
+
try {
|
|
2845
|
+
for await (const event of client.event.subscribe({
|
|
2846
|
+
signal: controller.signal,
|
|
2847
|
+
onActivity: () => silence.refresh()
|
|
2848
|
+
})) route(event);
|
|
2849
|
+
console.warn("the OpenCode event stream ended");
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
if (listener !== controller) return;
|
|
2852
|
+
console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
|
|
2853
|
+
} finally {
|
|
2854
|
+
clearTimeout(silence);
|
|
2592
2855
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2856
|
+
if (listener !== controller) return;
|
|
2857
|
+
connection = reconnect();
|
|
2858
|
+
connection.catch(() => void 0);
|
|
2859
|
+
}
|
|
2860
|
+
async function reconnect() {
|
|
2861
|
+
let failure;
|
|
2862
|
+
for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
|
|
2863
|
+
await setTimeout$1(RECONNECT_DELAY);
|
|
2864
|
+
let client;
|
|
2865
|
+
try {
|
|
2866
|
+
client = await connect();
|
|
2867
|
+
} catch (error) {
|
|
2868
|
+
failure = error;
|
|
2869
|
+
continue;
|
|
2870
|
+
}
|
|
2871
|
+
listen(client);
|
|
2872
|
+
await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
|
|
2873
|
+
return client;
|
|
2599
2874
|
}
|
|
2600
|
-
};
|
|
2601
|
-
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
|
|
2606
|
-
effort: variant === void 0 || variant === "default" ? null : variant,
|
|
2607
|
-
mode: session.agent ?? null
|
|
2608
|
-
};
|
|
2875
|
+
console.warn(`could not reconnect to OpenCode: ${failure}`);
|
|
2876
|
+
connection = void 0;
|
|
2877
|
+
for (const run of openCodeRuns()) run.exited();
|
|
2878
|
+
refreshSessions("opencode");
|
|
2879
|
+
throw failure;
|
|
2609
2880
|
}
|
|
2610
|
-
function
|
|
2611
|
-
|
|
2881
|
+
async function catchUp(client) {
|
|
2882
|
+
const active = await client.session.active();
|
|
2883
|
+
for (const run of openCodeRuns()) {
|
|
2884
|
+
const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
|
|
2885
|
+
run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
|
|
2886
|
+
}
|
|
2887
|
+
refreshSessions("opencode");
|
|
2888
|
+
}
|
|
2889
|
+
function route(event) {
|
|
2890
|
+
if (listChanges.has(event.type)) refreshSessions("opencode");
|
|
2891
|
+
const sessionId = sessionOf(event);
|
|
2892
|
+
if (sessionId === null) return;
|
|
2893
|
+
const run = runs.get(sessionId);
|
|
2894
|
+
if (run?.provider === "opencode") run.receive(event);
|
|
2895
|
+
}
|
|
2896
|
+
function sessionOf(event) {
|
|
2897
|
+
if (event.type === "form.created") return event.data.form.sessionID;
|
|
2898
|
+
const data = "data" in event ? event.data : null;
|
|
2899
|
+
if (isRecord(data) && typeof data.sessionID === "string") return data.sessionID;
|
|
2900
|
+
return null;
|
|
2901
|
+
}
|
|
2902
|
+
function openCodeRuns() {
|
|
2903
|
+
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
2612
2904
|
}
|
|
2613
2905
|
//#endregion
|
|
2614
2906
|
//#region src/services/accounts.ts
|
|
@@ -2622,10 +2914,36 @@ function providerName(provider) {
|
|
|
2622
2914
|
}
|
|
2623
2915
|
function storedAccounts() {
|
|
2624
2916
|
return (readJson("accounts.json") ?? []).map((item) => ({
|
|
2625
|
-
|
|
2626
|
-
provider: item.provider
|
|
2917
|
+
id: item.id,
|
|
2918
|
+
provider: item.provider,
|
|
2919
|
+
name: item.name,
|
|
2920
|
+
configDir: item.configDir,
|
|
2921
|
+
enabled: item.enabled ?? true,
|
|
2922
|
+
hiddenModels: item.hiddenModels ?? {}
|
|
2627
2923
|
}));
|
|
2628
2924
|
}
|
|
2925
|
+
function assertEnabled(provider, accountId) {
|
|
2926
|
+
if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
|
|
2927
|
+
if (provider === "opencode") return;
|
|
2928
|
+
if (!storedAccount(provider, accountId).enabled) throw new Error("That account is disabled in Settings");
|
|
2929
|
+
}
|
|
2930
|
+
async function stopProvider(provider) {
|
|
2931
|
+
const stopping = [...runs.values()].filter((run) => run.provider === provider);
|
|
2932
|
+
for (const run of stopping) run.stop();
|
|
2933
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2934
|
+
if (provider === "codex") stopAppServers();
|
|
2935
|
+
if (provider === "opencode") disconnectOpenCode();
|
|
2936
|
+
if (provider === "claude") {
|
|
2937
|
+
for (const account of storedAccounts()) if (account.provider === "claude") forgetClaude(account.id);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
async function stopAccount(account) {
|
|
2941
|
+
const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
|
|
2942
|
+
for (const run of stopping) run.stop();
|
|
2943
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2944
|
+
if (account.provider === "codex") stopAppServer(account.configDir);
|
|
2945
|
+
if (account.provider === "claude") forgetClaude(account.id);
|
|
2946
|
+
}
|
|
2629
2947
|
function storedAccount(provider, accountId) {
|
|
2630
2948
|
if (accountId === null) throw new Error(`Add a ${providerName(provider)} account in Settings → Accounts`);
|
|
2631
2949
|
const account = storedAccounts().find((item) => item.id === accountId);
|
|
@@ -2635,49 +2953,92 @@ function storedAccount(provider, accountId) {
|
|
|
2635
2953
|
}
|
|
2636
2954
|
async function accounts() {
|
|
2637
2955
|
const stored = await Promise.all(storedAccounts().map((account) => {
|
|
2956
|
+
if (!account.enabled || !readConfig().enabledProviders[account.provider]) return disabledAccount(account);
|
|
2638
2957
|
switch (account.provider) {
|
|
2639
2958
|
case "claude": return claudeAccount(account);
|
|
2640
2959
|
case "codex": return codexAccount(account);
|
|
2641
|
-
case "opencode": throw new Error("OpenCode has no stored accounts");
|
|
2642
2960
|
}
|
|
2643
2961
|
}));
|
|
2644
|
-
return
|
|
2962
|
+
return {
|
|
2963
|
+
claudeAccounts: stored.filter((item) => item.provider === "claude"),
|
|
2964
|
+
codexAccounts: stored.filter((item) => item.provider === "codex"),
|
|
2965
|
+
openCode: { available: openCodeInstalled() && readConfig().enabledProviders.opencode }
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function disabledAccount(account) {
|
|
2969
|
+
return {
|
|
2970
|
+
...account,
|
|
2971
|
+
email: null,
|
|
2972
|
+
plan: null,
|
|
2973
|
+
error: null,
|
|
2974
|
+
limits: [],
|
|
2975
|
+
linked: true,
|
|
2976
|
+
valid: false
|
|
2977
|
+
};
|
|
2645
2978
|
}
|
|
2646
2979
|
async function publishAccounts() {
|
|
2647
2980
|
try {
|
|
2648
2981
|
broadcast({
|
|
2649
2982
|
type: "accounts",
|
|
2650
|
-
|
|
2983
|
+
...await accounts()
|
|
2651
2984
|
});
|
|
2652
2985
|
} catch (error) {
|
|
2653
2986
|
console.warn(`could not list accounts: ${error}`);
|
|
2654
2987
|
}
|
|
2655
2988
|
}
|
|
2656
|
-
function capabilities(accountId, cwd) {
|
|
2657
|
-
|
|
2658
|
-
const
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2989
|
+
async function capabilities(provider, accountId, cwd) {
|
|
2990
|
+
assertEnabled(provider, accountId);
|
|
2991
|
+
const result = await providerCapabilities(provider, accountId, cwd);
|
|
2992
|
+
const settings = readConfig().models[provider];
|
|
2993
|
+
const overrides = provider === "opencode" ? {} : storedAccount(provider, accountId).hiddenModels;
|
|
2994
|
+
return {
|
|
2995
|
+
...result,
|
|
2996
|
+
defaultModel: defaultModels[provider],
|
|
2997
|
+
models: result.models.map((model) => {
|
|
2998
|
+
const deprecated = deprecatedModels[provider].includes(model.value);
|
|
2999
|
+
const setting = settings[model.value] ?? {
|
|
3000
|
+
hidden: deprecated,
|
|
3001
|
+
effort: null
|
|
3002
|
+
};
|
|
3003
|
+
return {
|
|
3004
|
+
...model,
|
|
3005
|
+
deprecated,
|
|
3006
|
+
hidden: overrides[model.value] ?? setting.hidden,
|
|
3007
|
+
effort: setting.effort
|
|
3008
|
+
};
|
|
3009
|
+
})
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
function providerCapabilities(provider, accountId, cwd) {
|
|
3013
|
+
switch (provider) {
|
|
3014
|
+
case "claude": return claudeCapabilities(storedAccount(provider, accountId), cwd);
|
|
3015
|
+
case "codex": return codexCapabilities(storedAccount(provider, accountId), cwd);
|
|
3016
|
+
case "opencode":
|
|
3017
|
+
if (accountId !== null) throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
3018
|
+
return openCodeCapabilities(cwd);
|
|
2664
3019
|
}
|
|
2665
3020
|
}
|
|
2666
3021
|
function addAccount(provider, name, configDir) {
|
|
2667
|
-
if (provider === "opencode") throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
2668
3022
|
saveAccounts([...storedAccounts(), {
|
|
2669
3023
|
id: randomUUID(),
|
|
2670
3024
|
provider,
|
|
2671
|
-
...validAccount(name, configDir)
|
|
3025
|
+
...validAccount(name, configDir),
|
|
3026
|
+
enabled: true,
|
|
3027
|
+
hiddenModels: {}
|
|
2672
3028
|
}]);
|
|
2673
3029
|
}
|
|
2674
|
-
function updateAccount(
|
|
2675
|
-
const
|
|
2676
|
-
|
|
3030
|
+
async function updateAccount(update) {
|
|
3031
|
+
const before = storedAccounts().find((item) => item.id === update.id);
|
|
3032
|
+
if (before === void 0) throw new Error("That account was removed");
|
|
3033
|
+
const account = validAccount(update.name, update.configDir);
|
|
3034
|
+
saveAccounts(storedAccounts().map((item) => item.id === update.id ? {
|
|
2677
3035
|
...item,
|
|
2678
|
-
...account
|
|
3036
|
+
...account,
|
|
3037
|
+
enabled: update.enabled,
|
|
3038
|
+
hiddenModels: update.hiddenModels
|
|
2679
3039
|
} : item));
|
|
2680
|
-
forgetClaude(id);
|
|
3040
|
+
forgetClaude(update.id);
|
|
3041
|
+
if (before.enabled && !update.enabled) await stopAccount(before);
|
|
2681
3042
|
}
|
|
2682
3043
|
function removeAccount(id) {
|
|
2683
3044
|
saveAccounts(storedAccounts().filter((item) => item.id !== id));
|
|
@@ -2700,81 +3061,6 @@ function saveAccounts(items) {
|
|
|
2700
3061
|
writeJson("accounts.json", items);
|
|
2701
3062
|
}
|
|
2702
3063
|
//#endregion
|
|
2703
|
-
//#region src/lib/claude/content.ts
|
|
2704
|
-
function toMessage(type, item, time) {
|
|
2705
|
-
return {
|
|
2706
|
-
type,
|
|
2707
|
-
uuid: item.uuid,
|
|
2708
|
-
time,
|
|
2709
|
-
content: isRecord(item.message) ? toBlocks(item.message.content, answersOf(item.tool_use_result)) : [],
|
|
2710
|
-
parent: item.parent_tool_use_id
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
function contextTokens(message) {
|
|
2714
|
-
if (!isRecord(message) || !isRecord(message.usage)) return null;
|
|
2715
|
-
const usage = message.usage;
|
|
2716
|
-
return [
|
|
2717
|
-
usage.input_tokens,
|
|
2718
|
-
usage.cache_creation_input_tokens,
|
|
2719
|
-
usage.cache_read_input_tokens,
|
|
2720
|
-
usage.output_tokens
|
|
2721
|
-
].reduce((total, value) => total + (typeof value === "number" ? value : 0), 0);
|
|
2722
|
-
}
|
|
2723
|
-
function answersOf(output) {
|
|
2724
|
-
if (!isRecord(output) || !isRecord(output.answers)) return null;
|
|
2725
|
-
const answers = {};
|
|
2726
|
-
for (const [question, answer] of Object.entries(output.answers)) {
|
|
2727
|
-
if (typeof answer !== "string") return null;
|
|
2728
|
-
answers[question] = answer;
|
|
2729
|
-
}
|
|
2730
|
-
return answers;
|
|
2731
|
-
}
|
|
2732
|
-
function toBlocks(content, answers) {
|
|
2733
|
-
if (typeof content === "string") return [{
|
|
2734
|
-
type: "text",
|
|
2735
|
-
text: content
|
|
2736
|
-
}];
|
|
2737
|
-
if (!Array.isArray(content)) return [];
|
|
2738
|
-
return content.flatMap((block) => {
|
|
2739
|
-
if (!isRecord(block)) return [];
|
|
2740
|
-
switch (block.type) {
|
|
2741
|
-
case "text": return typeof block.text === "string" && !block.text.startsWith("<system-reminder>") && !block.text.startsWith("<local-command-caveat>") ? [{
|
|
2742
|
-
type: "text",
|
|
2743
|
-
text: block.text
|
|
2744
|
-
}] : [];
|
|
2745
|
-
case "thinking": return typeof block.thinking === "string" && block.thinking !== "" ? [{
|
|
2746
|
-
type: "thinking",
|
|
2747
|
-
thinking: block.thinking
|
|
2748
|
-
}] : [];
|
|
2749
|
-
case "image": {
|
|
2750
|
-
const source = block.source;
|
|
2751
|
-
return isRecord(source) && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string" ? [{
|
|
2752
|
-
type: "image",
|
|
2753
|
-
mediaType: source.media_type,
|
|
2754
|
-
data: source.data
|
|
2755
|
-
}] : [];
|
|
2756
|
-
}
|
|
2757
|
-
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && isRecord(block.input) ? [{
|
|
2758
|
-
type: "tool_use",
|
|
2759
|
-
id: block.id,
|
|
2760
|
-
name: block.name,
|
|
2761
|
-
input: block.input
|
|
2762
|
-
}] : [];
|
|
2763
|
-
case "tool_result": return typeof block.tool_use_id === "string" ? [{
|
|
2764
|
-
type: "tool_result",
|
|
2765
|
-
toolUseId: block.tool_use_id,
|
|
2766
|
-
content: toBlocks(block.content, null).filter((item) => item.type === "text"),
|
|
2767
|
-
isError: block.is_error === true,
|
|
2768
|
-
answers
|
|
2769
|
-
}] : [];
|
|
2770
|
-
default: return [];
|
|
2771
|
-
}
|
|
2772
|
-
});
|
|
2773
|
-
}
|
|
2774
|
-
function isRecord(value) {
|
|
2775
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2776
|
-
}
|
|
2777
|
-
//#endregion
|
|
2778
3064
|
//#region src/lib/claude/claude.ts
|
|
2779
3065
|
const efforts = [
|
|
2780
3066
|
"low",
|
|
@@ -2817,13 +3103,15 @@ function claudeMode(value) {
|
|
|
2817
3103
|
}
|
|
2818
3104
|
function claudeOptions(accountId, cwd) {
|
|
2819
3105
|
const account = storedAccount("claude", accountId);
|
|
3106
|
+
const env = {
|
|
3107
|
+
...process.env,
|
|
3108
|
+
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
3109
|
+
CLAUDE_CONFIG_DIR: account.configDir
|
|
3110
|
+
};
|
|
3111
|
+
if (path.resolve(account.configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
|
|
2820
3112
|
return {
|
|
2821
3113
|
cwd,
|
|
2822
|
-
env
|
|
2823
|
-
...process.env,
|
|
2824
|
-
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
2825
|
-
CLAUDE_CONFIG_DIR: account.configDir
|
|
2826
|
-
},
|
|
3114
|
+
env,
|
|
2827
3115
|
pathToClaudeCodeExecutable: executable$1,
|
|
2828
3116
|
systemPrompt: {
|
|
2829
3117
|
type: "preset",
|
|
@@ -2832,7 +3120,7 @@ function claudeOptions(accountId, cwd) {
|
|
|
2832
3120
|
hooks: { PostToolUse: [{
|
|
2833
3121
|
matcher: "PushNotification",
|
|
2834
3122
|
hooks: [async (input) => {
|
|
2835
|
-
if (input.hook_event_name !== "PostToolUse" ||
|
|
3123
|
+
if (input.hook_event_name !== "PostToolUse" || !isRecord(input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
|
|
2836
3124
|
broadcast({
|
|
2837
3125
|
type: "notification",
|
|
2838
3126
|
sessionId: input.session_id,
|
|
@@ -2853,12 +3141,15 @@ function claudeOptions(accountId, cwd) {
|
|
|
2853
3141
|
async function claudeCapabilities(account, cwd) {
|
|
2854
3142
|
const result = await initialization(account.id, cwd);
|
|
2855
3143
|
return {
|
|
2856
|
-
models: result.models.map((model) =>
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
3144
|
+
models: result.models.filter((model) => model.value !== "default").map((model) => {
|
|
3145
|
+
if (model.resolvedModel === void 0) throw new Error(`Claude did not resolve the ${model.value} model`);
|
|
3146
|
+
return {
|
|
3147
|
+
value: model.resolvedModel,
|
|
3148
|
+
name: model.displayName,
|
|
3149
|
+
description: model.description,
|
|
3150
|
+
efforts: model.supportedEffortLevels ?? []
|
|
3151
|
+
};
|
|
3152
|
+
}),
|
|
2862
3153
|
commands: result.commands.map((command) => ({
|
|
2863
3154
|
name: command.name,
|
|
2864
3155
|
description: command.description,
|
|
@@ -2919,6 +3210,28 @@ function recordLimit(accountId, info) {
|
|
|
2919
3210
|
limits.set(accountId, current);
|
|
2920
3211
|
return accountLimits(accountId);
|
|
2921
3212
|
}
|
|
3213
|
+
async function recordUsage(accountId, probe) {
|
|
3214
|
+
try {
|
|
3215
|
+
const usage = await probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true });
|
|
3216
|
+
if (usage.rate_limits === null) return;
|
|
3217
|
+
const current = limits.get(accountId) ?? /* @__PURE__ */ new Map();
|
|
3218
|
+
const windows = {
|
|
3219
|
+
five_hour: usage.rate_limits.five_hour,
|
|
3220
|
+
seven_day: usage.rate_limits.seven_day,
|
|
3221
|
+
seven_day_opus: usage.rate_limits.seven_day_opus,
|
|
3222
|
+
seven_day_sonnet: usage.rate_limits.seven_day_sonnet
|
|
3223
|
+
};
|
|
3224
|
+
for (const [name, window] of Object.entries(windows)) if (window !== void 0 && window !== null) current.set(name, {
|
|
3225
|
+
window: name,
|
|
3226
|
+
utilization: window.utilization === null ? null : window.utilization / 100,
|
|
3227
|
+
resetsAt: window.resets_at === null ? null : Date.parse(window.resets_at) / 1e3,
|
|
3228
|
+
rejected: window.utilization !== null && window.utilization >= 100
|
|
3229
|
+
});
|
|
3230
|
+
limits.set(accountId, current);
|
|
3231
|
+
} catch (error) {
|
|
3232
|
+
console.warn(`could not read Claude usage limits: ${error}`);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
2922
3235
|
function isLinked(configDir) {
|
|
2923
3236
|
const base = readConfig().claudeBaseDir;
|
|
2924
3237
|
try {
|
|
@@ -2948,7 +3261,9 @@ async function runProbe(accountId, cwd) {
|
|
|
2948
3261
|
options: claudeOptions(accountId, cwd)
|
|
2949
3262
|
});
|
|
2950
3263
|
try {
|
|
2951
|
-
|
|
3264
|
+
const result = await probe.initializationResult();
|
|
3265
|
+
await recordUsage(accountId, probe);
|
|
3266
|
+
return result;
|
|
2952
3267
|
} finally {
|
|
2953
3268
|
closed.resolve();
|
|
2954
3269
|
probe.close();
|
|
@@ -3007,14 +3322,15 @@ function toItems(messages) {
|
|
|
3007
3322
|
}
|
|
3008
3323
|
if (message.type === "assistant") {
|
|
3009
3324
|
message.content.forEach((block, index) => {
|
|
3010
|
-
const
|
|
3011
|
-
|
|
3325
|
+
const shared = {
|
|
3326
|
+
id: `${message.uuid}:${index}`,
|
|
3327
|
+
parent: message.parent,
|
|
3328
|
+
time: message.time
|
|
3329
|
+
};
|
|
3012
3330
|
switch (block.type) {
|
|
3013
3331
|
case "text":
|
|
3014
3332
|
if (block.text.trim() !== "") items.push({
|
|
3015
|
-
|
|
3016
|
-
parent,
|
|
3017
|
-
time: message.time,
|
|
3333
|
+
...shared,
|
|
3018
3334
|
kind: "assistant",
|
|
3019
3335
|
text: block.text,
|
|
3020
3336
|
message: message.uuid
|
|
@@ -3022,21 +3338,19 @@ function toItems(messages) {
|
|
|
3022
3338
|
break;
|
|
3023
3339
|
case "thinking":
|
|
3024
3340
|
if (block.thinking.trim() !== "") items.push({
|
|
3025
|
-
|
|
3026
|
-
parent,
|
|
3027
|
-
time: message.time,
|
|
3341
|
+
...shared,
|
|
3028
3342
|
kind: "reasoning",
|
|
3029
3343
|
text: block.thinking
|
|
3030
3344
|
});
|
|
3031
3345
|
break;
|
|
3032
3346
|
case "tool_use":
|
|
3033
3347
|
if (block.name === "SendMessage") {
|
|
3034
|
-
const owner = owners.get(text(block.input.to));
|
|
3348
|
+
const owner = owners.get(text$1(block.input.to));
|
|
3035
3349
|
if (owner !== void 0) change(owner, { status: "running" });
|
|
3036
3350
|
}
|
|
3037
|
-
if (block.name === "TaskStop") stops.set(block.id, text(block.input.task_id));
|
|
3351
|
+
if (block.name === "TaskStop") stops.set(block.id, text$1(block.input.task_id));
|
|
3038
3352
|
tools.set(block.id, items.length);
|
|
3039
|
-
items.push(toolItem(block, parent, message.time, (to) => {
|
|
3353
|
+
items.push(toolItem(block, message.parent, message.time, (to) => {
|
|
3040
3354
|
const owner = owners.get(to);
|
|
3041
3355
|
return owner === void 0 ? null : items[owner].id;
|
|
3042
3356
|
}));
|
|
@@ -3125,7 +3439,7 @@ function toRequest(options) {
|
|
|
3125
3439
|
case "ExitPlanMode": return {
|
|
3126
3440
|
id: options.id,
|
|
3127
3441
|
kind: "plan",
|
|
3128
|
-
plan: text(options.input.plan)
|
|
3442
|
+
plan: text$1(options.input.plan)
|
|
3129
3443
|
};
|
|
3130
3444
|
default: return {
|
|
3131
3445
|
id: options.id,
|
|
@@ -3152,8 +3466,8 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3152
3466
|
case "Bash": return {
|
|
3153
3467
|
...base,
|
|
3154
3468
|
kind: "command",
|
|
3155
|
-
command: text(input.command),
|
|
3156
|
-
description: text(input.description),
|
|
3469
|
+
command: text$1(input.command),
|
|
3470
|
+
description: text$1(input.description),
|
|
3157
3471
|
output: "",
|
|
3158
3472
|
status: "running",
|
|
3159
3473
|
background: input.run_in_background === true ? {
|
|
@@ -3165,8 +3479,8 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3165
3479
|
case "Monitor": return {
|
|
3166
3480
|
...base,
|
|
3167
3481
|
kind: "command",
|
|
3168
|
-
command: text(input.command) || (isRecord(input.ws) ? text(input.ws.url) : ""),
|
|
3169
|
-
description: text(input.description),
|
|
3482
|
+
command: text$1(input.command) || (isRecord(input.ws) ? text$1(input.ws.url) : ""),
|
|
3483
|
+
description: text$1(input.description),
|
|
3170
3484
|
output: "",
|
|
3171
3485
|
status: "running",
|
|
3172
3486
|
background: {
|
|
@@ -3178,10 +3492,10 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3178
3492
|
case "Edit": return {
|
|
3179
3493
|
...base,
|
|
3180
3494
|
kind: "file_change",
|
|
3181
|
-
path: text(input.file_path),
|
|
3495
|
+
path: text$1(input.file_path),
|
|
3182
3496
|
diffs: [{
|
|
3183
|
-
before: text(input.old_string),
|
|
3184
|
-
after: text(input.new_string)
|
|
3497
|
+
before: text$1(input.old_string),
|
|
3498
|
+
after: text$1(input.new_string)
|
|
3185
3499
|
}],
|
|
3186
3500
|
output: "",
|
|
3187
3501
|
status: "running"
|
|
@@ -3189,7 +3503,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3189
3503
|
case "MultiEdit": return {
|
|
3190
3504
|
...base,
|
|
3191
3505
|
kind: "file_change",
|
|
3192
|
-
path: text(input.file_path),
|
|
3506
|
+
path: text$1(input.file_path),
|
|
3193
3507
|
diffs: input.edits.map((edit) => ({
|
|
3194
3508
|
before: edit.old_string,
|
|
3195
3509
|
after: edit.new_string
|
|
@@ -3200,10 +3514,10 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3200
3514
|
case "Write": return {
|
|
3201
3515
|
...base,
|
|
3202
3516
|
kind: "file_change",
|
|
3203
|
-
path: text(input.file_path),
|
|
3517
|
+
path: text$1(input.file_path),
|
|
3204
3518
|
diffs: [{
|
|
3205
3519
|
before: "",
|
|
3206
|
-
after: text(input.content)
|
|
3520
|
+
after: text$1(input.content)
|
|
3207
3521
|
}],
|
|
3208
3522
|
output: "",
|
|
3209
3523
|
status: "running"
|
|
@@ -3211,7 +3525,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3211
3525
|
case "NotebookEdit": return {
|
|
3212
3526
|
...base,
|
|
3213
3527
|
kind: "file_change",
|
|
3214
|
-
path: text(input.notebook_path),
|
|
3528
|
+
path: text$1(input.notebook_path),
|
|
3215
3529
|
diffs: [],
|
|
3216
3530
|
output: "",
|
|
3217
3531
|
status: "running"
|
|
@@ -3220,7 +3534,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3220
3534
|
...base,
|
|
3221
3535
|
kind: "web_search",
|
|
3222
3536
|
query: null,
|
|
3223
|
-
url: text(input.url),
|
|
3537
|
+
url: text$1(input.url),
|
|
3224
3538
|
results: [],
|
|
3225
3539
|
output: "",
|
|
3226
3540
|
status: "running"
|
|
@@ -3228,7 +3542,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3228
3542
|
case "WebSearch": return {
|
|
3229
3543
|
...base,
|
|
3230
3544
|
kind: "web_search",
|
|
3231
|
-
query: text(input.query),
|
|
3545
|
+
query: text$1(input.query),
|
|
3232
3546
|
url: null,
|
|
3233
3547
|
results: [],
|
|
3234
3548
|
output: "",
|
|
@@ -3237,7 +3551,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3237
3551
|
case "Skill": return {
|
|
3238
3552
|
...base,
|
|
3239
3553
|
kind: "skill",
|
|
3240
|
-
skill: text(input.skill),
|
|
3554
|
+
skill: text$1(input.skill),
|
|
3241
3555
|
output: "",
|
|
3242
3556
|
status: "running"
|
|
3243
3557
|
};
|
|
@@ -3252,7 +3566,7 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3252
3566
|
case "ExitPlanMode": return {
|
|
3253
3567
|
...base,
|
|
3254
3568
|
kind: "plan",
|
|
3255
|
-
plan: text(input.plan)
|
|
3569
|
+
plan: text$1(input.plan)
|
|
3256
3570
|
};
|
|
3257
3571
|
case "AskUserQuestion": return {
|
|
3258
3572
|
...base,
|
|
@@ -3263,23 +3577,23 @@ function toolItem(tool, parent, time, ownerOf) {
|
|
|
3263
3577
|
};
|
|
3264
3578
|
case "Agent":
|
|
3265
3579
|
case "Task": {
|
|
3266
|
-
const agent = text(input.subagent_type);
|
|
3580
|
+
const agent = text$1(input.subagent_type);
|
|
3267
3581
|
return {
|
|
3268
3582
|
...base,
|
|
3269
3583
|
kind: "subagent",
|
|
3270
3584
|
agent: agent === "" ? "general-purpose" : agent,
|
|
3271
|
-
description: text(input.description),
|
|
3585
|
+
description: text$1(input.description),
|
|
3272
3586
|
agentId: null,
|
|
3273
3587
|
status: "running"
|
|
3274
3588
|
};
|
|
3275
3589
|
}
|
|
3276
3590
|
case "SendMessage": {
|
|
3277
|
-
const summary = text(input.summary);
|
|
3591
|
+
const summary = text$1(input.summary);
|
|
3278
3592
|
return {
|
|
3279
3593
|
...base,
|
|
3280
3594
|
kind: "agent_message",
|
|
3281
|
-
target: ownerOf(text(input.to)),
|
|
3282
|
-
summary: summary === "" ? text(input.message).split("\n")[0] : summary,
|
|
3595
|
+
target: ownerOf(text$1(input.to)),
|
|
3596
|
+
summary: summary === "" ? text$1(input.message).split("\n")[0] : summary,
|
|
3283
3597
|
status: "running"
|
|
3284
3598
|
};
|
|
3285
3599
|
}
|
|
@@ -3369,7 +3683,7 @@ function userText(value) {
|
|
|
3369
3683
|
text: value
|
|
3370
3684
|
};
|
|
3371
3685
|
}
|
|
3372
|
-
function text(value) {
|
|
3686
|
+
function text$1(value) {
|
|
3373
3687
|
return typeof value === "string" ? value : "";
|
|
3374
3688
|
}
|
|
3375
3689
|
//#endregion
|
|
@@ -3498,11 +3812,13 @@ async function history(sessionId, cwd) {
|
|
|
3498
3812
|
const seen = new Set(messages.map((item) => item.uuid));
|
|
3499
3813
|
const last = kept.findLast((item) => item.type === "assistant" && item.parent_tool_use_id === null);
|
|
3500
3814
|
const tokens = last === void 0 ? null : contextTokens(last.message);
|
|
3815
|
+
const live = run?.messages ?? [];
|
|
3816
|
+
const compacted = live.findLastIndex((item) => item.type === "compaction");
|
|
3501
3817
|
return {
|
|
3502
3818
|
type: "history",
|
|
3503
3819
|
sessionId,
|
|
3504
3820
|
provider: "claude",
|
|
3505
|
-
items: toItems([...messages, ...
|
|
3821
|
+
items: toItems([...messages, ...live.slice(compacted + 1).filter((item) => !seen.has(item.uuid))]),
|
|
3506
3822
|
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
3507
3823
|
queue: run?.queue ?? [],
|
|
3508
3824
|
settings: run?.settings ?? null,
|
|
@@ -3538,12 +3854,6 @@ async function agentMessages(sessionId, cwd, agentId, parent) {
|
|
|
3538
3854
|
//#endregion
|
|
3539
3855
|
//#region src/lib/claude/run.ts
|
|
3540
3856
|
const IDLE_LIMIT = 6e5;
|
|
3541
|
-
const imageTypes = [
|
|
3542
|
-
"image/png",
|
|
3543
|
-
"image/jpeg",
|
|
3544
|
-
"image/gif",
|
|
3545
|
-
"image/webp"
|
|
3546
|
-
];
|
|
3547
3857
|
var Run = class {
|
|
3548
3858
|
provider = "claude";
|
|
3549
3859
|
sessionId;
|
|
@@ -3673,42 +3983,10 @@ var Run = class {
|
|
|
3673
3983
|
}
|
|
3674
3984
|
push(prompt, priority) {
|
|
3675
3985
|
clearTimeout(this.idleTimer);
|
|
3676
|
-
for (const image of prompt.images) if (!imageTypes.includes(image.mediaType)) throw new Error(`${image.mediaType} images are not supported`);
|
|
3677
3986
|
const uuid = randomUUID();
|
|
3678
|
-
this.inbox.push(
|
|
3679
|
-
type: "user",
|
|
3680
|
-
message: {
|
|
3681
|
-
role: "user",
|
|
3682
|
-
content: [...prompt.images.map((image) => ({
|
|
3683
|
-
type: "image",
|
|
3684
|
-
source: {
|
|
3685
|
-
type: "base64",
|
|
3686
|
-
media_type: image.mediaType,
|
|
3687
|
-
data: image.data
|
|
3688
|
-
}
|
|
3689
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3690
|
-
type: "text",
|
|
3691
|
-
text: prompt.text
|
|
3692
|
-
}]]
|
|
3693
|
-
},
|
|
3694
|
-
parent_tool_use_id: null,
|
|
3695
|
-
uuid,
|
|
3696
|
-
...priority === void 0 ? {} : { priority }
|
|
3697
|
-
});
|
|
3987
|
+
this.inbox.push(toUserInput(prompt, uuid, priority));
|
|
3698
3988
|
this.wake?.();
|
|
3699
|
-
this.add(
|
|
3700
|
-
type: "user",
|
|
3701
|
-
uuid,
|
|
3702
|
-
time: Date.now(),
|
|
3703
|
-
content: [...prompt.images.map((image) => ({
|
|
3704
|
-
type: "image",
|
|
3705
|
-
...image
|
|
3706
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3707
|
-
type: "text",
|
|
3708
|
-
text: prompt.text
|
|
3709
|
-
}]],
|
|
3710
|
-
parent: null
|
|
3711
|
-
});
|
|
3989
|
+
this.add(promptMessage(prompt, uuid));
|
|
3712
3990
|
this.running = true;
|
|
3713
3991
|
this.prompted = true;
|
|
3714
3992
|
broadcastActivity();
|
|
@@ -3902,6 +4180,7 @@ var Run = class {
|
|
|
3902
4180
|
request
|
|
3903
4181
|
});
|
|
3904
4182
|
broadcastActivity();
|
|
4183
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
3905
4184
|
options.signal.addEventListener("abort", () => this.settle(request.id, {
|
|
3906
4185
|
behavior: "deny",
|
|
3907
4186
|
message: "Cancelled"
|
|
@@ -3952,21 +4231,13 @@ var Run = class {
|
|
|
3952
4231
|
});
|
|
3953
4232
|
}
|
|
3954
4233
|
};
|
|
3955
|
-
function notice(text) {
|
|
3956
|
-
return {
|
|
3957
|
-
type: "notice",
|
|
3958
|
-
uuid: randomUUID(),
|
|
3959
|
-
time: Date.now(),
|
|
3960
|
-
text,
|
|
3961
|
-
isError: true
|
|
3962
|
-
};
|
|
3963
|
-
}
|
|
3964
4234
|
//#endregion
|
|
3965
4235
|
//#region src/services/git.ts
|
|
3966
4236
|
const DIFF_LIMIT = 8e4;
|
|
3967
4237
|
const UNTRACKED_LIMIT = 100;
|
|
3968
4238
|
const NEW_FILE_LIMIT = 262144;
|
|
3969
4239
|
const REPO_DEPTH = 3;
|
|
4240
|
+
const run$1 = promisify(execFile);
|
|
3970
4241
|
async function gitStatus(cwd) {
|
|
3971
4242
|
let output;
|
|
3972
4243
|
try {
|
|
@@ -4020,20 +4291,24 @@ async function push$1(cwd) {
|
|
|
4020
4291
|
return gitStatus(cwd);
|
|
4021
4292
|
}
|
|
4022
4293
|
async function createPullRequest(cwd) {
|
|
4023
|
-
const
|
|
4294
|
+
const url = (await run$1("gh", [
|
|
4024
4295
|
"pr",
|
|
4025
4296
|
"create",
|
|
4026
4297
|
"--fill"
|
|
4027
|
-
], { cwd });
|
|
4028
|
-
const url = stdout.trim().split("\n").at(-1);
|
|
4298
|
+
], { cwd })).stdout.trim().split("\n").at(-1);
|
|
4029
4299
|
if (url === void 0) throw new Error("gh did not return a pull request URL");
|
|
4030
4300
|
return { url };
|
|
4031
4301
|
}
|
|
4032
|
-
async function suggestCommitMessage(
|
|
4033
|
-
const
|
|
4302
|
+
async function suggestCommitMessage(cwd) {
|
|
4303
|
+
const settings = readConfig().commitMessage;
|
|
4304
|
+
const provider = settings.provider;
|
|
4305
|
+
const accountId = settings.accountId;
|
|
4306
|
+
const model = settings.model;
|
|
4307
|
+
if (provider === null || model === null) throw new Error("Choose a commit message model in Settings → Git");
|
|
4308
|
+
assertEnabled(provider, accountId);
|
|
4309
|
+
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4310
|
+
const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
|
|
4034
4311
|
if (diff === "") throw new Error("There is nothing to commit");
|
|
4035
|
-
const model = readConfig().commitModels[provider];
|
|
4036
|
-
if (model === null) throw new Error(`Choose a ${providerName(provider)} commit model in Settings → Accounts`);
|
|
4037
4312
|
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}`;
|
|
4038
4313
|
if (provider === "codex") return codexCommitMessage(accountId, cwd, model, prompt);
|
|
4039
4314
|
if (provider === "opencode") return openCodeCommitMessage(cwd, model, prompt);
|
|
@@ -4056,10 +4331,6 @@ async function suggestCommitMessage(provider, accountId, cwd) {
|
|
|
4056
4331
|
}
|
|
4057
4332
|
throw new Error("Claude did not reply");
|
|
4058
4333
|
}
|
|
4059
|
-
async function workingDiff(cwd) {
|
|
4060
|
-
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4061
|
-
return repoDiff(cwd, "");
|
|
4062
|
-
}
|
|
4063
4334
|
async function projectDiff(cwd) {
|
|
4064
4335
|
if (await isRepo(cwd)) return repoDiff(cwd, "");
|
|
4065
4336
|
const repos = childRepos(cwd, "", REPO_DEPTH);
|
|
@@ -4067,7 +4338,7 @@ async function projectDiff(cwd) {
|
|
|
4067
4338
|
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), `${repo}/`)))).join("");
|
|
4068
4339
|
}
|
|
4069
4340
|
function git(cwd, args) {
|
|
4070
|
-
return
|
|
4341
|
+
return run$1("git", args, {
|
|
4071
4342
|
cwd,
|
|
4072
4343
|
maxBuffer: 67108864
|
|
4073
4344
|
});
|
|
@@ -4216,7 +4487,7 @@ async function taskOutput(sessionId, file) {
|
|
|
4216
4487
|
try {
|
|
4217
4488
|
stats = await lstat(file);
|
|
4218
4489
|
} catch (error) {
|
|
4219
|
-
if (error
|
|
4490
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
4220
4491
|
throw error;
|
|
4221
4492
|
}
|
|
4222
4493
|
if (!stats.isFile()) throw new Error("Background task output is not a regular file");
|
|
@@ -4254,73 +4525,80 @@ function walk(root) {
|
|
|
4254
4525
|
}
|
|
4255
4526
|
//#endregion
|
|
4256
4527
|
//#region src/services/push.ts
|
|
4257
|
-
const pushFile = path.join(home, "push.json");
|
|
4258
4528
|
const TITLE_LIMIT = 100;
|
|
4259
4529
|
const BODY_LIMIT = 500;
|
|
4260
4530
|
const TIME_TO_LIVE = 900;
|
|
4261
4531
|
const SEND_TIMEOUT = 1e4;
|
|
4262
4532
|
const SUBJECT = "https://github.com/NazmusSayad/offsite";
|
|
4263
4533
|
let server$1 = "";
|
|
4264
|
-
let
|
|
4534
|
+
let notifier = null;
|
|
4265
4535
|
function startPush(address) {
|
|
4266
4536
|
server$1 = address;
|
|
4267
|
-
try {
|
|
4268
|
-
const store = readJson("push.json");
|
|
4269
|
-
if (store !== null) track(store);
|
|
4270
|
-
} catch (error) {
|
|
4271
|
-
console.warn(`push notifications are off until a browser turns them on again, could not read ${pushFile}: ${error}`);
|
|
4272
|
-
}
|
|
4273
4537
|
}
|
|
4274
4538
|
function pushActive() {
|
|
4275
|
-
return
|
|
4276
|
-
}
|
|
4277
|
-
function
|
|
4278
|
-
return
|
|
4279
|
-
}
|
|
4280
|
-
function
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
auth: subscription.keys.auth
|
|
4289
|
-
},
|
|
4290
|
-
server: server$1
|
|
4291
|
-
});
|
|
4539
|
+
return targets().length > 0;
|
|
4540
|
+
}
|
|
4541
|
+
function notifierClient() {
|
|
4542
|
+
return notifier === null ? null : notifier.client;
|
|
4543
|
+
}
|
|
4544
|
+
function claimNotifier(client, endpoint) {
|
|
4545
|
+
notifier = {
|
|
4546
|
+
client,
|
|
4547
|
+
endpoint
|
|
4548
|
+
};
|
|
4549
|
+
for (const item of clients) item.send({
|
|
4550
|
+
type: "notifier",
|
|
4551
|
+
active: item === client
|
|
4292
4552
|
});
|
|
4553
|
+
}
|
|
4554
|
+
function subscribe(registration, client) {
|
|
4555
|
+
if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4556
|
+
const values = {
|
|
4557
|
+
endpoint: registration.subscription.endpoint,
|
|
4558
|
+
p256dh: registration.subscription.keys.p256dh,
|
|
4559
|
+
auth: registration.subscription.keys.auth,
|
|
4560
|
+
publicKey: registration.publicKey,
|
|
4561
|
+
privateKey: registration.privateKey,
|
|
4562
|
+
machine: registration.machine,
|
|
4563
|
+
server: server$1
|
|
4564
|
+
};
|
|
4565
|
+
db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
|
|
4566
|
+
target: pushSubscriptions.endpoint,
|
|
4567
|
+
set: values
|
|
4568
|
+
}).run();
|
|
4569
|
+
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4293
4570
|
console.log("a browser subscribed to push notifications");
|
|
4294
4571
|
}
|
|
4295
4572
|
function unsubscribe(endpoint) {
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
});
|
|
4573
|
+
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4574
|
+
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4299
4575
|
console.log("a browser unsubscribed from push notifications");
|
|
4300
4576
|
}
|
|
4301
4577
|
async function sendPush(message) {
|
|
4302
|
-
|
|
4303
|
-
try {
|
|
4304
|
-
store = read();
|
|
4305
|
-
} catch (error) {
|
|
4306
|
-
console.error(`could not read push subscriptions: ${error}`);
|
|
4307
|
-
return;
|
|
4308
|
-
}
|
|
4309
|
-
const subscriptions = store.subscriptions.filter(ours);
|
|
4578
|
+
const subscriptions = targets();
|
|
4310
4579
|
if (subscriptions.length === 0) return;
|
|
4311
|
-
const
|
|
4580
|
+
const trimmed = {
|
|
4312
4581
|
...message,
|
|
4313
4582
|
title: [...message.title].slice(0, TITLE_LIMIT).join(""),
|
|
4314
4583
|
body: [...message.body].slice(0, BODY_LIMIT).join("")
|
|
4315
|
-
}
|
|
4584
|
+
};
|
|
4316
4585
|
const gone = [];
|
|
4317
4586
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4318
4587
|
try {
|
|
4319
|
-
await webPush.sendNotification(
|
|
4588
|
+
await webPush.sendNotification({
|
|
4589
|
+
endpoint: subscription.endpoint,
|
|
4590
|
+
keys: {
|
|
4591
|
+
p256dh: subscription.p256dh,
|
|
4592
|
+
auth: subscription.auth
|
|
4593
|
+
}
|
|
4594
|
+
}, JSON.stringify({
|
|
4595
|
+
...trimmed,
|
|
4596
|
+
machineId: subscription.machine
|
|
4597
|
+
}), {
|
|
4320
4598
|
vapidDetails: {
|
|
4321
4599
|
subject: SUBJECT,
|
|
4322
|
-
publicKey:
|
|
4323
|
-
privateKey:
|
|
4600
|
+
publicKey: subscription.publicKey,
|
|
4601
|
+
privateKey: subscription.privateKey
|
|
4324
4602
|
},
|
|
4325
4603
|
TTL: TIME_TO_LIVE,
|
|
4326
4604
|
urgency: "high",
|
|
@@ -4342,59 +4620,11 @@ async function sendPush(message) {
|
|
|
4342
4620
|
}
|
|
4343
4621
|
}));
|
|
4344
4622
|
if (gone.length === 0) return;
|
|
4345
|
-
|
|
4346
|
-
update((current) => {
|
|
4347
|
-
current.subscriptions = current.subscriptions.filter((subscription) => !gone.includes(subscription.endpoint));
|
|
4348
|
-
});
|
|
4349
|
-
} catch (error) {
|
|
4350
|
-
console.error(`could not remove gone push subscriptions: ${error}`);
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
function ours(subscription) {
|
|
4354
|
-
return subscription.server === server$1;
|
|
4623
|
+
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4355
4624
|
}
|
|
4356
|
-
function
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
function load() {
|
|
4360
|
-
let store;
|
|
4361
|
-
try {
|
|
4362
|
-
store = readJson("push.json");
|
|
4363
|
-
} catch (error) {
|
|
4364
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
4365
|
-
return replaceCorrupt(error);
|
|
4366
|
-
}
|
|
4367
|
-
if (store === null) return create();
|
|
4368
|
-
if (typeof store.publicKey !== "string" || typeof store.privateKey !== "string" || !Array.isArray(store.subscriptions)) return replaceCorrupt(/* @__PURE__ */ new Error("it is missing its keys or subscriptions"));
|
|
4369
|
-
return store;
|
|
4370
|
-
}
|
|
4371
|
-
function replaceCorrupt(error) {
|
|
4372
|
-
const aside = `${pushFile}.corrupt-${randomUUID()}`;
|
|
4373
|
-
renameSync(pushFile, aside);
|
|
4374
|
-
console.warn(`${pushFile} is corrupt, moved it to ${aside} and created a new one: ${error}`);
|
|
4375
|
-
return create();
|
|
4376
|
-
}
|
|
4377
|
-
function create() {
|
|
4378
|
-
const store = {
|
|
4379
|
-
...webPush.generateVAPIDKeys(),
|
|
4380
|
-
subscriptions: []
|
|
4381
|
-
};
|
|
4382
|
-
save(store);
|
|
4383
|
-
return store;
|
|
4384
|
-
}
|
|
4385
|
-
function save(store) {
|
|
4386
|
-
writeJson("push.json", store);
|
|
4387
|
-
}
|
|
4388
|
-
function read() {
|
|
4389
|
-
const store = load();
|
|
4390
|
-
track(store);
|
|
4391
|
-
return store;
|
|
4392
|
-
}
|
|
4393
|
-
function update(change) {
|
|
4394
|
-
const store = load();
|
|
4395
|
-
change(store);
|
|
4396
|
-
save(store);
|
|
4397
|
-
track(store);
|
|
4625
|
+
function targets() {
|
|
4626
|
+
if (notifier === null || notifier.endpoint === null) return [];
|
|
4627
|
+
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4398
4628
|
}
|
|
4399
4629
|
//#endregion
|
|
4400
4630
|
//#region src/services/runs.ts
|
|
@@ -4415,9 +4645,10 @@ function rewind(request) {
|
|
|
4415
4645
|
}
|
|
4416
4646
|
async function rewindNow(request) {
|
|
4417
4647
|
const sessionId = request.sessionId;
|
|
4418
|
-
|
|
4648
|
+
const cwd = projectPath(request.projectId);
|
|
4649
|
+
if (runs.get(sessionId)?.running === true) throw new Error(`Stop ${providerName(providerOf(sessionId))} before rewinding`);
|
|
4419
4650
|
if ((runs.get(sessionId)?.backgroundTasks ?? 0) > 0) throw new Error("Wait for background tasks to finish before rewinding");
|
|
4420
|
-
const run = await runFor(sessionId,
|
|
4651
|
+
const run = await runFor(sessionId, cwd, request.settings);
|
|
4421
4652
|
if (run.provider === "codex") {
|
|
4422
4653
|
if (request.code) throw new Error("Codex cannot restore files");
|
|
4423
4654
|
if (request.dryRun || !request.conversation) return null;
|
|
@@ -4430,7 +4661,7 @@ async function rewindNow(request) {
|
|
|
4430
4661
|
const result = await rewindOpenCode(request);
|
|
4431
4662
|
if (request.dryRun) return result;
|
|
4432
4663
|
run.items.clear();
|
|
4433
|
-
publish$2(sessionId, await openCodeHistory(sessionId,
|
|
4664
|
+
publish$2(sessionId, await openCodeHistory(sessionId, cwd));
|
|
4434
4665
|
return result;
|
|
4435
4666
|
}
|
|
4436
4667
|
let result = null;
|
|
@@ -4444,58 +4675,60 @@ async function rewindNow(request) {
|
|
|
4444
4675
|
};
|
|
4445
4676
|
}
|
|
4446
4677
|
if (request.dryRun || !request.conversation) return result;
|
|
4447
|
-
const disk = await getSessionMessages(sessionId, { dir:
|
|
4678
|
+
const disk = await getSessionMessages(sessionId, { dir: cwd });
|
|
4448
4679
|
const index = disk.findIndex((item) => item.uuid === request.messageId);
|
|
4449
4680
|
if (index < 1) throw new Error("Cannot rewind the conversation to that point");
|
|
4450
4681
|
run.stop();
|
|
4451
4682
|
await run.closed;
|
|
4452
4683
|
new Run({
|
|
4453
4684
|
sessionId,
|
|
4454
|
-
cwd
|
|
4685
|
+
cwd,
|
|
4455
4686
|
resume: true,
|
|
4456
4687
|
resumeAt: disk[index - 1].uuid,
|
|
4457
4688
|
settings: request.settings
|
|
4458
4689
|
});
|
|
4459
|
-
publish$2(sessionId, await history(sessionId,
|
|
4690
|
+
publish$2(sessionId, await history(sessionId, cwd));
|
|
4460
4691
|
return result;
|
|
4461
4692
|
}
|
|
4462
|
-
async function open$1(client, sessionId,
|
|
4693
|
+
async function open$1(client, sessionId, projectId) {
|
|
4463
4694
|
client.opening = sessionId;
|
|
4464
|
-
const event = await sessionHistory(sessionId,
|
|
4695
|
+
const event = await sessionHistory(sessionId, projectPath(projectId));
|
|
4465
4696
|
if (client.opening !== sessionId) return;
|
|
4466
4697
|
client.sessionId = sessionId;
|
|
4467
4698
|
client.send(event);
|
|
4468
4699
|
}
|
|
4469
4700
|
async function send$1(client, request) {
|
|
4470
|
-
|
|
4701
|
+
assertEnabled(request.settings.provider, request.settings.accountId);
|
|
4702
|
+
const cwd = projectPath(request.projectId);
|
|
4703
|
+
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
|
|
4471
4704
|
if (request.sessionId === null) {
|
|
4472
|
-
const run = await startRun(
|
|
4705
|
+
const run = await startRun(cwd, request.settings);
|
|
4473
4706
|
const sessionId = run.sessionId;
|
|
4474
4707
|
client.opening = sessionId;
|
|
4475
4708
|
client.sessionId = sessionId;
|
|
4476
4709
|
client.send({
|
|
4477
4710
|
type: "started",
|
|
4478
4711
|
sessionId,
|
|
4479
|
-
|
|
4712
|
+
projectId: request.projectId
|
|
4480
4713
|
});
|
|
4481
4714
|
await run.submit(request.prompt, request.delivery);
|
|
4482
4715
|
recordInteraction(sessionId);
|
|
4483
4716
|
return;
|
|
4484
4717
|
}
|
|
4485
4718
|
const sessionId = request.sessionId;
|
|
4486
|
-
await locked(sessionId, () => sendExisting(client, sessionId, request));
|
|
4719
|
+
await locked(sessionId, () => sendExisting(client, sessionId, cwd, request));
|
|
4487
4720
|
}
|
|
4488
|
-
async function sendExisting(client, sessionId, request) {
|
|
4721
|
+
async function sendExisting(client, sessionId, cwd, request) {
|
|
4489
4722
|
const provider = providerOf(sessionId);
|
|
4490
4723
|
if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
|
|
4491
4724
|
const existing = runs.get(sessionId);
|
|
4492
|
-
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing,
|
|
4725
|
+
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing, cwd) || existing?.provider === "codex" && !existing.running && existing.pending.size === 0 && (listedUpdatedAt("codex", sessionId) ?? 0) > existing.activeAt) {
|
|
4493
4726
|
existing.stop();
|
|
4494
4727
|
await existing.closed;
|
|
4495
4728
|
}
|
|
4496
|
-
const run = await runFor(sessionId,
|
|
4729
|
+
const run = await runFor(sessionId, cwd, request.settings);
|
|
4497
4730
|
if (run === existing) await run.configure(request.settings);
|
|
4498
|
-
else await open$1(client, sessionId, request.
|
|
4731
|
+
else await open$1(client, sessionId, request.projectId);
|
|
4499
4732
|
await run.submit(request.prompt, request.delivery);
|
|
4500
4733
|
recordInteraction(sessionId);
|
|
4501
4734
|
}
|
|
@@ -4639,7 +4872,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
4639
4872
|
}
|
|
4640
4873
|
//#endregion
|
|
4641
4874
|
//#region src/services/terminal/shell-integration.ts
|
|
4642
|
-
const scripts = path.join(serverDir, "src
|
|
4875
|
+
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
4643
4876
|
const folder = path.join(home, "shell-integration");
|
|
4644
4877
|
function prepare(env, command, shell) {
|
|
4645
4878
|
if (shellIntegrationIssue(command, platform(), shell) !== null) return null;
|
|
@@ -4856,7 +5089,7 @@ function launch() {
|
|
|
4856
5089
|
offsite: [
|
|
4857
5090
|
"offsite",
|
|
4858
5091
|
"offsite",
|
|
4859
|
-
|
|
5092
|
+
"0.0.0"
|
|
4860
5093
|
],
|
|
4861
5094
|
ghostty: [
|
|
4862
5095
|
"ghostty",
|
|
@@ -5139,7 +5372,7 @@ async function request(connection, message) {
|
|
|
5139
5372
|
return;
|
|
5140
5373
|
}
|
|
5141
5374
|
case "createTab":
|
|
5142
|
-
await createTab(message.
|
|
5375
|
+
await createTab(message.projectId);
|
|
5143
5376
|
break;
|
|
5144
5377
|
case "closeTab":
|
|
5145
5378
|
closeTab(message.id);
|
|
@@ -5163,17 +5396,17 @@ async function request(connection, message) {
|
|
|
5163
5396
|
break;
|
|
5164
5397
|
}
|
|
5165
5398
|
case "select": {
|
|
5166
|
-
const project = projects.get(message.
|
|
5399
|
+
const project = projects.get(message.projectId) ?? {
|
|
5167
5400
|
tabs: [],
|
|
5168
5401
|
activeTab: null
|
|
5169
5402
|
};
|
|
5170
|
-
projects.set(message.
|
|
5403
|
+
projects.set(message.projectId, project);
|
|
5171
5404
|
const tab = project.tabs.find((item) => item.id === message.tabId);
|
|
5172
5405
|
if (tab !== void 0) {
|
|
5173
5406
|
tab.minimized = false;
|
|
5174
5407
|
project.activeTab = tab.id;
|
|
5175
5408
|
}
|
|
5176
|
-
if (project.tabs.length === 0) await createTab(message.
|
|
5409
|
+
if (project.tabs.length === 0) await createTab(message.projectId);
|
|
5177
5410
|
break;
|
|
5178
5411
|
}
|
|
5179
5412
|
}
|
|
@@ -5206,16 +5439,17 @@ function input(connection, bytes) {
|
|
|
5206
5439
|
}
|
|
5207
5440
|
terminal.pty.write(Buffer.from(frame.data));
|
|
5208
5441
|
}
|
|
5209
|
-
async function createTab(
|
|
5442
|
+
async function createTab(projectId) {
|
|
5443
|
+
const cwd = projectPath(projectId);
|
|
5210
5444
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
5211
5445
|
console.error(`${cwd} is not a folder, terminal not created`);
|
|
5212
5446
|
return;
|
|
5213
5447
|
}
|
|
5214
|
-
const project = projects.get(
|
|
5448
|
+
const project = projects.get(projectId) ?? {
|
|
5215
5449
|
tabs: [],
|
|
5216
5450
|
activeTab: null
|
|
5217
5451
|
};
|
|
5218
|
-
projects.set(
|
|
5452
|
+
projects.set(projectId, project);
|
|
5219
5453
|
const active = project.activeTab === null ? void 0 : terminals.get(project.activeTab);
|
|
5220
5454
|
let directory = cwd;
|
|
5221
5455
|
if (active !== void 0) try {
|
|
@@ -5323,7 +5557,7 @@ function scan(id, terminal, data) {
|
|
|
5323
5557
|
return;
|
|
5324
5558
|
}
|
|
5325
5559
|
const alert = terminal.scanner.scan(data);
|
|
5326
|
-
if (alert === null ||
|
|
5560
|
+
if (alert === null || notifierWatching()) return;
|
|
5327
5561
|
if (!(alert.type === "bell" ? terminal.alerts.startBell() : terminal.alerts.startNotification())) return;
|
|
5328
5562
|
const windowTitle = terminal.scanner.title;
|
|
5329
5563
|
setTimeout(() => push(id, alert, windowTitle), 500);
|
|
@@ -5340,7 +5574,7 @@ function push(id, alert, windowTitle) {
|
|
|
5340
5574
|
title = alert.title;
|
|
5341
5575
|
body = alert.body;
|
|
5342
5576
|
}
|
|
5343
|
-
if (
|
|
5577
|
+
if (notifierWatching()) return;
|
|
5344
5578
|
const entry = [...projects].find(([, project]) => project.tabs.some((tab) => tab.id === id));
|
|
5345
5579
|
if (entry === void 0) return;
|
|
5346
5580
|
const tab = findTab(id);
|
|
@@ -5348,18 +5582,17 @@ function push(id, alert, windowTitle) {
|
|
|
5348
5582
|
sendPush({
|
|
5349
5583
|
title,
|
|
5350
5584
|
body,
|
|
5351
|
-
|
|
5585
|
+
projectId: entry[0],
|
|
5352
5586
|
tabId: id
|
|
5353
5587
|
});
|
|
5354
5588
|
}
|
|
5355
|
-
function
|
|
5356
|
-
|
|
5589
|
+
function notifierWatching() {
|
|
5590
|
+
const client = notifierClient();
|
|
5591
|
+
if (client === null || !connections.has(client)) return false;
|
|
5592
|
+
return Date.now() - client.heardAt < SILENCE_LIMIT;
|
|
5357
5593
|
}
|
|
5358
5594
|
function findTab(id) {
|
|
5359
|
-
|
|
5360
|
-
const tab = project.tabs.find((item) => item.id === id);
|
|
5361
|
-
if (tab !== void 0) return tab;
|
|
5362
|
-
}
|
|
5595
|
+
return [...projects.values()].flatMap((project) => project.tabs).find((tab) => tab.id === id);
|
|
5363
5596
|
}
|
|
5364
5597
|
function nearestVisible(tabs, index) {
|
|
5365
5598
|
for (let position = index; position < tabs.length; position++) if (!tabs[position].minimized) return position;
|
|
@@ -5388,33 +5621,41 @@ function send(connection, event) {
|
|
|
5388
5621
|
//#region src/api/socket.ts
|
|
5389
5622
|
const handlers = {
|
|
5390
5623
|
ping: () => null,
|
|
5391
|
-
sync:
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5624
|
+
sync: () => {
|
|
5625
|
+
refreshAll();
|
|
5626
|
+
return {
|
|
5627
|
+
...configInfo(),
|
|
5628
|
+
hostname: os.hostname(),
|
|
5629
|
+
projects: projects$1(),
|
|
5630
|
+
sessions: sessions(),
|
|
5631
|
+
providers: providerStatuses(),
|
|
5632
|
+
activity: activity()
|
|
5633
|
+
};
|
|
5634
|
+
},
|
|
5399
5635
|
"config.get": () => configInfo(),
|
|
5400
|
-
"config.save": (params) => {
|
|
5401
|
-
const before = readConfig()
|
|
5636
|
+
"config.save": async (params) => {
|
|
5637
|
+
const before = readConfig();
|
|
5402
5638
|
const saved = saveConfig(params);
|
|
5403
|
-
|
|
5639
|
+
for (const provider of [
|
|
5640
|
+
"claude",
|
|
5641
|
+
"codex",
|
|
5642
|
+
"opencode"
|
|
5643
|
+
]) if (before.enabledProviders[provider] && !saved.enabledProviders[provider]) await stopProvider(provider);
|
|
5644
|
+
if (saved.claudeBaseDir === before.claudeBaseDir && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
|
|
5404
5645
|
watchSessions();
|
|
5405
|
-
|
|
5646
|
+
refreshAll();
|
|
5406
5647
|
publishAccounts();
|
|
5407
5648
|
return saved;
|
|
5408
5649
|
},
|
|
5409
5650
|
"projects.add": async (params) => {
|
|
5410
5651
|
const project = await addProject(params.path);
|
|
5411
|
-
|
|
5652
|
+
refreshAll();
|
|
5412
5653
|
return project;
|
|
5413
5654
|
},
|
|
5414
|
-
"projects.update": (params) => updateProject(params.
|
|
5655
|
+
"projects.update": (params) => updateProject(params.id, params.patch),
|
|
5415
5656
|
"projects.remove": (params) => {
|
|
5416
|
-
removeProject(params.
|
|
5417
|
-
|
|
5657
|
+
removeProject(params.id);
|
|
5658
|
+
refreshAll();
|
|
5418
5659
|
return null;
|
|
5419
5660
|
},
|
|
5420
5661
|
"directories.list": (params) => directories(params.path),
|
|
@@ -5435,14 +5676,14 @@ const handlers = {
|
|
|
5435
5676
|
return null;
|
|
5436
5677
|
},
|
|
5437
5678
|
"sessions.open": async (params, client) => {
|
|
5438
|
-
await open$1(client, params.sessionId, params.
|
|
5679
|
+
await open$1(client, params.sessionId, params.projectId);
|
|
5439
5680
|
return null;
|
|
5440
5681
|
},
|
|
5441
5682
|
"sessions.earlier": (params) => earlier(params.sessionId, params.before),
|
|
5442
5683
|
"sessions.image": (params) => image(params.sessionId, params.itemId, params.index),
|
|
5443
5684
|
"sessions.agent": (params) => {
|
|
5444
5685
|
switch (providerOf(params.sessionId)) {
|
|
5445
|
-
case "claude": return agentMessages(params.sessionId, params.
|
|
5686
|
+
case "claude": return agentMessages(params.sessionId, projectPath(params.projectId), params.agentId, params.parent);
|
|
5446
5687
|
case "codex": return codexAgent(params.agentId, params.parent);
|
|
5447
5688
|
case "opencode": return openCodeAgent(params.agentId, params.parent);
|
|
5448
5689
|
}
|
|
@@ -5480,7 +5721,7 @@ const handlers = {
|
|
|
5480
5721
|
},
|
|
5481
5722
|
"sessions.rewind": (params) => rewind(params),
|
|
5482
5723
|
"sessions.fork": async (params) => {
|
|
5483
|
-
const result = await fork(params.sessionId, params.
|
|
5724
|
+
const result = await fork(params.sessionId, params.projectId, params.messageId);
|
|
5484
5725
|
publishSessions();
|
|
5485
5726
|
return result;
|
|
5486
5727
|
},
|
|
@@ -5490,8 +5731,8 @@ const handlers = {
|
|
|
5490
5731
|
publishAccounts();
|
|
5491
5732
|
return null;
|
|
5492
5733
|
},
|
|
5493
|
-
"accounts.update": (params) => {
|
|
5494
|
-
updateAccount(params
|
|
5734
|
+
"accounts.update": async (params) => {
|
|
5735
|
+
await updateAccount(params);
|
|
5495
5736
|
publishAccounts();
|
|
5496
5737
|
return null;
|
|
5497
5738
|
},
|
|
@@ -5500,26 +5741,29 @@ const handlers = {
|
|
|
5500
5741
|
publishAccounts();
|
|
5501
5742
|
return null;
|
|
5502
5743
|
},
|
|
5503
|
-
capabilities: (params) => capabilities(params.accountId, params.
|
|
5504
|
-
"files.list": (params) => listFiles(params.
|
|
5505
|
-
"files.read": (params) => readProjectFile(params.
|
|
5744
|
+
capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
|
|
5745
|
+
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
5746
|
+
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
5506
5747
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
5507
5748
|
"tasks.output": (params) => taskOutput(params.sessionId, params.path),
|
|
5508
|
-
"git.status": (params) => gitStatus(params.
|
|
5509
|
-
"git.diff": (params) => projectDiff(params.
|
|
5510
|
-
"git.commit": (params) => commit(params.
|
|
5511
|
-
"git.push": (params) => push$1(params.
|
|
5512
|
-
"git.pullRequest": (params) => createPullRequest(params.
|
|
5513
|
-
"git.message": (params) => suggestCommitMessage(params.
|
|
5514
|
-
"push.
|
|
5515
|
-
|
|
5516
|
-
subscribe(params);
|
|
5749
|
+
"git.status": (params) => gitStatus(projectPath(params.projectId)),
|
|
5750
|
+
"git.diff": (params) => projectDiff(projectPath(params.projectId)),
|
|
5751
|
+
"git.commit": (params) => commit(projectPath(params.projectId), params.message),
|
|
5752
|
+
"git.push": (params) => push$1(projectPath(params.projectId)),
|
|
5753
|
+
"git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
|
|
5754
|
+
"git.message": (params) => suggestCommitMessage(projectPath(params.projectId)),
|
|
5755
|
+
"push.subscribe": (params, client) => {
|
|
5756
|
+
subscribe(params, client);
|
|
5517
5757
|
return null;
|
|
5518
5758
|
},
|
|
5519
5759
|
"push.unsubscribe": (params) => {
|
|
5520
5760
|
unsubscribe(params.endpoint);
|
|
5521
5761
|
return null;
|
|
5522
5762
|
},
|
|
5763
|
+
"notifications.claim": (params, client) => {
|
|
5764
|
+
claimNotifier(client, params.endpoint);
|
|
5765
|
+
return null;
|
|
5766
|
+
},
|
|
5523
5767
|
terminal: async (params, client) => {
|
|
5524
5768
|
await terminalRequest(client, params);
|
|
5525
5769
|
return null;
|
|
@@ -5579,40 +5823,32 @@ async function answer(client, text) {
|
|
|
5579
5823
|
//#endregion
|
|
5580
5824
|
//#region src/storage/token.ts
|
|
5581
5825
|
function readTokens() {
|
|
5582
|
-
|
|
5583
|
-
return stored === null ? [] : stored.tokens;
|
|
5826
|
+
return db.select().from(tokens).orderBy(tokens.createdAt).all();
|
|
5584
5827
|
}
|
|
5585
5828
|
function addToken(name) {
|
|
5586
5829
|
const token = randomBytes(32).toString("base64url");
|
|
5587
|
-
|
|
5588
|
-
token,
|
|
5830
|
+
db.insert(tokens).values({
|
|
5831
|
+
hash: hashToken(token),
|
|
5589
5832
|
name,
|
|
5590
5833
|
createdAt: Date.now()
|
|
5591
|
-
}
|
|
5834
|
+
}).run();
|
|
5592
5835
|
return token;
|
|
5593
5836
|
}
|
|
5594
|
-
function removeTokens(
|
|
5595
|
-
|
|
5837
|
+
function removeTokens(hashes) {
|
|
5838
|
+
db.delete(tokens).where(inArray(tokens.hash, hashes)).run();
|
|
5596
5839
|
}
|
|
5597
|
-
function
|
|
5598
|
-
return
|
|
5840
|
+
function hasTokens() {
|
|
5841
|
+
return db.select().from(tokens).limit(1).get() !== void 0;
|
|
5599
5842
|
}
|
|
5600
5843
|
function matchesToken(given) {
|
|
5601
5844
|
if (given === null) return false;
|
|
5602
|
-
|
|
5603
|
-
return readTokens().some((item) => {
|
|
5604
|
-
const expected = Buffer.from(item.token);
|
|
5605
|
-
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
5606
|
-
});
|
|
5607
|
-
}
|
|
5608
|
-
function readUsage() {
|
|
5609
|
-
return readJson("token-usage.json") ?? {};
|
|
5845
|
+
return db.select().from(tokens).where(eq(tokens.hash, hashToken(given))).get() !== void 0;
|
|
5610
5846
|
}
|
|
5611
5847
|
function recordUse(token) {
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5848
|
+
db.update(tokens).set({ lastUsedAt: Date.now() }).where(eq(tokens.hash, hashToken(token))).run();
|
|
5849
|
+
}
|
|
5850
|
+
function hashToken(token) {
|
|
5851
|
+
return createHash("sha256").update(token).digest("hex");
|
|
5616
5852
|
}
|
|
5617
5853
|
//#endregion
|
|
5618
5854
|
//#region src/main.ts
|
|
@@ -5637,12 +5873,7 @@ const address = `http://${args.host}:${port}`;
|
|
|
5637
5873
|
const fallback = args.fallback === void 0 ? null : readFallback(args.fallback);
|
|
5638
5874
|
if (args.command === "pair") {
|
|
5639
5875
|
const links = pairingLinks(addToken(args.name));
|
|
5640
|
-
console.log(
|
|
5641
|
-
console.log("");
|
|
5642
|
-
console.log(renderUnicodeCompact(links.hosted, { border: 2 }));
|
|
5643
|
-
console.log("");
|
|
5644
|
-
console.log("Or open one of these links in the browser to pair:");
|
|
5645
|
-
console.log(` ${links.hosted}`);
|
|
5876
|
+
console.log("Open this link in the browser to pair:");
|
|
5646
5877
|
console.log(` ${links.local}`);
|
|
5647
5878
|
if (fallback !== null) console.log(`Fallback: ${fallback}`);
|
|
5648
5879
|
process.exit(0);
|
|
@@ -5654,27 +5885,18 @@ if (args.command === "unpair") {
|
|
|
5654
5885
|
lockHome();
|
|
5655
5886
|
process.env.CLAUDE_CONFIG_DIR = readConfig().claudeBaseDir;
|
|
5656
5887
|
watchSessions();
|
|
5888
|
+
refreshAll();
|
|
5657
5889
|
if (findClaude() === null) {
|
|
5658
5890
|
console.error("could not find claude on PATH; install Claude Code first");
|
|
5659
5891
|
process.exit(1);
|
|
5660
5892
|
}
|
|
5661
5893
|
const publicDir = path.join(import.meta.dirname, "..", "..", "web", "dist");
|
|
5662
|
-
const token = args.auth && args.pair ?
|
|
5894
|
+
const token = args.auth && args.pair && !hasTokens() ? addToken(null) : null;
|
|
5663
5895
|
const server = createServer((request, response) => {
|
|
5664
5896
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5665
|
-
if (url.pathname === "/version") {
|
|
5666
|
-
response.setHeader("access-control-allow-origin", "*");
|
|
5667
|
-
response.setHeader("content-type", "application/json");
|
|
5668
|
-
response.end(JSON.stringify({
|
|
5669
|
-
version,
|
|
5670
|
-
protocol: 2,
|
|
5671
|
-
authorized: authorized(url)
|
|
5672
|
-
}));
|
|
5673
|
-
return;
|
|
5674
|
-
}
|
|
5675
5897
|
if (args.command === "serve") {
|
|
5676
5898
|
response.statusCode = 404;
|
|
5677
|
-
response.end(
|
|
5899
|
+
response.end("offsite serve has no web app");
|
|
5678
5900
|
return;
|
|
5679
5901
|
}
|
|
5680
5902
|
serveFile(url.pathname, response);
|
|
@@ -5684,7 +5906,7 @@ const paired = /* @__PURE__ */ new Map();
|
|
|
5684
5906
|
server.on("upgrade", (request, socket, head) => {
|
|
5685
5907
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5686
5908
|
const given = url.searchParams.get("token");
|
|
5687
|
-
if (url.pathname !== "/ws" ||
|
|
5909
|
+
if (url.pathname !== "/ws" || !authorized(url)) {
|
|
5688
5910
|
socket.destroy();
|
|
5689
5911
|
return;
|
|
5690
5912
|
}
|
|
@@ -5701,13 +5923,12 @@ if (args.auth) setInterval(() => {
|
|
|
5701
5923
|
}, UNPAIR_CHECK);
|
|
5702
5924
|
server.listen(port, args.host, () => {
|
|
5703
5925
|
startPush(`${args.host}:${port}`);
|
|
5704
|
-
if (args.pair) {
|
|
5926
|
+
if (args.pair && (token !== null || !args.auth)) {
|
|
5705
5927
|
const links = pairingLinks(token);
|
|
5706
5928
|
if (args.command === "web") console.log(`offsite is running at ${links.local}`);
|
|
5707
|
-
console.log(`Open it on ${HOSTED_APP}: ${links.hosted}`);
|
|
5708
5929
|
} else {
|
|
5709
5930
|
console.log(`offsite is running at ${address}`);
|
|
5710
|
-
console.log(
|
|
5931
|
+
if (args.pair) console.log("Run offsite pair to pair another browser");
|
|
5711
5932
|
}
|
|
5712
5933
|
if (!args.auth) console.warn("auth is off; anyone who can reach this port can control this machine");
|
|
5713
5934
|
});
|
|
@@ -5730,23 +5951,18 @@ async function unpair() {
|
|
|
5730
5951
|
console.error("offsite unpair needs an interactive terminal");
|
|
5731
5952
|
process.exit(1);
|
|
5732
5953
|
}
|
|
5733
|
-
const usage = readUsage();
|
|
5734
5954
|
const picked = await consola.prompt("Which browsers should be unpaired?", {
|
|
5735
5955
|
type: "multiselect",
|
|
5736
5956
|
required: false,
|
|
5737
5957
|
cancel: "null",
|
|
5738
|
-
options: tokens.map((item) => {
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
used === void 0 ? "never used" : `last used ${new Date(used).toLocaleString()}`
|
|
5747
|
-
].join(" · ")
|
|
5748
|
-
};
|
|
5749
|
-
})
|
|
5958
|
+
options: tokens.map((item) => ({
|
|
5959
|
+
value: item.hash,
|
|
5960
|
+
label: [
|
|
5961
|
+
item.name ?? "Unnamed",
|
|
5962
|
+
`paired ${new Date(item.createdAt).toLocaleString()}`,
|
|
5963
|
+
item.lastUsedAt === null ? "never used" : `last used ${new Date(item.lastUsedAt).toLocaleString()}`
|
|
5964
|
+
].join(" · ")
|
|
5965
|
+
}))
|
|
5750
5966
|
});
|
|
5751
5967
|
if (picked === null || picked.length === 0) {
|
|
5752
5968
|
console.log("Nothing unpaired");
|
|
@@ -5770,15 +5986,10 @@ function readFallback(link) {
|
|
|
5770
5986
|
return url.origin;
|
|
5771
5987
|
}
|
|
5772
5988
|
function pairingLinks(token) {
|
|
5773
|
-
|
|
5989
|
+
return { local: `${address}/pair#${new URLSearchParams({
|
|
5774
5990
|
machine: address,
|
|
5775
|
-
...fallback !== null && { fallback },
|
|
5776
5991
|
...token !== null && { token }
|
|
5777
|
-
});
|
|
5778
|
-
return {
|
|
5779
|
-
local: token === null ? address : `${address}/pair#${new URLSearchParams({ token })}`,
|
|
5780
|
-
hosted: `${HOSTED_APP}/pair#${hosted}`
|
|
5781
|
-
};
|
|
5992
|
+
})}` };
|
|
5782
5993
|
}
|
|
5783
5994
|
function serveFile(pathname, response) {
|
|
5784
5995
|
const requested = path.join(publicDir, path.normalize(pathname));
|