offsite 0.0.10 → 0.0.12
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 +24 -24
- package/apps/server/dist/main.mjs +2035 -1875
- package/apps/server/dist/main.mjs.map +1 -1
- 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/server/drizzle/20261017120143_device_tokens/migration.sql +15 -0
- package/apps/server/drizzle/20261017120143_device_tokens/snapshot.json +305 -0
- package/apps/web/dist/apple-touch-icon.png +0 -0
- package/apps/web/dist/assets/{angular-html-BEUtPJ31.js → angular-html-DNGrmdhg.js} +1 -1
- package/apps/web/dist/assets/{angular-ts-Bnr4rzFr.js → angular-ts-D4hBqsOh.js} +1 -1
- package/apps/web/dist/assets/{apl-B-JNBTdP.js → apl-CYvdPOPE.js} +1 -1
- package/apps/web/dist/assets/{astro-DLjK95IW.js → astro-BqtVnRyx.js} +1 -1
- package/apps/web/dist/assets/{blade-PLMCMBeb.js → blade-BPJV2sdT.js} +1 -1
- package/apps/web/dist/assets/{c-D5z8xNe6.js → c-CBQDE6Uf.js} +1 -1
- package/apps/web/dist/assets/{chapel-B9y2HxI_.js → chapel-BHOESU2p.js} +1 -1
- package/apps/web/dist/assets/{cobol-BSwRu2X7.js → cobol-CMpvzrel.js} +1 -1
- package/apps/web/dist/assets/{coffee-BAWFR6iF.js → coffee-Bv27C06e.js} +1 -1
- package/apps/web/dist/assets/{cpp-CO-vZI4m.js → cpp-MTwxWDTP.js} +1 -1
- package/apps/web/dist/assets/{crystal-BSfyPt6R.js → crystal-CI5fSUCy.js} +1 -1
- package/apps/web/dist/assets/{css-BOzKs1dt.js → css-BW8DHcmj.js} +1 -1
- package/apps/web/dist/assets/{edge-OBqkt95y.js → edge-C2HOl06F.js} +1 -1
- package/apps/web/dist/assets/{elixir-B14D2jMM.js → elixir-OV4G1_r7.js} +1 -1
- package/apps/web/dist/assets/{elm-DgfIayFq.js → elm-BF0pK-yg.js} +1 -1
- package/apps/web/dist/assets/{erb-w-PUv7ic.js → erb-DFQCtOmK.js} +1 -1
- package/apps/web/dist/assets/{git-rebase-DnzmyV_3.js → git-rebase-C9nAAu5A.js} +1 -1
- package/apps/web/dist/assets/{glimmer-js-CLephpyX.js → glimmer-js-B9gnYdT1.js} +1 -1
- package/apps/web/dist/assets/{glimmer-ts-CvphZOog.js → glimmer-ts-BkG38Inm.js} +1 -1
- package/apps/web/dist/assets/{glsl-Ddj2GHKO.js → glsl-wwo6dKBr.js} +1 -1
- package/apps/web/dist/assets/{graphql-CeGXMmoK.js → graphql-tQJRbYuy.js} +1 -1
- package/apps/web/dist/assets/{hack-CtBKZ5VW.js → hack-C9e9oyUG.js} +1 -1
- package/apps/web/dist/assets/{haml-DVvVd-Mp.js → haml-CTolMb7J.js} +1 -1
- package/apps/web/dist/assets/{handlebars-Blx6Nluh.js → handlebars-CrJTM5vD.js} +1 -1
- package/apps/web/dist/assets/{html-O-gZ5M1-.js → html-Be6zOrn3.js} +1 -1
- package/apps/web/dist/assets/{html-derivative-BsDe9c3P.js → html-derivative-10Bvy_Et.js} +1 -1
- package/apps/web/dist/assets/{http-CAUZeWmG.js → http-GZlGWwa-.js} +1 -1
- package/apps/web/dist/assets/{hurl-CYnRsJrb.js → hurl-DEYfhn1R.js} +1 -1
- package/apps/web/dist/assets/index-6ofGnhX4.css +2 -0
- package/apps/web/dist/assets/index-CiQ326rG.js +1203 -0
- package/apps/web/dist/assets/{java-DjjpMT1c.js → java-BsFb26JS.js} +1 -1
- package/apps/web/dist/assets/{javascript-BtiswSWS.js → javascript-DDLm5bzW.js} +1 -1
- package/apps/web/dist/assets/{jinja-ZQGJY58d.js → jinja-NBeNjUXf.js} +1 -1
- package/apps/web/dist/assets/{jison-VPkdeMFr.js → jison-DKz4RK3A.js} +1 -1
- package/apps/web/dist/assets/{json-D5BAJIHo.js → json-Bg4r8xCd.js} +1 -1
- package/apps/web/dist/assets/{jsx-DMEKVcfT.js → jsx-BMnPLKsa.js} +1 -1
- package/apps/web/dist/assets/{julia-BmOLQFch.js → julia-Wll5K4al.js} +1 -1
- package/apps/web/dist/assets/{just-YZGutsxo.js → just-BLn1iI69.js} +1 -1
- package/apps/web/dist/assets/{latex-CvrF79xl.js → latex-D15HRF2E.js} +1 -1
- package/apps/web/dist/assets/{liquid-B--WbaSh.js → liquid-COHkGI02.js} +1 -1
- package/apps/web/dist/assets/{lua-BNEqLlv-.js → lua-CkbAAxOH.js} +1 -1
- package/apps/web/dist/assets/{marko-jEzbzXvj.js → marko-Dmgo6IU5.js} +1 -1
- package/apps/web/dist/assets/{mdc-yfiYVuXh.js → mdc-D_OV_cCt.js} +1 -1
- package/apps/web/dist/assets/{nginx-j7vnr3lB.js → nginx-ADn2lIMW.js} +1 -1
- package/apps/web/dist/assets/{nim-CV2OP2RC.js → nim-C6zTq4FI.js} +1 -1
- package/apps/web/dist/assets/{org-DTTS5Tv_.js → org-D9XGvcFV.js} +1 -1
- package/apps/web/dist/assets/{perl-DuF7yMdF.js → perl-B7-K-xLH.js} +1 -1
- package/apps/web/dist/assets/{php-8ufnwSAo.js → php-CO9ZzQRp.js} +1 -1
- package/apps/web/dist/assets/{pug-D-_t9dIn.js → pug-BaAyPwTO.js} +1 -1
- package/apps/web/dist/assets/{qml-DFxD5uAC.js → qml-Cp3pyCa-.js} +1 -1
- package/apps/web/dist/assets/{r-D8-Yi2KS.js → r-CT05_vfW.js} +1 -1
- package/apps/web/dist/assets/{razor-DhBq7-pb.js → razor-D5rHsTSp.js} +1 -1
- package/apps/web/dist/assets/{regexp-C20CUsFr.js → regexp-D6kzd_X5.js} +1 -1
- package/apps/web/dist/assets/{rst-DsIaukoD.js → rst-AkFwrhnR.js} +1 -1
- package/apps/web/dist/assets/{ruby-C__EgK1w.js → ruby-LNJDVfMG.js} +1 -1
- package/apps/web/dist/assets/{sas-BdDDRZUF.js → sas-C8o3xi__.js} +1 -1
- package/apps/web/dist/assets/{scss-nPFPh8nF.js → scss-Ch7Tact0.js} +1 -1
- package/apps/web/dist/assets/{shellscript-Cfn0DgiQ.js → shellscript-CN-vf6fZ.js} +1 -1
- package/apps/web/dist/assets/{shellsession-DFGAcvZ4.js → shellsession-CxId-awN.js} +1 -1
- package/apps/web/dist/assets/{soy-8DrmW46F.js → soy-PXtFJtGw.js} +1 -1
- package/apps/web/dist/assets/{sql-BeU26hky.js → sql-B7js2_Lu.js} +1 -1
- package/apps/web/dist/assets/{stata-OFfE-Kh9.js → stata-C0Zwsf36.js} +1 -1
- package/apps/web/dist/assets/{surrealql-Bwt4eVgP.js → surrealql-DawzxLLe.js} +1 -1
- package/apps/web/dist/assets/{svelte-DF7N9uBp.js → svelte-CbCoXemF.js} +1 -1
- package/apps/web/dist/assets/{templ-CEirsWl9.js → templ-CQyn0W4C.js} +1 -1
- package/apps/web/dist/assets/{tex-BvBhDkV4.js → tex-FeZKQwyK.js} +1 -1
- package/apps/web/dist/assets/{ts-tags-Dx8bn0ts.js → ts-tags-KsJvoinT.js} +1 -1
- package/apps/web/dist/assets/{tsx-vXk5Q55O.js → tsx-Cw6pGPeZ.js} +1 -1
- package/apps/web/dist/assets/{twig-CIgLDHIW.js → twig-Cj9MLPms.js} +1 -1
- package/apps/web/dist/assets/{typescript-CCW6p4Fc.js → typescript-DwZR66Da.js} +1 -1
- package/apps/web/dist/assets/{typst-CQL-qDz3.js → typst--K-bHYFW.js} +1 -1
- package/apps/web/dist/assets/{vue-gFAUYOUp.js → vue-BrB0TrD_.js} +1 -1
- package/apps/web/dist/assets/{vue-html-BHuiCeAo.js → vue-html-BgNSGBvl.js} +1 -1
- package/apps/web/dist/assets/{vue-vine-DxeD8nHa.js → vue-vine-C2qLo1pd.js} +1 -1
- package/apps/web/dist/assets/{xml-B43ZEHE5.js → xml-DzdlZG_g.js} +1 -1
- package/apps/web/dist/assets/{xsl-icvYLDXv.js → xsl-D3HxAJEY.js} +1 -1
- package/apps/web/dist/assets/{yaml-Bj0dlmrV.js → yaml-SX7TQG-a.js} +1 -1
- package/apps/web/dist/icon-192.png +0 -0
- package/apps/web/dist/icon-512.png +0 -0
- package/apps/web/dist/icon-maskable-512.png +0 -0
- package/apps/web/dist/icon.svg +4 -2
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/manifest.webmanifest +1 -0
- package/apps/web/dist/sw.js +1 -1
- package/package.json +4 -3
- package/apps/web/dist/assets/index-5vPpgW8A.css +0 -2
- package/apps/web/dist/assets/index-CGSSO_em.js +0 -1152
|
@@ -1,29 +1,29 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
1
2
|
import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { consola } from "consola";
|
|
5
|
-
import { renderUnicodeCompact } from "uqr";
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
9
|
-
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";
|
|
16
|
+
import { and, eq, gt, inArray, lte, sql } from "drizzle-orm";
|
|
17
|
+
import { sumBy } from "es-toolkit";
|
|
17
18
|
import { DatabaseSync } from "node:sqlite";
|
|
18
19
|
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
19
20
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
20
|
-
import webPush from "web-push";
|
|
21
21
|
import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
22
22
|
import { lstat, open, readFile, readdir, readlink, stat } from "node:fs/promises";
|
|
23
23
|
import { promisify } from "node:util";
|
|
24
|
+
import webPush from "web-push";
|
|
24
25
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
25
26
|
//#region ../core/src/protocol.ts
|
|
26
|
-
const HOSTED_APP = "https://offsite.sayad.dev";
|
|
27
27
|
function encodeFrame(kind, id, data) {
|
|
28
28
|
const frame = new Uint8Array(5 + data.length);
|
|
29
29
|
frame[0] = kind;
|
|
@@ -40,6 +40,27 @@ function decodeFrame(frame) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
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
|
|
43
64
|
//#region src/lib/hub.ts
|
|
44
65
|
const clients = /* @__PURE__ */ new Set();
|
|
45
66
|
function publish$2(sessionId, event) {
|
|
@@ -49,281 +70,6 @@ function broadcast(event) {
|
|
|
49
70
|
for (const client of clients) client.send(event);
|
|
50
71
|
}
|
|
51
72
|
//#endregion
|
|
52
|
-
//#region src/args.ts
|
|
53
|
-
let command = "web";
|
|
54
|
-
let name = null;
|
|
55
|
-
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");
|
|
56
|
-
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
57
|
-
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
58
|
-
command = "serve";
|
|
59
|
-
});
|
|
60
|
-
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) => {
|
|
61
|
-
command = "pair";
|
|
62
|
-
name = options.name ?? null;
|
|
63
|
-
});
|
|
64
|
-
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
65
|
-
command = "unpair";
|
|
66
|
-
});
|
|
67
|
-
program.parse();
|
|
68
|
-
const options = program.opts();
|
|
69
|
-
const args = {
|
|
70
|
-
...options,
|
|
71
|
-
dir: path.resolve(options.dir),
|
|
72
|
-
command,
|
|
73
|
-
name
|
|
74
|
-
};
|
|
75
|
-
const serverDir = path.join(import.meta.dirname, "..");
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/storage/storage.ts
|
|
78
|
-
const home = args.dir;
|
|
79
|
-
const lockFile = path.join(home, "server.pid");
|
|
80
|
-
mkdirSync(home, { recursive: true });
|
|
81
|
-
function lockHome() {
|
|
82
|
-
lock();
|
|
83
|
-
process.on("exit", () => unlinkSync(lockFile));
|
|
84
|
-
}
|
|
85
|
-
function lock() {
|
|
86
|
-
try {
|
|
87
|
-
writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
|
|
88
|
-
return;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (!hasCode(error, "EEXIST")) throw error;
|
|
91
|
-
}
|
|
92
|
-
let content;
|
|
93
|
-
let modified;
|
|
94
|
-
try {
|
|
95
|
-
content = readFileSync(lockFile, "utf8");
|
|
96
|
-
modified = statSync(lockFile).mtimeMs;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (hasCode(error, "ENOENT")) return lock();
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
const pid = Number(content);
|
|
102
|
-
const validPid = Number.isInteger(pid) && pid > 0;
|
|
103
|
-
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
104
|
-
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
105
|
-
consola.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
unlinkSync(lockFile);
|
|
109
|
-
lock();
|
|
110
|
-
}
|
|
111
|
-
function isRunning(pid) {
|
|
112
|
-
try {
|
|
113
|
-
process.kill(pid, 0);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (hasCode(error, "ESRCH")) return false;
|
|
117
|
-
if (hasCode(error, "EPERM")) return true;
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function hasCode(error, code) {
|
|
122
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
123
|
-
}
|
|
124
|
-
function readJson(name) {
|
|
125
|
-
try {
|
|
126
|
-
return JSON.parse(readFileSync(path.join(home, name), "utf8"));
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (error.code === "ENOENT") return null;
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
function writeJson(name, value) {
|
|
133
|
-
const file = path.join(home, name);
|
|
134
|
-
mkdirSync(home, { recursive: true });
|
|
135
|
-
writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
|
|
136
|
-
renameSync(`${file}.tmp`, file);
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/storage/config.ts
|
|
140
|
-
const version = "0.0.0";
|
|
141
|
-
const configFile = path.join(home, "config.json");
|
|
142
|
-
const defaults = {
|
|
143
|
-
claudeBaseDir: path.join(os.homedir(), ".claude"),
|
|
144
|
-
composer: {
|
|
145
|
-
provider: "claude",
|
|
146
|
-
claude: {
|
|
147
|
-
accountId: null,
|
|
148
|
-
model: "haiku",
|
|
149
|
-
effort: "low",
|
|
150
|
-
mode: "bypassPermissions",
|
|
151
|
-
plan: false
|
|
152
|
-
},
|
|
153
|
-
codex: {
|
|
154
|
-
accountId: null,
|
|
155
|
-
model: null,
|
|
156
|
-
effort: null,
|
|
157
|
-
mode: "auto",
|
|
158
|
-
plan: false
|
|
159
|
-
},
|
|
160
|
-
opencode: {
|
|
161
|
-
accountId: "opencode",
|
|
162
|
-
model: null,
|
|
163
|
-
effort: null,
|
|
164
|
-
mode: null,
|
|
165
|
-
plan: false
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
commitModels: {
|
|
169
|
-
claude: "haiku",
|
|
170
|
-
codex: "gpt-6-luna",
|
|
171
|
-
opencode: null
|
|
172
|
-
},
|
|
173
|
-
terminal: {
|
|
174
|
-
shell: {
|
|
175
|
-
macos: null,
|
|
176
|
-
linux: null,
|
|
177
|
-
windows: null
|
|
178
|
-
},
|
|
179
|
-
behavior: {
|
|
180
|
-
terminalIdentity: "offsite",
|
|
181
|
-
shellIntegration: true
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
const choices = {
|
|
186
|
-
"composer.provider": [
|
|
187
|
-
"claude",
|
|
188
|
-
"codex",
|
|
189
|
-
"opencode"
|
|
190
|
-
],
|
|
191
|
-
"composer.claude.effort": [
|
|
192
|
-
"low",
|
|
193
|
-
"medium",
|
|
194
|
-
"high",
|
|
195
|
-
"xhigh",
|
|
196
|
-
"max"
|
|
197
|
-
],
|
|
198
|
-
"composer.claude.mode": [
|
|
199
|
-
"default",
|
|
200
|
-
"acceptEdits",
|
|
201
|
-
"bypassPermissions",
|
|
202
|
-
"plan",
|
|
203
|
-
"dontAsk",
|
|
204
|
-
"auto"
|
|
205
|
-
],
|
|
206
|
-
"composer.codex.mode": [
|
|
207
|
-
"read-only",
|
|
208
|
-
"auto",
|
|
209
|
-
"full-access"
|
|
210
|
-
],
|
|
211
|
-
"terminal.behavior.terminalIdentity": [
|
|
212
|
-
"offsite",
|
|
213
|
-
"ghostty",
|
|
214
|
-
"iterm2"
|
|
215
|
-
]
|
|
216
|
-
};
|
|
217
|
-
const nullable = [
|
|
218
|
-
"composer.claude.accountId",
|
|
219
|
-
"composer.claude.model",
|
|
220
|
-
"composer.claude.effort",
|
|
221
|
-
"composer.claude.mode",
|
|
222
|
-
"composer.codex.accountId",
|
|
223
|
-
"composer.codex.model",
|
|
224
|
-
"composer.codex.effort",
|
|
225
|
-
"composer.codex.mode",
|
|
226
|
-
"composer.opencode.accountId",
|
|
227
|
-
"composer.opencode.model",
|
|
228
|
-
"composer.opencode.effort",
|
|
229
|
-
"composer.opencode.mode",
|
|
230
|
-
"commitModels.claude",
|
|
231
|
-
"commitModels.codex",
|
|
232
|
-
"commitModels.opencode",
|
|
233
|
-
"terminal.shell.macos",
|
|
234
|
-
"terminal.shell.linux",
|
|
235
|
-
"terminal.shell.windows"
|
|
236
|
-
];
|
|
237
|
-
function platform() {
|
|
238
|
-
if (process.platform === "darwin") return "macos";
|
|
239
|
-
if (process.platform === "linux") return "linux";
|
|
240
|
-
if (process.platform === "win32") return "windows";
|
|
241
|
-
throw new Error(`unsupported platform ${process.platform}`);
|
|
242
|
-
}
|
|
243
|
-
function defaultShell() {
|
|
244
|
-
if (process.platform === "win32") return process.env.COMSPEC ?? "cmd.exe";
|
|
245
|
-
return os.userInfo().shell ?? "/bin/sh";
|
|
246
|
-
}
|
|
247
|
-
function configInfo() {
|
|
248
|
-
return {
|
|
249
|
-
config: readConfig(),
|
|
250
|
-
defaults,
|
|
251
|
-
platform: platform(),
|
|
252
|
-
version,
|
|
253
|
-
defaultShell: defaultShell()
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function readConfig() {
|
|
257
|
-
let user;
|
|
258
|
-
try {
|
|
259
|
-
user = readJson("config.json");
|
|
260
|
-
} catch (error) {
|
|
261
|
-
console.warn(`invalid ${configFile}, using the default config: ${error}`);
|
|
262
|
-
return structuredClone(defaults);
|
|
263
|
-
}
|
|
264
|
-
if (user === null) return structuredClone(defaults);
|
|
265
|
-
if (!isObject(user)) {
|
|
266
|
-
console.warn(`${configFile} is not an object, using the default config`);
|
|
267
|
-
return structuredClone(defaults);
|
|
268
|
-
}
|
|
269
|
-
let config = structuredClone(defaults);
|
|
270
|
-
for (const [key, value] of Object.entries(user)) config = merge(config, [key], value);
|
|
271
|
-
return config;
|
|
272
|
-
}
|
|
273
|
-
function saveConfig(value) {
|
|
274
|
-
const error = invalid(value, defaults, []);
|
|
275
|
-
if (error !== null) throw new Error(error);
|
|
276
|
-
writeJson("config.json", value);
|
|
277
|
-
process.env.CLAUDE_CONFIG_DIR = value.claudeBaseDir;
|
|
278
|
-
broadcast({
|
|
279
|
-
type: "config",
|
|
280
|
-
config: value
|
|
281
|
-
});
|
|
282
|
-
return value;
|
|
283
|
-
}
|
|
284
|
-
function merge(config, keys, user) {
|
|
285
|
-
if (isObject(keys.reduce((value, key) => isObject(value) ? value[key] : void 0, config)) && isObject(user)) {
|
|
286
|
-
let next = config;
|
|
287
|
-
for (const [key, value] of Object.entries(user)) next = merge(next, [...keys, key], value);
|
|
288
|
-
return next;
|
|
289
|
-
}
|
|
290
|
-
const candidate = structuredClone(config);
|
|
291
|
-
const parent = keys.slice(0, -1).reduce((value, key) => value[key], candidate);
|
|
292
|
-
parent[keys[keys.length - 1]] = user;
|
|
293
|
-
const error = invalid(candidate, defaults, []);
|
|
294
|
-
if (error === null) return candidate;
|
|
295
|
-
console.warn(`invalid ${keys.join(".")} in ${configFile}, using its default: ${error}`);
|
|
296
|
-
return config;
|
|
297
|
-
}
|
|
298
|
-
function invalid(value, template, keys) {
|
|
299
|
-
const name = keys.join(".");
|
|
300
|
-
if (isObject(template)) {
|
|
301
|
-
if (!isObject(value)) return `${name || "config"} must be an object`;
|
|
302
|
-
for (const key of Object.keys(value)) if (!(key in template)) return `unknown setting ${[...keys, key].join(".")}`;
|
|
303
|
-
for (const key of Object.keys(template)) {
|
|
304
|
-
if (!(key in value)) return `missing setting ${[...keys, key].join(".")}`;
|
|
305
|
-
const error = invalid(value[key], template[key], [...keys, key]);
|
|
306
|
-
if (error !== null) return error;
|
|
307
|
-
}
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
if (nullable.includes(name) && value === null) return null;
|
|
311
|
-
if (name === "claudeBaseDir") return typeof value === "string" && path.isAbsolute(value) ? null : "claudeBaseDir must be an absolute path";
|
|
312
|
-
if (name in choices) {
|
|
313
|
-
if (choices[name].includes(value)) return null;
|
|
314
|
-
return `${name} must be one of ${choices[name].join(", ")}`;
|
|
315
|
-
}
|
|
316
|
-
if (template === null) {
|
|
317
|
-
if (typeof value === "string") return null;
|
|
318
|
-
return `${name} must be text or null`;
|
|
319
|
-
}
|
|
320
|
-
if (typeof value !== typeof template) return `${name} must be a ${typeof template}`;
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
function isObject(value) {
|
|
324
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
325
|
-
}
|
|
326
|
-
//#endregion
|
|
327
73
|
//#region src/lib/codex/app-server.ts
|
|
328
74
|
const IDLE_LIMIT$3 = 18e5;
|
|
329
75
|
const START_TIMEOUT = 15e3;
|
|
@@ -340,6 +86,9 @@ function appServer(home) {
|
|
|
340
86
|
function stopAppServers() {
|
|
341
87
|
for (const server of servers.values()) server.stop();
|
|
342
88
|
}
|
|
89
|
+
function stopAppServer(home) {
|
|
90
|
+
servers.get(home)?.stop();
|
|
91
|
+
}
|
|
343
92
|
function findCodex() {
|
|
344
93
|
const name = process.platform === "win32" ? "codex.exe" : "codex";
|
|
345
94
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
@@ -378,8 +127,8 @@ var AppServer = class {
|
|
|
378
127
|
this.ready = this.call("initialize", {
|
|
379
128
|
clientInfo: {
|
|
380
129
|
name: "offsite",
|
|
381
|
-
title: "
|
|
382
|
-
version
|
|
130
|
+
title: "OffSite",
|
|
131
|
+
version: "0.0.0"
|
|
383
132
|
},
|
|
384
133
|
capabilities: {
|
|
385
134
|
experimentalApi: true,
|
|
@@ -616,82 +365,210 @@ function isMirror(dir) {
|
|
|
616
365
|
}
|
|
617
366
|
}
|
|
618
367
|
//#endregion
|
|
619
|
-
//#region src/lib/
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
+
};
|
|
632
383
|
}
|
|
633
|
-
function
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
};
|
|
637
406
|
}
|
|
638
|
-
function
|
|
407
|
+
function promptMessage(prompt, uuid) {
|
|
639
408
|
return {
|
|
640
|
-
|
|
641
|
-
|
|
409
|
+
type: "user",
|
|
410
|
+
uuid,
|
|
642
411
|
time: Date.now(),
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
645
420
|
};
|
|
646
421
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
time
|
|
422
|
+
function notice(text) {
|
|
423
|
+
return {
|
|
424
|
+
type: "notice",
|
|
425
|
+
uuid: randomUUID(),
|
|
426
|
+
time: Date.now(),
|
|
427
|
+
text,
|
|
428
|
+
isError: true
|
|
652
429
|
};
|
|
653
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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}`,
|
|
569
|
+
kind: "file_change",
|
|
570
|
+
path: change.path,
|
|
571
|
+
diffs: change.kind.type === "add" ? [{
|
|
695
572
|
before: "",
|
|
696
573
|
after: change.diff
|
|
697
574
|
}] : change.kind.type === "delete" ? [{
|
|
@@ -812,50 +689,293 @@ function hunks(diff) {
|
|
|
812
689
|
return result;
|
|
813
690
|
}
|
|
814
691
|
function contentText$1(content) {
|
|
815
|
-
return content.map((block) =>
|
|
692
|
+
return content.map((block) => isRecord(block) && typeof block.text === "string" ? block.text : JSON.stringify(block)).join("\n");
|
|
816
693
|
}
|
|
817
694
|
function record$1(value) {
|
|
818
|
-
|
|
819
|
-
return { value };
|
|
695
|
+
return isRecord(value) ? value : { value };
|
|
820
696
|
}
|
|
821
697
|
//#endregion
|
|
822
|
-
//#region src/
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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 pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline");
|
|
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 code for another browser and prints its link").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));
|
|
833
730
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
})
|
|
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);
|
|
839
783
|
}
|
|
840
784
|
//#endregion
|
|
841
|
-
//#region src/
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
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",
|
|
859
979
|
background: null
|
|
860
980
|
}
|
|
861
981
|
};
|
|
@@ -1182,6 +1302,7 @@ var CodexRun = class CodexRun {
|
|
|
1182
1302
|
request
|
|
1183
1303
|
});
|
|
1184
1304
|
broadcastActivity();
|
|
1305
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
1185
1306
|
}
|
|
1186
1307
|
async startTurn(prompt) {
|
|
1187
1308
|
clearTimeout(this.idleTimer);
|
|
@@ -1439,1058 +1560,530 @@ async function readThread(threadId) {
|
|
|
1439
1560
|
})).thread;
|
|
1440
1561
|
}
|
|
1441
1562
|
//#endregion
|
|
1442
|
-
//#region src/
|
|
1443
|
-
const
|
|
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
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const LOGO_TYPES = [
|
|
1506
|
-
"image/png",
|
|
1507
|
-
"image/jpeg",
|
|
1508
|
-
"image/gif",
|
|
1509
|
-
"image/webp",
|
|
1510
|
-
"image/svg+xml",
|
|
1511
|
-
"image/x-icon",
|
|
1512
|
-
"image/vnd.microsoft.icon"
|
|
1513
|
-
];
|
|
1514
|
-
const LOGO_LIMIT = 524288;
|
|
1515
|
-
function projects$1() {
|
|
1516
|
-
return db.select().from(projects$2).all();
|
|
1517
|
-
}
|
|
1518
|
-
function projectPath(id) {
|
|
1519
|
-
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
1520
|
-
if (project === void 0) throw new Error("That project was removed");
|
|
1521
|
-
return project.path;
|
|
1522
|
-
}
|
|
1523
|
-
async function addProject(input) {
|
|
1524
|
-
const folder = path.resolve(expandHome(input.trim()));
|
|
1525
|
-
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
1526
|
-
db.insert(projects$2).values({
|
|
1527
|
-
id: randomUUID(),
|
|
1528
|
-
path: folder,
|
|
1529
|
-
name: path.basename(folder) || folder,
|
|
1530
|
-
logo: null
|
|
1531
|
-
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
1532
|
-
publish$1();
|
|
1533
|
-
const project = projects$1().find((item) => item.path === folder);
|
|
1534
|
-
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
1535
|
-
return project;
|
|
1536
|
-
}
|
|
1537
|
-
function updateProject(id, patch) {
|
|
1538
|
-
if (patch.name !== void 0) {
|
|
1539
|
-
const name = patch.name.trim();
|
|
1540
|
-
if (name === "") throw new Error("Project name cannot be empty");
|
|
1541
|
-
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
1542
|
-
}
|
|
1543
|
-
if (patch.logo !== void 0) {
|
|
1544
|
-
if (patch.logo !== null) checkLogo(patch.logo);
|
|
1545
|
-
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
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 [];
|
|
1546
1626
|
}
|
|
1547
|
-
publish$1();
|
|
1548
|
-
return null;
|
|
1549
1627
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
+
}]);
|
|
1553
1635
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
+
}));
|
|
1566
1652
|
}
|
|
1567
|
-
function
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1653
|
+
function partItems(message, part, index, parent) {
|
|
1654
|
+
const base = {
|
|
1655
|
+
id: `${message.id}:${index}`,
|
|
1656
|
+
parent,
|
|
1657
|
+
time: message.time.created
|
|
1658
|
+
};
|
|
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
|
+
}
|
|
1572
1677
|
}
|
|
1573
|
-
function
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1678
|
+
function toolItems(base, tool) {
|
|
1679
|
+
const state = tool.state;
|
|
1680
|
+
const status = toolStatuses[state.status];
|
|
1681
|
+
const input = state.status === "streaming" ? {} : state.input;
|
|
1682
|
+
const metadata = state.status === "streaming" ? {} : state.metadata ?? {};
|
|
1683
|
+
const output = state.status === "error" ? state.error.message : state.status === "completed" ? contentText(state.content) : "";
|
|
1684
|
+
switch (tool.name) {
|
|
1685
|
+
case "shell": return [{
|
|
1686
|
+
...base,
|
|
1687
|
+
kind: "command",
|
|
1688
|
+
command: text$2(input.command),
|
|
1689
|
+
description: text$2(input.description),
|
|
1690
|
+
output,
|
|
1691
|
+
status,
|
|
1692
|
+
background: null
|
|
1693
|
+
}];
|
|
1694
|
+
case "edit": return [{
|
|
1695
|
+
...base,
|
|
1696
|
+
kind: "file_change",
|
|
1697
|
+
path: text$2(input.path ?? input.filePath),
|
|
1698
|
+
diffs: [{
|
|
1699
|
+
before: text$2(input.oldString),
|
|
1700
|
+
after: text$2(input.newString)
|
|
1701
|
+
}],
|
|
1702
|
+
output,
|
|
1703
|
+
status
|
|
1704
|
+
}];
|
|
1705
|
+
case "write": return [{
|
|
1706
|
+
...base,
|
|
1707
|
+
kind: "file_change",
|
|
1708
|
+
path: text$2(input.path ?? input.filePath),
|
|
1709
|
+
diffs: [{
|
|
1710
|
+
before: "",
|
|
1711
|
+
after: text$2(input.content)
|
|
1712
|
+
}],
|
|
1713
|
+
output,
|
|
1714
|
+
status
|
|
1715
|
+
}];
|
|
1716
|
+
case "patch":
|
|
1717
|
+
case "apply_patch":
|
|
1718
|
+
if (!Array.isArray(metadata.files)) break;
|
|
1719
|
+
return metadata.files.map((file, index) => ({
|
|
1720
|
+
...base,
|
|
1721
|
+
id: `${base.id}:${index}`,
|
|
1722
|
+
kind: "file_change",
|
|
1723
|
+
path: text$2(record(file).file),
|
|
1724
|
+
diffs: hunks(text$2(record(file).patch)),
|
|
1725
|
+
output,
|
|
1726
|
+
status
|
|
1727
|
+
}));
|
|
1728
|
+
case "todowrite": return [{
|
|
1729
|
+
...base,
|
|
1730
|
+
kind: "todo_list",
|
|
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"
|
|
1734
|
+
}))
|
|
1735
|
+
}];
|
|
1736
|
+
case "subagent":
|
|
1737
|
+
case "task": return [{
|
|
1738
|
+
...base,
|
|
1739
|
+
kind: "subagent",
|
|
1740
|
+
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
1741
|
+
description: text$2(input.description),
|
|
1742
|
+
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
1743
|
+
status
|
|
1744
|
+
}];
|
|
1745
|
+
case "webfetch":
|
|
1746
|
+
case "websearch": return [{
|
|
1747
|
+
...base,
|
|
1748
|
+
kind: "web_search",
|
|
1749
|
+
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
1750
|
+
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
1751
|
+
results: [],
|
|
1752
|
+
output,
|
|
1753
|
+
status
|
|
1754
|
+
}];
|
|
1755
|
+
case "question": {
|
|
1756
|
+
const questions = list$1(input.questions).map((item) => ({
|
|
1757
|
+
question: text$2(record(item).question),
|
|
1758
|
+
header: text$2(record(item).header),
|
|
1759
|
+
multiSelect: record(item).multiple === true,
|
|
1760
|
+
options: list$1(record(item).options).map((option) => ({
|
|
1761
|
+
label: text$2(record(option).label),
|
|
1762
|
+
description: text$2(record(option).description)
|
|
1763
|
+
}))
|
|
1764
|
+
}));
|
|
1765
|
+
const answers = metadata.answers;
|
|
1766
|
+
return [{
|
|
1767
|
+
...base,
|
|
1768
|
+
kind: "question",
|
|
1769
|
+
questions,
|
|
1770
|
+
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list$1(answers[index]).map(text$2).join(", ")])) : null,
|
|
1771
|
+
status
|
|
1772
|
+
}];
|
|
1773
|
+
}
|
|
1774
|
+
case "skill": return [{
|
|
1775
|
+
...base,
|
|
1776
|
+
kind: "skill",
|
|
1777
|
+
skill: text$2(input.name),
|
|
1778
|
+
output,
|
|
1779
|
+
status
|
|
1780
|
+
}];
|
|
1781
|
+
}
|
|
1782
|
+
return [{
|
|
1783
|
+
...base,
|
|
1784
|
+
kind: "tool",
|
|
1785
|
+
name: tool.name,
|
|
1786
|
+
input,
|
|
1787
|
+
output,
|
|
1788
|
+
status
|
|
1789
|
+
}];
|
|
1577
1790
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
1581
|
-
return input;
|
|
1791
|
+
function questionText(field) {
|
|
1792
|
+
return field.description ?? field.title ?? field.key;
|
|
1582
1793
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
claude: {
|
|
1600
|
-
provider: "claude",
|
|
1601
|
-
state: "loading",
|
|
1602
|
-
error: null
|
|
1603
|
-
},
|
|
1604
|
-
codex: {
|
|
1605
|
-
provider: "codex",
|
|
1606
|
-
state: "loading",
|
|
1607
|
-
error: null
|
|
1608
|
-
},
|
|
1609
|
-
opencode: {
|
|
1610
|
-
provider: "opencode",
|
|
1611
|
-
state: "loading",
|
|
1612
|
-
error: null
|
|
1794
|
+
function fieldOptions(field) {
|
|
1795
|
+
switch (field.type) {
|
|
1796
|
+
case "string":
|
|
1797
|
+
case "multiselect": return (field.options ?? []).map((option) => ({
|
|
1798
|
+
label: option.label,
|
|
1799
|
+
description: option.description ?? ""
|
|
1800
|
+
}));
|
|
1801
|
+
case "boolean": return [{
|
|
1802
|
+
label: "Yes",
|
|
1803
|
+
description: ""
|
|
1804
|
+
}, {
|
|
1805
|
+
label: "No",
|
|
1806
|
+
description: ""
|
|
1807
|
+
}];
|
|
1808
|
+
case "number":
|
|
1809
|
+
case "integer": return [];
|
|
1613
1810
|
}
|
|
1614
|
-
};
|
|
1615
|
-
const refreshing = /* @__PURE__ */ new Map();
|
|
1616
|
-
const timers = /* @__PURE__ */ new Map();
|
|
1617
|
-
const providers = /* @__PURE__ */ new Map();
|
|
1618
|
-
let watchers = [];
|
|
1619
|
-
function sessions() {
|
|
1620
|
-
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
1621
|
-
const known = new Set(projects$1().map((project) => project.id));
|
|
1622
|
-
return allProviders.flatMap((provider) => listed[provider].filter((item) => known.has(item.projectId)).map((item) => {
|
|
1623
|
-
const record = records.get(item.id);
|
|
1624
|
-
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
1625
|
-
return {
|
|
1626
|
-
...item,
|
|
1627
|
-
updatedAt,
|
|
1628
|
-
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
1629
|
-
};
|
|
1630
|
-
}));
|
|
1631
1811
|
}
|
|
1632
|
-
function
|
|
1633
|
-
return
|
|
1812
|
+
function optionValue(options, label) {
|
|
1813
|
+
return options.find((option) => option.label === label)?.value ?? label;
|
|
1634
1814
|
}
|
|
1635
|
-
function
|
|
1636
|
-
return
|
|
1815
|
+
function contentText(content) {
|
|
1816
|
+
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
1637
1817
|
}
|
|
1638
|
-
function
|
|
1639
|
-
|
|
1640
|
-
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
1641
|
-
return provider;
|
|
1818
|
+
function text$2(value) {
|
|
1819
|
+
return typeof value === "string" ? value : "";
|
|
1642
1820
|
}
|
|
1643
|
-
function
|
|
1644
|
-
|
|
1645
|
-
sessionId: id,
|
|
1646
|
-
interactedAt: Date.now(),
|
|
1647
|
-
status: "active"
|
|
1648
|
-
}).onConflictDoUpdate({
|
|
1649
|
-
target: sessions$1.sessionId,
|
|
1650
|
-
set: {
|
|
1651
|
-
interactedAt: Date.now(),
|
|
1652
|
-
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
1653
|
-
}
|
|
1654
|
-
}).run();
|
|
1821
|
+
function list$1(value) {
|
|
1822
|
+
return Array.isArray(value) ? value : [];
|
|
1655
1823
|
}
|
|
1656
|
-
function
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1824
|
+
function record(value) {
|
|
1825
|
+
return isRecord(value) ? value : {};
|
|
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();
|
|
1832
|
+
function openCodeInstalled() {
|
|
1833
|
+
return findOpenCode() !== null;
|
|
1834
|
+
}
|
|
1835
|
+
async function openCodeCapabilities(cwd) {
|
|
1836
|
+
const client = await openCode();
|
|
1837
|
+
const location = { directory: cwd };
|
|
1838
|
+
const [models, commands, skills, agents] = await Promise.all([
|
|
1839
|
+
client.model.list({ location }),
|
|
1840
|
+
client.command.list({ location }),
|
|
1841
|
+
client.skill.list({ location }),
|
|
1842
|
+
client.agent.list({ location })
|
|
1843
|
+
]);
|
|
1844
|
+
return {
|
|
1845
|
+
models: models.data.filter((model) => model.enabled).map((model) => ({
|
|
1846
|
+
value: `${model.providerID}/${model.id}`,
|
|
1847
|
+
name: model.name,
|
|
1848
|
+
description: model.providerID,
|
|
1849
|
+
efforts: model.variants.map((variant) => variant.id)
|
|
1850
|
+
})),
|
|
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
|
+
}))],
|
|
1860
|
+
agents: agents.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden).map((agent) => ({
|
|
1861
|
+
id: agent.id,
|
|
1862
|
+
name: agent.name,
|
|
1863
|
+
description: agent.description ?? ""
|
|
1864
|
+
}))
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
async function openCodeSessions(paths) {
|
|
1868
|
+
if (paths.length === 0 || !openCodeInstalled()) return [];
|
|
1869
|
+
const client = await openCode();
|
|
1870
|
+
return (await Promise.all(paths.map(async (directory) => {
|
|
1871
|
+
const found = [];
|
|
1872
|
+
let cursor = null;
|
|
1873
|
+
do {
|
|
1874
|
+
const page = await client.session.list(cursor === null ? {
|
|
1875
|
+
directory,
|
|
1876
|
+
parentID: null,
|
|
1877
|
+
limit: PAGE
|
|
1878
|
+
} : { cursor });
|
|
1879
|
+
found.push(...page.data);
|
|
1880
|
+
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
1881
|
+
} while (cursor !== null);
|
|
1882
|
+
return found;
|
|
1883
|
+
}))).flat().filter((session) => !temporary.has(session.id));
|
|
1884
|
+
}
|
|
1885
|
+
async function openCodeHistory(sessionId, cwd) {
|
|
1886
|
+
const run = openCodeRun(sessionId);
|
|
1887
|
+
const client = await openCode();
|
|
1888
|
+
const messages = await allMessages(client, sessionId);
|
|
1889
|
+
return {
|
|
1890
|
+
type: "history",
|
|
1891
|
+
sessionId,
|
|
1892
|
+
provider: "opencode",
|
|
1893
|
+
items: messages.flatMap((message) => messageItems(message, null)),
|
|
1894
|
+
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
1895
|
+
queue: run?.queue ?? [],
|
|
1896
|
+
settings: run?.settings ?? null,
|
|
1897
|
+
context: run?.context ?? await contextOf(client, cwd, messages),
|
|
1898
|
+
before: null,
|
|
1899
|
+
partial: run?.partial ?? ""
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
async function openCodeAgent(agentId, parent) {
|
|
1903
|
+
return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
|
|
1904
|
+
}
|
|
1905
|
+
async function renameOpenCode(sessionId, title) {
|
|
1906
|
+
await (await openCode()).session.update({
|
|
1907
|
+
sessionID: sessionId,
|
|
1908
|
+
title
|
|
1660
1909
|
});
|
|
1661
1910
|
}
|
|
1662
|
-
function
|
|
1663
|
-
|
|
1911
|
+
async function removeOpenCode(sessionId) {
|
|
1912
|
+
await (await openCode()).session.remove({ sessionID: sessionId });
|
|
1664
1913
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1914
|
+
async function forkOpenCode(sessionId, messageId) {
|
|
1915
|
+
const client = await openCode();
|
|
1916
|
+
const messages = await allMessages(client, sessionId);
|
|
1917
|
+
const index = messages.findIndex((message) => message.id === messageId);
|
|
1918
|
+
if (index < 0) throw new Error("Cannot fork the session at that point");
|
|
1919
|
+
const next = messages.at(index + 1);
|
|
1920
|
+
return { sessionId: (await client.session.fork({
|
|
1921
|
+
sessionID: sessionId,
|
|
1922
|
+
...next === void 0 ? {} : { before: next.id }
|
|
1923
|
+
})).id };
|
|
1671
1924
|
}
|
|
1672
|
-
async function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1925
|
+
async function rewindOpenCode(request) {
|
|
1926
|
+
if (request.code && !request.conversation && !request.dryRun) throw new Error("OpenCode restores files only with the conversation");
|
|
1927
|
+
const client = await openCode();
|
|
1928
|
+
const sessionID = request.sessionId;
|
|
1929
|
+
const files = (await client.session.revert.stage({
|
|
1930
|
+
sessionID,
|
|
1931
|
+
messageID: request.messageId,
|
|
1932
|
+
files: request.code
|
|
1933
|
+
})).files ?? [];
|
|
1934
|
+
const result = request.code ? {
|
|
1935
|
+
files: files.map((file) => file.file),
|
|
1936
|
+
insertions: sumBy(files, (file) => file.additions),
|
|
1937
|
+
deletions: sumBy(files, (file) => file.deletions)
|
|
1938
|
+
} : null;
|
|
1939
|
+
if (request.dryRun) await client.session.revert.clear({ sessionID });
|
|
1940
|
+
else await client.session.revert.commit({ sessionID });
|
|
1941
|
+
return result;
|
|
1685
1942
|
}
|
|
1686
|
-
async function
|
|
1687
|
-
const
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1943
|
+
async function openCodeCommitMessage(cwd, model, prompt) {
|
|
1944
|
+
const client = await openCode();
|
|
1945
|
+
const session = await client.session.create({
|
|
1946
|
+
location: { directory: cwd },
|
|
1947
|
+
agent: "plan",
|
|
1948
|
+
model: modelRef(model, null)
|
|
1949
|
+
});
|
|
1950
|
+
temporary.add(session.id);
|
|
1690
1951
|
try {
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
listed[provider] = found;
|
|
1703
|
-
statuses[provider] = {
|
|
1704
|
-
provider,
|
|
1705
|
-
state: "ready",
|
|
1706
|
-
error: null
|
|
1707
|
-
};
|
|
1708
|
-
for (const item of found) providers.set(item.id, provider);
|
|
1952
|
+
await client.session.prompt({
|
|
1953
|
+
sessionID: session.id,
|
|
1954
|
+
text: prompt
|
|
1955
|
+
});
|
|
1956
|
+
await client.session.wait({ sessionID: session.id }, { signal: AbortSignal.timeout(COMMIT_LIMIT) });
|
|
1957
|
+
const messages = await allMessages(client, session.id);
|
|
1958
|
+
for (const message of messages.toReversed()) {
|
|
1959
|
+
if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
|
|
1960
|
+
if (message.type !== "assistant") continue;
|
|
1961
|
+
const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
|
|
1962
|
+
if (text !== "") return { message: text };
|
|
1709
1963
|
}
|
|
1710
|
-
|
|
1711
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1712
|
-
console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
1713
|
-
statuses[provider] = {
|
|
1714
|
-
provider,
|
|
1715
|
-
state: "error",
|
|
1716
|
-
error: message
|
|
1717
|
-
};
|
|
1964
|
+
throw new Error("OpenCode did not reply");
|
|
1718
1965
|
} finally {
|
|
1719
|
-
|
|
1966
|
+
await client.session.remove({ sessionID: session.id });
|
|
1967
|
+
temporary.delete(session.id);
|
|
1720
1968
|
}
|
|
1721
|
-
publishSessions();
|
|
1722
|
-
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
1723
|
-
type: "providers",
|
|
1724
|
-
providers: providerStatuses()
|
|
1725
|
-
});
|
|
1726
1969
|
}
|
|
1727
|
-
async function
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
title: item.summary,
|
|
1737
|
-
projectId,
|
|
1738
|
-
branch: item.gitBranch ?? null,
|
|
1739
|
-
updatedAt: item.lastModified
|
|
1740
|
-
}))))).flat();
|
|
1741
|
-
case "codex":
|
|
1742
|
-
if (!codexInstalled()) return null;
|
|
1743
|
-
return (await codexThreads([...paths.keys()])).flatMap((thread) => {
|
|
1744
|
-
const projectId = paths.get(thread.cwd);
|
|
1745
|
-
if (projectId === void 0) return [];
|
|
1746
|
-
return [{
|
|
1747
|
-
id: thread.id,
|
|
1748
|
-
provider: "codex",
|
|
1749
|
-
title: thread.name ?? thread.preview,
|
|
1750
|
-
projectId,
|
|
1751
|
-
branch: thread.gitInfo?.branch ?? null,
|
|
1752
|
-
updatedAt: thread.updatedAt * 1e3
|
|
1753
|
-
}];
|
|
1754
|
-
});
|
|
1755
|
-
case "opencode":
|
|
1756
|
-
if (!openCodeInstalled()) return null;
|
|
1757
|
-
return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
|
|
1758
|
-
const projectId = paths.get(session.location.directory);
|
|
1759
|
-
if (projectId === void 0) return [];
|
|
1760
|
-
return [{
|
|
1761
|
-
id: session.id,
|
|
1762
|
-
provider: "opencode",
|
|
1763
|
-
title: session.title ?? "New session",
|
|
1764
|
-
projectId,
|
|
1765
|
-
branch: null,
|
|
1766
|
-
updatedAt: session.time.updated
|
|
1767
|
-
}];
|
|
1768
|
-
});
|
|
1769
|
-
}
|
|
1970
|
+
async function contextOf(client, cwd, messages) {
|
|
1971
|
+
const last = messages.findLast((message) => message.type === "assistant" && message.tokens !== void 0);
|
|
1972
|
+
if (last?.type !== "assistant" || last.tokens === void 0) return null;
|
|
1973
|
+
const tokens = last.tokens;
|
|
1974
|
+
const model = (await client.model.list({ location: { directory: cwd } })).data.find((item) => item.providerID === last.model.providerID && item.id === last.model.id);
|
|
1975
|
+
return {
|
|
1976
|
+
tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write,
|
|
1977
|
+
window: model?.limit.context ?? null
|
|
1978
|
+
};
|
|
1770
1979
|
}
|
|
1771
|
-
function
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if (error.code !== "ENOENT") throw error;
|
|
1780
|
-
return [];
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
1980
|
+
function modelRef(value, effort) {
|
|
1981
|
+
const index = value.indexOf("/");
|
|
1982
|
+
if (index < 0) throw new Error(`${value} is not an OpenCode model`);
|
|
1983
|
+
return {
|
|
1984
|
+
providerID: value.slice(0, index),
|
|
1985
|
+
id: value.slice(index + 1),
|
|
1986
|
+
...effort === null ? {} : { variant: effort }
|
|
1987
|
+
};
|
|
1783
1988
|
}
|
|
1784
|
-
async function
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
})
|
|
1799
|
-
|
|
1800
|
-
set: { status: patch.status }
|
|
1801
|
-
}).run();
|
|
1802
|
-
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
1803
|
-
}
|
|
1804
|
-
async function removeSession(id) {
|
|
1805
|
-
const provider = providerOf(id);
|
|
1806
|
-
const run = runs.get(id);
|
|
1807
|
-
if (run !== void 0) {
|
|
1808
|
-
run.stop();
|
|
1809
|
-
await run.closed;
|
|
1810
|
-
}
|
|
1811
|
-
switch (provider) {
|
|
1812
|
-
case "claude":
|
|
1813
|
-
await deleteSession(id);
|
|
1814
|
-
break;
|
|
1815
|
-
case "codex":
|
|
1816
|
-
await deleteThread(id);
|
|
1817
|
-
break;
|
|
1818
|
-
case "opencode": await removeOpenCode(id);
|
|
1819
|
-
}
|
|
1820
|
-
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
1821
|
-
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
1822
|
-
}
|
|
1823
|
-
async function fork(sessionId, projectId, messageId) {
|
|
1824
|
-
const provider = providerOf(sessionId);
|
|
1825
|
-
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
1826
|
-
providers.set(result.sessionId, provider);
|
|
1827
|
-
recordInteraction(result.sessionId);
|
|
1828
|
-
refreshSessions(provider);
|
|
1829
|
-
return result;
|
|
1989
|
+
async function allMessages(client, sessionId) {
|
|
1990
|
+
const messages = [];
|
|
1991
|
+
let cursor = null;
|
|
1992
|
+
do {
|
|
1993
|
+
const page = await client.message.list(cursor === null ? {
|
|
1994
|
+
sessionID: sessionId,
|
|
1995
|
+
limit: PAGE,
|
|
1996
|
+
order: "asc"
|
|
1997
|
+
} : {
|
|
1998
|
+
sessionID: sessionId,
|
|
1999
|
+
cursor
|
|
2000
|
+
});
|
|
2001
|
+
messages.push(...page.data);
|
|
2002
|
+
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2003
|
+
} while (cursor !== null);
|
|
2004
|
+
return messages;
|
|
1830
2005
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
|
|
1835
|
-
return { sessionId: (await forkSession(sessionId, {
|
|
1836
|
-
dir: cwd,
|
|
1837
|
-
upToMessageId: messageId
|
|
1838
|
-
})).sessionId };
|
|
2006
|
+
function openCodeRun(sessionId) {
|
|
2007
|
+
const run = runs.get(sessionId);
|
|
2008
|
+
return run?.provider === "opencode" ? run : void 0;
|
|
1839
2009
|
}
|
|
1840
2010
|
//#endregion
|
|
1841
|
-
//#region src/lib/opencode/
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
"
|
|
1846
|
-
"
|
|
1847
|
-
"
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
"
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2011
|
+
//#region src/lib/opencode/run.ts
|
|
2012
|
+
const IDLE_LIMIT$1 = 18e5;
|
|
2013
|
+
const RECENT = 30;
|
|
2014
|
+
const replies = {
|
|
2015
|
+
allow: "once",
|
|
2016
|
+
always: "always",
|
|
2017
|
+
deny: "reject"
|
|
2018
|
+
};
|
|
2019
|
+
var OpenCodeRun = class OpenCodeRun {
|
|
2020
|
+
provider = "opencode";
|
|
2021
|
+
sessionId;
|
|
2022
|
+
cwd;
|
|
2023
|
+
pending = /* @__PURE__ */ new Map();
|
|
2024
|
+
items = /* @__PURE__ */ new Map();
|
|
2025
|
+
closed;
|
|
2026
|
+
queue = [];
|
|
2027
|
+
settings;
|
|
2028
|
+
context = null;
|
|
2029
|
+
partial = "";
|
|
2030
|
+
running = false;
|
|
2031
|
+
prompted = false;
|
|
2032
|
+
backgroundTasks = 0;
|
|
2033
|
+
tasks = {};
|
|
2034
|
+
close;
|
|
2035
|
+
applied;
|
|
2036
|
+
loading = Promise.resolve();
|
|
2037
|
+
refreshTimer;
|
|
2038
|
+
idleTimer;
|
|
2039
|
+
static async start(cwd, settings) {
|
|
2040
|
+
const session = await (await openCode()).session.create({
|
|
2041
|
+
location: { directory: cwd },
|
|
2042
|
+
...settings.mode === null ? {} : { agent: settings.mode },
|
|
2043
|
+
...settings.model === null ? {} : { model: modelRef(settings.model, settings.effort) }
|
|
2044
|
+
});
|
|
2045
|
+
return new OpenCodeRun(session.id, cwd, settings, settings);
|
|
1861
2046
|
}
|
|
1862
|
-
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
connection ??= start();
|
|
1866
|
-
return connection;
|
|
1867
|
-
}
|
|
1868
|
-
async function start() {
|
|
1869
|
-
try {
|
|
1870
|
-
const client = await connect();
|
|
1871
|
-
listen(client);
|
|
1872
|
-
return client;
|
|
1873
|
-
} catch (error) {
|
|
1874
|
-
connection = void 0;
|
|
1875
|
-
throw error;
|
|
2047
|
+
static async resume(sessionId, cwd, settings) {
|
|
2048
|
+
const session = await (await openCode()).session.get({ sessionID: sessionId });
|
|
2049
|
+
return new OpenCodeRun(sessionId, cwd, settings, sessionSettings(session, settings));
|
|
1876
2050
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
1888
|
-
try {
|
|
1889
|
-
for await (const event of client.event.subscribe({
|
|
1890
|
-
signal: controller.signal,
|
|
1891
|
-
onActivity: () => silence.refresh()
|
|
1892
|
-
})) route(event);
|
|
1893
|
-
console.warn("the OpenCode event stream ended");
|
|
1894
|
-
} catch (error) {
|
|
1895
|
-
console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
|
|
1896
|
-
} finally {
|
|
1897
|
-
clearTimeout(silence);
|
|
2051
|
+
constructor(sessionId, cwd, settings, applied) {
|
|
2052
|
+
this.sessionId = sessionId;
|
|
2053
|
+
this.cwd = cwd;
|
|
2054
|
+
this.settings = settings;
|
|
2055
|
+
this.applied = applied;
|
|
2056
|
+
const closed = Promise.withResolvers();
|
|
2057
|
+
this.closed = closed.promise;
|
|
2058
|
+
this.close = closed.resolve;
|
|
2059
|
+
runs.set(sessionId, this);
|
|
2060
|
+
this.armIdleTimer();
|
|
1898
2061
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
2062
|
+
async submit(prompt, delivery) {
|
|
2063
|
+
if (!this.running) {
|
|
2064
|
+
await this.startTurn(prompt);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
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();
|
|
1912
2082
|
}
|
|
1913
|
-
listen(client);
|
|
1914
|
-
await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
|
|
1915
|
-
return client;
|
|
1916
2083
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
refreshSessions("opencode");
|
|
1921
|
-
throw failure;
|
|
1922
|
-
}
|
|
1923
|
-
async function catchUp(client) {
|
|
1924
|
-
const active = await client.session.active();
|
|
1925
|
-
for (const run of openCodeRuns()) {
|
|
1926
|
-
const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
|
|
1927
|
-
run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
|
|
1928
|
-
}
|
|
1929
|
-
refreshSessions("opencode");
|
|
1930
|
-
}
|
|
1931
|
-
function route(event) {
|
|
1932
|
-
if (listChanges.has(event.type)) refreshSessions("opencode");
|
|
1933
|
-
const sessionId = sessionOf(event);
|
|
1934
|
-
if (sessionId === null) return;
|
|
1935
|
-
const run = runs.get(sessionId);
|
|
1936
|
-
if (run?.provider === "opencode") run.receive(event);
|
|
1937
|
-
}
|
|
1938
|
-
function sessionOf(event) {
|
|
1939
|
-
if (event.type === "form.created") return event.data.form.sessionID;
|
|
1940
|
-
const data = "data" in event ? event.data : null;
|
|
1941
|
-
if (typeof data === "object" && data !== null && "sessionID" in data && typeof data.sessionID === "string") return data.sessionID;
|
|
1942
|
-
return null;
|
|
1943
|
-
}
|
|
1944
|
-
function openCodeRuns() {
|
|
1945
|
-
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
1946
|
-
}
|
|
1947
|
-
//#endregion
|
|
1948
|
-
//#region src/lib/opencode/items.ts
|
|
1949
|
-
const toolStatuses = {
|
|
1950
|
-
streaming: "running",
|
|
1951
|
-
running: "running",
|
|
1952
|
-
completed: "completed",
|
|
1953
|
-
error: "failed"
|
|
1954
|
-
};
|
|
1955
|
-
const todoStatuses$1 = {
|
|
1956
|
-
pending: "pending",
|
|
1957
|
-
in_progress: "running",
|
|
1958
|
-
completed: "completed",
|
|
1959
|
-
cancelled: "completed"
|
|
1960
|
-
};
|
|
1961
|
-
function messageItems(message, parent) {
|
|
1962
|
-
const base = {
|
|
1963
|
-
id: message.id,
|
|
1964
|
-
parent,
|
|
1965
|
-
time: message.time.created
|
|
1966
|
-
};
|
|
1967
|
-
switch (message.type) {
|
|
1968
|
-
case "user": return [{
|
|
1969
|
-
...base,
|
|
1970
|
-
kind: "user",
|
|
1971
|
-
text: message.text,
|
|
1972
|
-
images: (message.files ?? []).filter((file) => file.mime.startsWith("image/")).map((file) => ({
|
|
1973
|
-
mediaType: file.mime,
|
|
1974
|
-
data: file.data
|
|
1975
|
-
})),
|
|
1976
|
-
format: "text"
|
|
1977
|
-
}];
|
|
1978
|
-
case "skill": return [{
|
|
1979
|
-
...base,
|
|
1980
|
-
kind: "skill",
|
|
1981
|
-
skill: message.name,
|
|
1982
|
-
output: "",
|
|
1983
|
-
status: "completed"
|
|
1984
|
-
}];
|
|
1985
|
-
case "shell": return [{
|
|
1986
|
-
...base,
|
|
1987
|
-
kind: "command",
|
|
1988
|
-
command: message.command,
|
|
1989
|
-
description: "",
|
|
1990
|
-
output: message.output?.output ?? "",
|
|
1991
|
-
status: message.status === "running" ? "running" : message.status === "exited" ? message.exit === 0 ? "completed" : "failed" : "stopped",
|
|
1992
|
-
background: null
|
|
1993
|
-
}];
|
|
1994
|
-
case "assistant": return message.content.flatMap((part, index) => partItems(message, part, index, parent));
|
|
1995
|
-
case "compaction": return message.status === "failed" ? [{
|
|
1996
|
-
...errorItem(message.id, message.error.message),
|
|
1997
|
-
parent
|
|
1998
|
-
}] : [{
|
|
1999
|
-
...base,
|
|
2000
|
-
kind: "compaction"
|
|
2001
|
-
}];
|
|
2002
|
-
case "idle": return message.outcome === "failed" && message.error !== void 0 ? [{
|
|
2003
|
-
...errorItem(message.id, message.error.message),
|
|
2004
|
-
parent
|
|
2005
|
-
}] : [];
|
|
2006
|
-
case "agent-switched":
|
|
2007
|
-
case "model-switched":
|
|
2008
|
-
case "location-switched":
|
|
2009
|
-
case "synthetic":
|
|
2010
|
-
case "system": return [];
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
function formQuestions(form) {
|
|
2014
|
-
return form.fields.flatMap((field) => field.type === "external" ? [] : [{
|
|
2015
|
-
question: questionText(field),
|
|
2016
|
-
header: field.title ?? "",
|
|
2017
|
-
multiSelect: field.type === "multiselect",
|
|
2018
|
-
options: fieldOptions(field)
|
|
2019
|
-
}]);
|
|
2020
|
-
}
|
|
2021
|
-
function formAnswer(form, answers) {
|
|
2022
|
-
return Object.fromEntries(form.fields.flatMap((field) => {
|
|
2023
|
-
if (field.type === "external") return [];
|
|
2024
|
-
const answer = answers[questionText(field)];
|
|
2025
|
-
if (answer === void 0 || answer === "") return [];
|
|
2026
|
-
switch (field.type) {
|
|
2027
|
-
case "string": return [[field.key, optionValue(field.options ?? [], answer)]];
|
|
2028
|
-
case "multiselect": return [[field.key, answer.split(", ").map((label) => optionValue(field.options, label))]];
|
|
2029
|
-
case "boolean":
|
|
2030
|
-
if (answer === "Yes") return [[field.key, true]];
|
|
2031
|
-
if (answer === "No") return [[field.key, false]];
|
|
2032
|
-
throw new Error(`${field.key} needs Yes or No`);
|
|
2033
|
-
case "number":
|
|
2034
|
-
case "integer": return [[field.key, Number(answer)]];
|
|
2035
|
-
}
|
|
2036
|
-
}));
|
|
2037
|
-
}
|
|
2038
|
-
function partItems(message, part, index, parent) {
|
|
2039
|
-
const base = {
|
|
2040
|
-
id: `${message.id}:${index}`,
|
|
2041
|
-
parent,
|
|
2042
|
-
time: message.time.created
|
|
2043
|
-
};
|
|
2044
|
-
switch (part.type) {
|
|
2045
|
-
case "text": return part.text.trim() === "" ? [] : [{
|
|
2046
|
-
...base,
|
|
2047
|
-
kind: "assistant",
|
|
2048
|
-
text: part.text,
|
|
2049
|
-
message: message.id
|
|
2050
|
-
}];
|
|
2051
|
-
case "reasoning": return part.text.trim() === "" ? [] : [{
|
|
2052
|
-
...base,
|
|
2053
|
-
kind: "reasoning",
|
|
2054
|
-
text: part.text.trim()
|
|
2055
|
-
}];
|
|
2056
|
-
case "tool": return toolItems({
|
|
2057
|
-
id: `${message.id}:${part.id}`,
|
|
2058
|
-
parent,
|
|
2059
|
-
time: part.time.created
|
|
2060
|
-
}, part);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
function toolItems(base, tool) {
|
|
2064
|
-
const state = tool.state;
|
|
2065
|
-
const status = toolStatuses[state.status];
|
|
2066
|
-
const input = state.status === "streaming" ? {} : state.input;
|
|
2067
|
-
const metadata = state.status === "streaming" ? {} : state.metadata ?? {};
|
|
2068
|
-
const output = state.status === "error" ? state.error.message : state.status === "completed" ? contentText(state.content) : "";
|
|
2069
|
-
switch (tool.name) {
|
|
2070
|
-
case "shell": return [{
|
|
2071
|
-
...base,
|
|
2072
|
-
kind: "command",
|
|
2073
|
-
command: text$2(input.command),
|
|
2074
|
-
description: text$2(input.description),
|
|
2075
|
-
output,
|
|
2076
|
-
status,
|
|
2077
|
-
background: null
|
|
2078
|
-
}];
|
|
2079
|
-
case "edit": return [{
|
|
2080
|
-
...base,
|
|
2081
|
-
kind: "file_change",
|
|
2082
|
-
path: text$2(input.path ?? input.filePath),
|
|
2083
|
-
diffs: [{
|
|
2084
|
-
before: text$2(input.oldString),
|
|
2085
|
-
after: text$2(input.newString)
|
|
2086
|
-
}],
|
|
2087
|
-
output,
|
|
2088
|
-
status
|
|
2089
|
-
}];
|
|
2090
|
-
case "write": return [{
|
|
2091
|
-
...base,
|
|
2092
|
-
kind: "file_change",
|
|
2093
|
-
path: text$2(input.path ?? input.filePath),
|
|
2094
|
-
diffs: [{
|
|
2095
|
-
before: "",
|
|
2096
|
-
after: text$2(input.content)
|
|
2097
|
-
}],
|
|
2098
|
-
output,
|
|
2099
|
-
status
|
|
2100
|
-
}];
|
|
2101
|
-
case "patch":
|
|
2102
|
-
case "apply_patch":
|
|
2103
|
-
if (!Array.isArray(metadata.files)) break;
|
|
2104
|
-
return metadata.files.map((file, index) => ({
|
|
2105
|
-
...base,
|
|
2106
|
-
id: `${base.id}:${index}`,
|
|
2107
|
-
kind: "file_change",
|
|
2108
|
-
path: text$2(record(file).file),
|
|
2109
|
-
diffs: hunks(text$2(record(file).patch)),
|
|
2110
|
-
output,
|
|
2111
|
-
status
|
|
2112
|
-
}));
|
|
2113
|
-
case "todowrite": return [{
|
|
2114
|
-
...base,
|
|
2115
|
-
kind: "todo_list",
|
|
2116
|
-
steps: list(input.todos).map((todo) => ({
|
|
2117
|
-
text: text$2(record(todo).content),
|
|
2118
|
-
status: todoStatuses$1[text$2(record(todo).status)] ?? "pending"
|
|
2119
|
-
}))
|
|
2120
|
-
}];
|
|
2121
|
-
case "subagent":
|
|
2122
|
-
case "task": return [{
|
|
2123
|
-
...base,
|
|
2124
|
-
kind: "subagent",
|
|
2125
|
-
agent: text$2(input.agent ?? input.subagent_type) || "Agent",
|
|
2126
|
-
description: text$2(input.description),
|
|
2127
|
-
agentId: typeof metadata.sessionID === "string" ? metadata.sessionID : null,
|
|
2128
|
-
status
|
|
2129
|
-
}];
|
|
2130
|
-
case "webfetch":
|
|
2131
|
-
case "websearch": return [{
|
|
2132
|
-
...base,
|
|
2133
|
-
kind: "web_search",
|
|
2134
|
-
query: tool.name === "websearch" ? text$2(input.query) : null,
|
|
2135
|
-
url: tool.name === "webfetch" ? text$2(input.url) : null,
|
|
2136
|
-
results: [],
|
|
2137
|
-
output,
|
|
2138
|
-
status
|
|
2139
|
-
}];
|
|
2140
|
-
case "question": {
|
|
2141
|
-
const questions = list(input.questions).map((item) => ({
|
|
2142
|
-
question: text$2(record(item).question),
|
|
2143
|
-
header: text$2(record(item).header),
|
|
2144
|
-
multiSelect: record(item).multiple === true,
|
|
2145
|
-
options: list(record(item).options).map((option) => ({
|
|
2146
|
-
label: text$2(record(option).label),
|
|
2147
|
-
description: text$2(record(option).description)
|
|
2148
|
-
}))
|
|
2149
|
-
}));
|
|
2150
|
-
const answers = metadata.answers;
|
|
2151
|
-
return [{
|
|
2152
|
-
...base,
|
|
2153
|
-
kind: "question",
|
|
2154
|
-
questions,
|
|
2155
|
-
answers: Array.isArray(answers) ? Object.fromEntries(questions.map((question, index) => [question.question, list(answers[index]).map(text$2).join(", ")])) : null,
|
|
2156
|
-
status
|
|
2157
|
-
}];
|
|
2158
|
-
}
|
|
2159
|
-
case "skill": return [{
|
|
2160
|
-
...base,
|
|
2161
|
-
kind: "skill",
|
|
2162
|
-
skill: text$2(input.name),
|
|
2163
|
-
output,
|
|
2164
|
-
status
|
|
2165
|
-
}];
|
|
2166
|
-
}
|
|
2167
|
-
return [{
|
|
2168
|
-
...base,
|
|
2169
|
-
kind: "tool",
|
|
2170
|
-
name: tool.name,
|
|
2171
|
-
input,
|
|
2172
|
-
output,
|
|
2173
|
-
status
|
|
2174
|
-
}];
|
|
2175
|
-
}
|
|
2176
|
-
function questionText(field) {
|
|
2177
|
-
return field.description ?? field.title ?? field.key;
|
|
2178
|
-
}
|
|
2179
|
-
function fieldOptions(field) {
|
|
2180
|
-
switch (field.type) {
|
|
2181
|
-
case "string":
|
|
2182
|
-
case "multiselect": return (field.options ?? []).map((option) => ({
|
|
2183
|
-
label: option.label,
|
|
2184
|
-
description: option.description ?? ""
|
|
2185
|
-
}));
|
|
2186
|
-
case "boolean": return [{
|
|
2187
|
-
label: "Yes",
|
|
2188
|
-
description: ""
|
|
2189
|
-
}, {
|
|
2190
|
-
label: "No",
|
|
2191
|
-
description: ""
|
|
2192
|
-
}];
|
|
2193
|
-
case "number":
|
|
2194
|
-
case "integer": return [];
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
function optionValue(options, label) {
|
|
2198
|
-
return options.find((option) => option.label === label)?.value ?? label;
|
|
2199
|
-
}
|
|
2200
|
-
function contentText(content) {
|
|
2201
|
-
return content.map((block) => block.type === "text" ? block.text : block.uri).join("\n");
|
|
2202
|
-
}
|
|
2203
|
-
function text$2(value) {
|
|
2204
|
-
return typeof value === "string" ? value : "";
|
|
2205
|
-
}
|
|
2206
|
-
function list(value) {
|
|
2207
|
-
return Array.isArray(value) ? value : [];
|
|
2208
|
-
}
|
|
2209
|
-
function record(value) {
|
|
2210
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2211
|
-
}
|
|
2212
|
-
//#endregion
|
|
2213
|
-
//#region src/lib/opencode/sessions.ts
|
|
2214
|
-
const PAGE = 100;
|
|
2215
|
-
const COMMIT_LIMIT = 12e4;
|
|
2216
|
-
const temporary = /* @__PURE__ */ new Set();
|
|
2217
|
-
function openCodeAccount() {
|
|
2218
|
-
return {
|
|
2219
|
-
id: "opencode",
|
|
2220
|
-
provider: "opencode",
|
|
2221
|
-
name: "OpenCode",
|
|
2222
|
-
configDir: "",
|
|
2223
|
-
email: null,
|
|
2224
|
-
plan: null,
|
|
2225
|
-
error: null,
|
|
2226
|
-
limits: [],
|
|
2227
|
-
linked: true,
|
|
2228
|
-
valid: true
|
|
2229
|
-
};
|
|
2230
|
-
}
|
|
2231
|
-
function openCodeInstalled() {
|
|
2232
|
-
return findOpenCode() !== null;
|
|
2233
|
-
}
|
|
2234
|
-
async function openCodeCapabilities(cwd) {
|
|
2235
|
-
const client = await openCode();
|
|
2236
|
-
const location = { directory: cwd };
|
|
2237
|
-
const [models, commands, skills, agents] = await Promise.all([
|
|
2238
|
-
client.model.list({ location }),
|
|
2239
|
-
client.command.list({ location }),
|
|
2240
|
-
client.skill.list({ location }),
|
|
2241
|
-
client.agent.list({ location })
|
|
2242
|
-
]);
|
|
2243
|
-
return {
|
|
2244
|
-
models: models.data.filter((model) => model.enabled).map((model) => ({
|
|
2245
|
-
value: `${model.providerID}/${model.id}`,
|
|
2246
|
-
name: model.name,
|
|
2247
|
-
description: model.providerID,
|
|
2248
|
-
efforts: model.variants.map((variant) => variant.id)
|
|
2249
|
-
})),
|
|
2250
|
-
commands: [
|
|
2251
|
-
{
|
|
2252
|
-
name: "compact",
|
|
2253
|
-
description: "Summarize the conversation to free up context",
|
|
2254
|
-
hint: ""
|
|
2255
|
-
},
|
|
2256
|
-
...commands.data.map((command) => ({
|
|
2257
|
-
name: command.name,
|
|
2258
|
-
description: command.description ?? "",
|
|
2259
|
-
hint: ""
|
|
2260
|
-
})),
|
|
2261
|
-
...skills.data.map((skill) => ({
|
|
2262
|
-
name: skill.name,
|
|
2263
|
-
description: skill.description ?? "",
|
|
2264
|
-
hint: ""
|
|
2265
|
-
}))
|
|
2266
|
-
],
|
|
2267
|
-
agents: agents.data.filter((agent) => agent.mode !== "subagent" && !agent.hidden).map((agent) => ({
|
|
2268
|
-
id: agent.id,
|
|
2269
|
-
name: agent.name,
|
|
2270
|
-
description: agent.description ?? ""
|
|
2271
|
-
}))
|
|
2272
|
-
};
|
|
2273
|
-
}
|
|
2274
|
-
async function openCodeSessions(paths) {
|
|
2275
|
-
if (paths.length === 0 || !openCodeInstalled()) return [];
|
|
2276
|
-
const client = await openCode();
|
|
2277
|
-
return (await Promise.all(paths.map(async (directory) => {
|
|
2278
|
-
const found = [];
|
|
2279
|
-
let cursor = null;
|
|
2280
|
-
do {
|
|
2281
|
-
const page = await client.session.list(cursor === null ? {
|
|
2282
|
-
directory,
|
|
2283
|
-
parentID: null,
|
|
2284
|
-
limit: PAGE
|
|
2285
|
-
} : { cursor });
|
|
2286
|
-
found.push(...page.data);
|
|
2287
|
-
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2288
|
-
} while (cursor !== null);
|
|
2289
|
-
return found;
|
|
2290
|
-
}))).flat().filter((session) => !temporary.has(session.id));
|
|
2291
|
-
}
|
|
2292
|
-
async function openCodeHistory(sessionId, cwd) {
|
|
2293
|
-
const run = openCodeRun(sessionId);
|
|
2294
|
-
const client = await openCode();
|
|
2295
|
-
const messages = await allMessages(client, sessionId);
|
|
2296
|
-
return {
|
|
2297
|
-
type: "history",
|
|
2298
|
-
sessionId,
|
|
2299
|
-
provider: "opencode",
|
|
2300
|
-
items: messages.flatMap((message) => messageItems(message, null)),
|
|
2301
|
-
requests: [...run?.pending.values() ?? []].map((item) => item.request),
|
|
2302
|
-
queue: run?.queue ?? [],
|
|
2303
|
-
settings: run?.settings ?? null,
|
|
2304
|
-
context: run?.context ?? await contextOf(client, cwd, messages),
|
|
2305
|
-
before: null,
|
|
2306
|
-
partial: run?.partial ?? ""
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
async function openCodeAgent(agentId, parent) {
|
|
2310
|
-
return (await allMessages(await openCode(), agentId)).flatMap((message) => messageItems(message, parent));
|
|
2311
|
-
}
|
|
2312
|
-
async function renameOpenCode(sessionId, title) {
|
|
2313
|
-
await (await openCode()).session.update({
|
|
2314
|
-
sessionID: sessionId,
|
|
2315
|
-
title
|
|
2316
|
-
});
|
|
2317
|
-
}
|
|
2318
|
-
async function removeOpenCode(sessionId) {
|
|
2319
|
-
await (await openCode()).session.remove({ sessionID: sessionId });
|
|
2320
|
-
}
|
|
2321
|
-
async function forkOpenCode(sessionId, messageId) {
|
|
2322
|
-
const client = await openCode();
|
|
2323
|
-
const messages = await allMessages(client, sessionId);
|
|
2324
|
-
const index = messages.findIndex((message) => message.id === messageId);
|
|
2325
|
-
if (index < 0) throw new Error("Cannot fork the session at that point");
|
|
2326
|
-
const next = messages.at(index + 1);
|
|
2327
|
-
return { sessionId: (await client.session.fork({
|
|
2328
|
-
sessionID: sessionId,
|
|
2329
|
-
...next === void 0 ? {} : { before: next.id }
|
|
2330
|
-
})).id };
|
|
2331
|
-
}
|
|
2332
|
-
async function rewindOpenCode(request) {
|
|
2333
|
-
if (request.code && !request.conversation && !request.dryRun) throw new Error("OpenCode restores files only with the conversation");
|
|
2334
|
-
const client = await openCode();
|
|
2335
|
-
const sessionID = request.sessionId;
|
|
2336
|
-
const files = (await client.session.revert.stage({
|
|
2337
|
-
sessionID,
|
|
2338
|
-
messageID: request.messageId,
|
|
2339
|
-
files: request.code
|
|
2340
|
-
})).files ?? [];
|
|
2341
|
-
const result = request.code ? {
|
|
2342
|
-
files: files.map((file) => file.file),
|
|
2343
|
-
insertions: files.reduce((sum, file) => sum + file.additions, 0),
|
|
2344
|
-
deletions: files.reduce((sum, file) => sum + file.deletions, 0)
|
|
2345
|
-
} : null;
|
|
2346
|
-
if (request.dryRun) await client.session.revert.clear({ sessionID });
|
|
2347
|
-
else await client.session.revert.commit({ sessionID });
|
|
2348
|
-
return result;
|
|
2349
|
-
}
|
|
2350
|
-
async function openCodeCommitMessage(cwd, model, prompt) {
|
|
2351
|
-
const client = await openCode();
|
|
2352
|
-
const session = await client.session.create({
|
|
2353
|
-
location: { directory: cwd },
|
|
2354
|
-
agent: "plan",
|
|
2355
|
-
model: modelRef(model, null)
|
|
2356
|
-
});
|
|
2357
|
-
temporary.add(session.id);
|
|
2358
|
-
try {
|
|
2359
|
-
await client.session.prompt({
|
|
2360
|
-
sessionID: session.id,
|
|
2361
|
-
text: prompt
|
|
2362
|
-
});
|
|
2363
|
-
await client.session.wait({ sessionID: session.id }, { signal: AbortSignal.timeout(COMMIT_LIMIT) });
|
|
2364
|
-
const messages = await allMessages(client, session.id);
|
|
2365
|
-
for (const message of messages.toReversed()) {
|
|
2366
|
-
if (message.type === "idle" && message.outcome === "failed") throw new Error(message.error?.message ?? "OpenCode failed");
|
|
2367
|
-
if (message.type !== "assistant") continue;
|
|
2368
|
-
const text = message.content.flatMap((part) => part.type === "text" ? [part.text] : []).join("").trim();
|
|
2369
|
-
if (text !== "") return { message: text };
|
|
2370
|
-
}
|
|
2371
|
-
throw new Error("OpenCode did not reply");
|
|
2372
|
-
} finally {
|
|
2373
|
-
await client.session.remove({ sessionID: session.id });
|
|
2374
|
-
temporary.delete(session.id);
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
async function contextOf(client, cwd, messages) {
|
|
2378
|
-
const last = messages.findLast((message) => message.type === "assistant" && message.tokens !== void 0);
|
|
2379
|
-
if (last?.type !== "assistant" || last.tokens === void 0) return null;
|
|
2380
|
-
const tokens = last.tokens;
|
|
2381
|
-
const model = (await client.model.list({ location: { directory: cwd } })).data.find((item) => item.providerID === last.model.providerID && item.id === last.model.id);
|
|
2382
|
-
return {
|
|
2383
|
-
tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write,
|
|
2384
|
-
window: model?.limit.context ?? null
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
function modelRef(value, effort) {
|
|
2388
|
-
const index = value.indexOf("/");
|
|
2389
|
-
if (index < 0) throw new Error(`${value} is not an OpenCode model`);
|
|
2390
|
-
return {
|
|
2391
|
-
providerID: value.slice(0, index),
|
|
2392
|
-
id: value.slice(index + 1),
|
|
2393
|
-
...effort === null ? {} : { variant: effort }
|
|
2394
|
-
};
|
|
2395
|
-
}
|
|
2396
|
-
async function allMessages(client, sessionId) {
|
|
2397
|
-
const messages = [];
|
|
2398
|
-
let cursor = null;
|
|
2399
|
-
do {
|
|
2400
|
-
const page = await client.message.list(cursor === null ? {
|
|
2401
|
-
sessionID: sessionId,
|
|
2402
|
-
limit: PAGE,
|
|
2403
|
-
order: "asc"
|
|
2404
|
-
} : {
|
|
2405
|
-
sessionID: sessionId,
|
|
2406
|
-
cursor
|
|
2407
|
-
});
|
|
2408
|
-
messages.push(...page.data);
|
|
2409
|
-
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2410
|
-
} while (cursor !== null);
|
|
2411
|
-
return messages;
|
|
2412
|
-
}
|
|
2413
|
-
function openCodeRun(sessionId) {
|
|
2414
|
-
const run = runs.get(sessionId);
|
|
2415
|
-
return run?.provider === "opencode" ? run : void 0;
|
|
2416
|
-
}
|
|
2417
|
-
//#endregion
|
|
2418
|
-
//#region src/lib/opencode/run.ts
|
|
2419
|
-
const IDLE_LIMIT$1 = 18e5;
|
|
2420
|
-
const RECENT = 30;
|
|
2421
|
-
const replies = {
|
|
2422
|
-
allow: "once",
|
|
2423
|
-
always: "always",
|
|
2424
|
-
deny: "reject"
|
|
2425
|
-
};
|
|
2426
|
-
var OpenCodeRun = class OpenCodeRun {
|
|
2427
|
-
provider = "opencode";
|
|
2428
|
-
sessionId;
|
|
2429
|
-
cwd;
|
|
2430
|
-
pending = /* @__PURE__ */ new Map();
|
|
2431
|
-
items = /* @__PURE__ */ new Map();
|
|
2432
|
-
closed;
|
|
2433
|
-
queue = [];
|
|
2434
|
-
settings;
|
|
2435
|
-
context = null;
|
|
2436
|
-
partial = "";
|
|
2437
|
-
running = false;
|
|
2438
|
-
prompted = false;
|
|
2439
|
-
backgroundTasks = 0;
|
|
2440
|
-
tasks = {};
|
|
2441
|
-
close;
|
|
2442
|
-
applied;
|
|
2443
|
-
loading = Promise.resolve();
|
|
2444
|
-
refreshTimer;
|
|
2445
|
-
idleTimer;
|
|
2446
|
-
static async start(cwd, settings) {
|
|
2447
|
-
const session = await (await openCode()).session.create({
|
|
2448
|
-
location: { directory: cwd },
|
|
2449
|
-
...settings.mode === null ? {} : { agent: settings.mode },
|
|
2450
|
-
...settings.model === null ? {} : { model: modelRef(settings.model, settings.effort) }
|
|
2451
|
-
});
|
|
2452
|
-
return new OpenCodeRun(session.id, cwd, settings, settings);
|
|
2453
|
-
}
|
|
2454
|
-
static async resume(sessionId, cwd, settings) {
|
|
2455
|
-
const session = await (await openCode()).session.get({ sessionID: sessionId });
|
|
2456
|
-
return new OpenCodeRun(sessionId, cwd, settings, sessionSettings(session, settings));
|
|
2457
|
-
}
|
|
2458
|
-
constructor(sessionId, cwd, settings, applied) {
|
|
2459
|
-
this.sessionId = sessionId;
|
|
2460
|
-
this.cwd = cwd;
|
|
2461
|
-
this.settings = settings;
|
|
2462
|
-
this.applied = applied;
|
|
2463
|
-
const closed = Promise.withResolvers();
|
|
2464
|
-
this.closed = closed.promise;
|
|
2465
|
-
this.close = closed.resolve;
|
|
2466
|
-
runs.set(sessionId, this);
|
|
2467
|
-
this.armIdleTimer();
|
|
2468
|
-
}
|
|
2469
|
-
async submit(prompt, delivery) {
|
|
2470
|
-
if (!this.running) {
|
|
2471
|
-
await this.startTurn(prompt);
|
|
2472
|
-
return;
|
|
2473
|
-
}
|
|
2474
|
-
switch (delivery) {
|
|
2475
|
-
case "steer":
|
|
2476
|
-
await (await openCode()).session.prompt({
|
|
2477
|
-
sessionID: this.sessionId,
|
|
2478
|
-
text: prompt.text,
|
|
2479
|
-
files: files(prompt),
|
|
2480
|
-
delivery: "steer"
|
|
2481
|
-
});
|
|
2482
|
-
return;
|
|
2483
|
-
case "queue":
|
|
2484
|
-
this.queue = [...this.queue, {
|
|
2485
|
-
id: randomUUID(),
|
|
2486
|
-
prompt
|
|
2487
|
-
}];
|
|
2488
|
-
this.publishQueue();
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
unqueue(id) {
|
|
2492
|
-
this.queue = this.queue.filter((item) => item.id !== id);
|
|
2493
|
-
this.publishQueue();
|
|
2084
|
+
unqueue(id) {
|
|
2085
|
+
this.queue = this.queue.filter((item) => item.id !== id);
|
|
2086
|
+
this.publishQueue();
|
|
2494
2087
|
}
|
|
2495
2088
|
steer(id) {
|
|
2496
2089
|
const item = this.queue.find((queued) => queued.id === id);
|
|
@@ -2616,187 +2209,704 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2616
2209
|
context
|
|
2617
2210
|
});
|
|
2618
2211
|
}
|
|
2619
|
-
async startTurn(prompt) {
|
|
2620
|
-
clearTimeout(this.idleTimer);
|
|
2621
|
-
this.running = true;
|
|
2622
|
-
this.prompted = true;
|
|
2623
|
-
broadcastActivity();
|
|
2624
|
-
try {
|
|
2625
|
-
await this.apply();
|
|
2626
|
-
await this.send(prompt);
|
|
2627
|
-
} catch (error) {
|
|
2628
|
-
this.running = false;
|
|
2629
|
-
broadcastActivity();
|
|
2630
|
-
this.armIdleTimer();
|
|
2631
|
-
throw error;
|
|
2632
|
-
}
|
|
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();
|
|
2337
|
+
}
|
|
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();
|
|
2346
|
+
}
|
|
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 = "";
|
|
2351
|
+
publish$2(this.sessionId, {
|
|
2352
|
+
type: "items",
|
|
2353
|
+
sessionId: this.sessionId,
|
|
2354
|
+
items
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
fail(error) {
|
|
2358
|
+
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2359
|
+
}
|
|
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);
|
|
2366
|
+
}
|
|
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
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
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 pairingCodes = snakeCase.table("pairing_codes", {
|
|
2427
|
+
hash: text().primaryKey(),
|
|
2428
|
+
name: text(),
|
|
2429
|
+
expiresAt: integer().notNull()
|
|
2430
|
+
});
|
|
2431
|
+
const devices = snakeCase.table("devices", {
|
|
2432
|
+
hash: text().primaryKey(),
|
|
2433
|
+
name: text(),
|
|
2434
|
+
createdAt: integer().notNull(),
|
|
2435
|
+
expiresAt: integer().notNull(),
|
|
2436
|
+
lastUsedAt: integer()
|
|
2437
|
+
});
|
|
2438
|
+
const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
2439
|
+
endpoint: text().primaryKey(),
|
|
2440
|
+
p256dh: text().notNull(),
|
|
2441
|
+
auth: text().notNull(),
|
|
2442
|
+
publicKey: text().notNull(),
|
|
2443
|
+
privateKey: text().notNull(),
|
|
2444
|
+
machine: text().notNull(),
|
|
2445
|
+
server: text().notNull()
|
|
2446
|
+
});
|
|
2447
|
+
//#endregion
|
|
2448
|
+
//#region src/services/projects.ts
|
|
2449
|
+
const LOGO_TYPES = [
|
|
2450
|
+
"image/png",
|
|
2451
|
+
"image/jpeg",
|
|
2452
|
+
"image/gif",
|
|
2453
|
+
"image/webp",
|
|
2454
|
+
"image/svg+xml",
|
|
2455
|
+
"image/x-icon",
|
|
2456
|
+
"image/vnd.microsoft.icon"
|
|
2457
|
+
];
|
|
2458
|
+
const LOGO_LIMIT = 524288;
|
|
2459
|
+
function projects$1() {
|
|
2460
|
+
return db.select().from(projects$2).all();
|
|
2461
|
+
}
|
|
2462
|
+
function projectPath(id) {
|
|
2463
|
+
const project = db.select().from(projects$2).where(eq(projects$2.id, id)).get();
|
|
2464
|
+
if (project === void 0) throw new Error("That project was removed");
|
|
2465
|
+
return project.path;
|
|
2466
|
+
}
|
|
2467
|
+
async function addProject(input) {
|
|
2468
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2469
|
+
if (!(await stat(folder)).isDirectory()) throw new Error(`${folder} is not a folder`);
|
|
2470
|
+
db.insert(projects$2).values({
|
|
2471
|
+
id: randomUUID(),
|
|
2472
|
+
path: folder,
|
|
2473
|
+
name: path.basename(folder) || folder,
|
|
2474
|
+
logo: null
|
|
2475
|
+
}).onConflictDoNothing({ target: projects$2.path }).run();
|
|
2476
|
+
publish$1();
|
|
2477
|
+
const project = projects$1().find((item) => item.path === folder);
|
|
2478
|
+
if (project === void 0) throw new Error(`Could not add ${folder}`);
|
|
2479
|
+
return project;
|
|
2480
|
+
}
|
|
2481
|
+
function updateProject(id, patch) {
|
|
2482
|
+
if (patch.name !== void 0) {
|
|
2483
|
+
const name = patch.name.trim();
|
|
2484
|
+
if (name === "") throw new Error("Project name cannot be empty");
|
|
2485
|
+
db.update(projects$2).set({ name }).where(eq(projects$2.id, id)).run();
|
|
2486
|
+
}
|
|
2487
|
+
if (patch.logo !== void 0) {
|
|
2488
|
+
if (patch.logo !== null) checkLogo(patch.logo);
|
|
2489
|
+
db.update(projects$2).set({ logo: patch.logo }).where(eq(projects$2.id, id)).run();
|
|
2633
2490
|
}
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2491
|
+
publish$1();
|
|
2492
|
+
return null;
|
|
2493
|
+
}
|
|
2494
|
+
function removeProject(id) {
|
|
2495
|
+
db.delete(projects$2).where(eq(projects$2.id, id)).run();
|
|
2496
|
+
publish$1();
|
|
2497
|
+
}
|
|
2498
|
+
async function directories(input) {
|
|
2499
|
+
const folder = path.resolve(expandHome(input.trim()));
|
|
2500
|
+
const entries = await readdir(folder, { withFileTypes: true });
|
|
2501
|
+
const parent = path.dirname(folder);
|
|
2502
|
+
return {
|
|
2503
|
+
path: folder,
|
|
2504
|
+
parent: parent === folder ? null : parent,
|
|
2505
|
+
directories: entries.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
2506
|
+
name: entry.name,
|
|
2507
|
+
path: path.join(folder, entry.name)
|
|
2508
|
+
})).sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function publish$1() {
|
|
2512
|
+
broadcast({
|
|
2513
|
+
type: "projects",
|
|
2514
|
+
projects: projects$1()
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
function checkLogo(logo) {
|
|
2518
|
+
const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
|
|
2519
|
+
if (type === void 0 || !LOGO_TYPES.includes(type)) throw new Error("Use a PNG, JPEG, GIF, WebP, SVG or ICO image");
|
|
2520
|
+
if (logo.length > LOGO_LIMIT) throw new Error("Use an image smaller than 384 KB");
|
|
2521
|
+
}
|
|
2522
|
+
function expandHome(input) {
|
|
2523
|
+
if (input === "~") return os.homedir();
|
|
2524
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
2525
|
+
return input;
|
|
2526
|
+
}
|
|
2527
|
+
//#endregion
|
|
2528
|
+
//#region src/services/sessions.ts
|
|
2529
|
+
const day = 864e5;
|
|
2530
|
+
const LIST_TIMEOUT = 1e4;
|
|
2531
|
+
const REFRESH_DELAY = 1e3;
|
|
2532
|
+
const allProviders = [
|
|
2533
|
+
"claude",
|
|
2534
|
+
"codex",
|
|
2535
|
+
"opencode"
|
|
2536
|
+
];
|
|
2537
|
+
const listed = {
|
|
2538
|
+
claude: [],
|
|
2539
|
+
codex: [],
|
|
2540
|
+
opencode: []
|
|
2541
|
+
};
|
|
2542
|
+
const statuses = {
|
|
2543
|
+
claude: {
|
|
2544
|
+
provider: "claude",
|
|
2545
|
+
state: "loading",
|
|
2546
|
+
error: null
|
|
2547
|
+
},
|
|
2548
|
+
codex: {
|
|
2549
|
+
provider: "codex",
|
|
2550
|
+
state: "loading",
|
|
2551
|
+
error: null
|
|
2552
|
+
},
|
|
2553
|
+
opencode: {
|
|
2554
|
+
provider: "opencode",
|
|
2555
|
+
state: "loading",
|
|
2556
|
+
error: null
|
|
2669
2557
|
}
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
mode: settings.mode ?? this.applied.mode
|
|
2558
|
+
};
|
|
2559
|
+
const refreshing = /* @__PURE__ */ new Map();
|
|
2560
|
+
const timers = /* @__PURE__ */ new Map();
|
|
2561
|
+
const providers = /* @__PURE__ */ new Map();
|
|
2562
|
+
let watchers = [];
|
|
2563
|
+
function sessions() {
|
|
2564
|
+
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
2565
|
+
const known = new Set(projects$1().map((project) => project.id));
|
|
2566
|
+
return allProviders.flatMap((provider) => listed[provider].filter((item) => known.has(item.projectId)).map((item) => {
|
|
2567
|
+
const record = records.get(item.id);
|
|
2568
|
+
const updatedAt = record?.interactedAt ?? item.updatedAt;
|
|
2569
|
+
return {
|
|
2570
|
+
...item,
|
|
2571
|
+
updatedAt,
|
|
2572
|
+
status: record === void 0 || record.status === "active" && Date.now() - updatedAt > day ? "inactive" : record.status
|
|
2686
2573
|
};
|
|
2574
|
+
}));
|
|
2575
|
+
}
|
|
2576
|
+
function providerStatuses() {
|
|
2577
|
+
return allProviders.map((provider) => statuses[provider]);
|
|
2578
|
+
}
|
|
2579
|
+
function listedUpdatedAt(provider, sessionId) {
|
|
2580
|
+
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
2581
|
+
}
|
|
2582
|
+
function providerOf(sessionId) {
|
|
2583
|
+
const provider = runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
2584
|
+
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
2585
|
+
return provider;
|
|
2586
|
+
}
|
|
2587
|
+
function recordInteraction(id) {
|
|
2588
|
+
db.insert(sessions$1).values({
|
|
2589
|
+
sessionId: id,
|
|
2590
|
+
interactedAt: Date.now(),
|
|
2591
|
+
status: "active"
|
|
2592
|
+
}).onConflictDoUpdate({
|
|
2593
|
+
target: sessions$1.sessionId,
|
|
2594
|
+
set: {
|
|
2595
|
+
interactedAt: Date.now(),
|
|
2596
|
+
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
2597
|
+
}
|
|
2598
|
+
}).run();
|
|
2599
|
+
}
|
|
2600
|
+
function publishSessions() {
|
|
2601
|
+
broadcast({
|
|
2602
|
+
type: "sessions",
|
|
2603
|
+
sessions: sessions()
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
function refreshAll() {
|
|
2607
|
+
for (const provider of allProviders) refresh(provider);
|
|
2608
|
+
}
|
|
2609
|
+
function refreshSessions(provider) {
|
|
2610
|
+
if (timers.has(provider)) return;
|
|
2611
|
+
timers.set(provider, setTimeout(() => {
|
|
2612
|
+
timers.delete(provider);
|
|
2613
|
+
refresh(provider);
|
|
2614
|
+
}, REFRESH_DELAY));
|
|
2615
|
+
}
|
|
2616
|
+
async function refresh(provider) {
|
|
2617
|
+
const current = refreshing.get(provider);
|
|
2618
|
+
if (current !== void 0) {
|
|
2619
|
+
current.again = true;
|
|
2620
|
+
return;
|
|
2687
2621
|
}
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
request: {
|
|
2694
|
-
id: permission.id,
|
|
2695
|
-
kind: "approval",
|
|
2696
|
-
title: permission.message ?? `Allow ${permission.action} ${permission.resources.join(", ")}?`,
|
|
2697
|
-
canAlwaysAllow: (permission.save ?? []).length > 0,
|
|
2698
|
-
item: item ?? {
|
|
2699
|
-
id: permission.id,
|
|
2700
|
-
parent: null,
|
|
2701
|
-
time: Date.now(),
|
|
2702
|
-
kind: "tool",
|
|
2703
|
-
name: permission.action,
|
|
2704
|
-
input: { resources: permission.resources },
|
|
2705
|
-
output: "",
|
|
2706
|
-
status: "running"
|
|
2707
|
-
}
|
|
2708
|
-
}
|
|
2709
|
-
});
|
|
2622
|
+
const entry = { again: true };
|
|
2623
|
+
refreshing.set(provider, entry);
|
|
2624
|
+
while (entry.again) {
|
|
2625
|
+
entry.again = false;
|
|
2626
|
+
await list(provider);
|
|
2710
2627
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2628
|
+
refreshing.delete(provider);
|
|
2629
|
+
}
|
|
2630
|
+
async function list(provider) {
|
|
2631
|
+
const paths = new Map(projects$1().map((project) => [project.path, project.id]));
|
|
2632
|
+
const before = statuses[provider];
|
|
2633
|
+
let timer;
|
|
2634
|
+
try {
|
|
2635
|
+
if (!readConfig().enabledProviders[provider]) {
|
|
2636
|
+
listed[provider] = [];
|
|
2637
|
+
statuses[provider] = {
|
|
2638
|
+
provider,
|
|
2639
|
+
state: "disabled",
|
|
2640
|
+
error: null
|
|
2641
|
+
};
|
|
2642
|
+
return;
|
|
2643
|
+
}
|
|
2644
|
+
const found = await Promise.race([find(provider, paths), new Promise((_, reject) => {
|
|
2645
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${providerName(provider)} did not list its sessions within ${LIST_TIMEOUT / 1e3} seconds`)), LIST_TIMEOUT);
|
|
2646
|
+
})]);
|
|
2647
|
+
if (found === null) {
|
|
2648
|
+
listed[provider] = [];
|
|
2649
|
+
statuses[provider] = {
|
|
2650
|
+
provider,
|
|
2651
|
+
state: "missing",
|
|
2652
|
+
error: null
|
|
2653
|
+
};
|
|
2654
|
+
} else {
|
|
2655
|
+
listed[provider] = found;
|
|
2656
|
+
statuses[provider] = {
|
|
2657
|
+
provider,
|
|
2658
|
+
state: "ready",
|
|
2659
|
+
error: null
|
|
2660
|
+
};
|
|
2661
|
+
for (const item of found) providers.set(item.id, provider);
|
|
2662
|
+
}
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2665
|
+
console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
2666
|
+
statuses[provider] = {
|
|
2667
|
+
provider,
|
|
2668
|
+
state: "error",
|
|
2669
|
+
error: message
|
|
2670
|
+
};
|
|
2671
|
+
} finally {
|
|
2672
|
+
clearTimeout(timer);
|
|
2673
|
+
publishSessions();
|
|
2674
|
+
if (JSON.stringify(statuses[provider]) !== JSON.stringify(before)) broadcast({
|
|
2675
|
+
type: "providers",
|
|
2676
|
+
providers: providerStatuses()
|
|
2720
2677
|
});
|
|
2721
2678
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2679
|
+
}
|
|
2680
|
+
async function find(provider, paths) {
|
|
2681
|
+
switch (provider) {
|
|
2682
|
+
case "claude": return (await Promise.all([...paths].map(async ([cwd, projectId]) => (await listSessions({
|
|
2683
|
+
dir: cwd,
|
|
2684
|
+
includeWorktrees: false,
|
|
2685
|
+
includeProgrammatic: false
|
|
2686
|
+
})).map((item) => ({
|
|
2687
|
+
id: item.sessionId,
|
|
2688
|
+
provider: "claude",
|
|
2689
|
+
title: item.summary,
|
|
2690
|
+
projectId,
|
|
2691
|
+
branch: item.gitBranch ?? null,
|
|
2692
|
+
updatedAt: item.lastModified
|
|
2693
|
+
}))))).flat();
|
|
2694
|
+
case "codex":
|
|
2695
|
+
if (!codexInstalled()) return null;
|
|
2696
|
+
return (await codexThreads([...paths.keys()])).flatMap((thread) => {
|
|
2697
|
+
const projectId = paths.get(thread.cwd);
|
|
2698
|
+
if (projectId === void 0) return [];
|
|
2699
|
+
return [{
|
|
2700
|
+
id: thread.id,
|
|
2701
|
+
provider: "codex",
|
|
2702
|
+
title: thread.name ?? thread.preview,
|
|
2703
|
+
projectId,
|
|
2704
|
+
branch: thread.gitInfo?.branch ?? null,
|
|
2705
|
+
updatedAt: thread.updatedAt * 1e3
|
|
2706
|
+
}];
|
|
2707
|
+
});
|
|
2708
|
+
case "opencode":
|
|
2709
|
+
if (!openCodeInstalled()) return null;
|
|
2710
|
+
return (await openCodeSessions([...paths.keys()])).flatMap((session) => {
|
|
2711
|
+
const projectId = paths.get(session.location.directory);
|
|
2712
|
+
if (projectId === void 0) return [];
|
|
2713
|
+
return [{
|
|
2714
|
+
id: session.id,
|
|
2715
|
+
provider: "opencode",
|
|
2716
|
+
title: session.title ?? "New session",
|
|
2717
|
+
projectId,
|
|
2718
|
+
branch: null,
|
|
2719
|
+
updatedAt: session.time.updated
|
|
2720
|
+
}];
|
|
2721
|
+
});
|
|
2730
2722
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2723
|
+
}
|
|
2724
|
+
function watchSessions() {
|
|
2725
|
+
for (const watcher of watchers) watcher.close();
|
|
2726
|
+
watchers = [["claude", path.join(readConfig().claudeBaseDir, "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
2727
|
+
if (!readConfig().enabledProviders[provider]) return [];
|
|
2728
|
+
try {
|
|
2729
|
+
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
2730
|
+
watcher.on("error", (error) => console.warn(`stopped watching ${dir}: ${error}`));
|
|
2731
|
+
return [watcher];
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
if (error.code !== "ENOENT") throw error;
|
|
2734
|
+
return [];
|
|
2739
2735
|
}
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
async function updateSession(id, patch) {
|
|
2739
|
+
if (patch.title !== void 0) switch (providerOf(id)) {
|
|
2740
|
+
case "claude":
|
|
2741
|
+
await renameSession(id, patch.title);
|
|
2742
|
+
break;
|
|
2743
|
+
case "codex":
|
|
2744
|
+
await renameThread(id, patch.title);
|
|
2745
|
+
break;
|
|
2746
|
+
case "opencode": await renameOpenCode(id, patch.title);
|
|
2743
2747
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
}
|
|
2751
|
-
|
|
2748
|
+
if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2749
|
+
else if (patch.status !== void 0) db.insert(sessions$1).values({
|
|
2750
|
+
sessionId: id,
|
|
2751
|
+
status: patch.status
|
|
2752
|
+
}).onConflictDoUpdate({
|
|
2753
|
+
target: sessions$1.sessionId,
|
|
2754
|
+
set: { status: patch.status }
|
|
2755
|
+
}).run();
|
|
2756
|
+
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
2757
|
+
}
|
|
2758
|
+
async function removeSession(id) {
|
|
2759
|
+
const provider = providerOf(id);
|
|
2760
|
+
const run = runs.get(id);
|
|
2761
|
+
if (run !== void 0) {
|
|
2762
|
+
run.stop();
|
|
2763
|
+
await run.closed;
|
|
2752
2764
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
});
|
|
2765
|
+
switch (provider) {
|
|
2766
|
+
case "claude":
|
|
2767
|
+
await deleteSession(id);
|
|
2768
|
+
break;
|
|
2769
|
+
case "codex":
|
|
2770
|
+
await deleteThread(id);
|
|
2771
|
+
break;
|
|
2772
|
+
case "opencode": await removeOpenCode(id);
|
|
2762
2773
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2774
|
+
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
2775
|
+
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
2776
|
+
}
|
|
2777
|
+
async function fork(sessionId, projectId, messageId) {
|
|
2778
|
+
const provider = providerOf(sessionId);
|
|
2779
|
+
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
2780
|
+
providers.set(result.sessionId, provider);
|
|
2781
|
+
recordInteraction(result.sessionId);
|
|
2782
|
+
refreshSessions(provider);
|
|
2783
|
+
return result;
|
|
2784
|
+
}
|
|
2785
|
+
async function forkWith(provider, sessionId, cwd, messageId) {
|
|
2786
|
+
if (provider === "codex") return forkThread(sessionId, messageId);
|
|
2787
|
+
if (provider === "opencode") return forkOpenCode(sessionId, messageId);
|
|
2788
|
+
if ((await getSessionMessages(sessionId, { dir: cwd })).findIndex((item) => item.uuid === messageId) < 0) throw new Error("Cannot fork the session at that point");
|
|
2789
|
+
return { sessionId: (await forkSession(sessionId, {
|
|
2790
|
+
dir: cwd,
|
|
2791
|
+
upToMessageId: messageId
|
|
2792
|
+
})).sessionId };
|
|
2793
|
+
}
|
|
2794
|
+
//#endregion
|
|
2795
|
+
//#region src/lib/opencode/client.ts
|
|
2796
|
+
const listChanges = /* @__PURE__ */ new Set([
|
|
2797
|
+
"session.created",
|
|
2798
|
+
"session.renamed",
|
|
2799
|
+
"session.moved",
|
|
2800
|
+
"session.forked",
|
|
2801
|
+
"session.deleted",
|
|
2802
|
+
"session.execution.succeeded",
|
|
2803
|
+
"session.execution.failed",
|
|
2804
|
+
"session.execution.interrupted"
|
|
2805
|
+
]);
|
|
2806
|
+
const SILENCE_LIMIT$1 = 45e3;
|
|
2807
|
+
const RECONNECT_ATTEMPTS = 5;
|
|
2808
|
+
const RECONNECT_DELAY = 2e3;
|
|
2809
|
+
let connection;
|
|
2810
|
+
let listener;
|
|
2811
|
+
function findOpenCode() {
|
|
2812
|
+
const name = process.platform === "win32" ? "opencode.exe" : "opencode";
|
|
2813
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
2814
|
+
const candidate = path.join(dir, name);
|
|
2815
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isFile()) return candidate;
|
|
2765
2816
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2817
|
+
return null;
|
|
2818
|
+
}
|
|
2819
|
+
function openCode() {
|
|
2820
|
+
connection ??= start();
|
|
2821
|
+
return connection;
|
|
2822
|
+
}
|
|
2823
|
+
function disconnectOpenCode() {
|
|
2824
|
+
const controller = listener;
|
|
2825
|
+
listener = void 0;
|
|
2826
|
+
connection = void 0;
|
|
2827
|
+
controller?.abort();
|
|
2828
|
+
}
|
|
2829
|
+
async function start() {
|
|
2830
|
+
try {
|
|
2831
|
+
const client = await connect();
|
|
2832
|
+
listen(client);
|
|
2833
|
+
return client;
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
connection = void 0;
|
|
2836
|
+
throw error;
|
|
2772
2837
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2838
|
+
}
|
|
2839
|
+
async function connect() {
|
|
2840
|
+
const endpoint = await ensure();
|
|
2841
|
+
return OpenCode.make({
|
|
2842
|
+
baseUrl: endpoint.url,
|
|
2843
|
+
headers: headers(endpoint)
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
async function listen(client) {
|
|
2847
|
+
const controller = new AbortController();
|
|
2848
|
+
listener = controller;
|
|
2849
|
+
const silence = setTimeout(() => controller.abort(), SILENCE_LIMIT$1);
|
|
2850
|
+
try {
|
|
2851
|
+
for await (const event of client.event.subscribe({
|
|
2852
|
+
signal: controller.signal,
|
|
2853
|
+
onActivity: () => silence.refresh()
|
|
2854
|
+
})) route(event);
|
|
2855
|
+
console.warn("the OpenCode event stream ended");
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
if (listener !== controller) return;
|
|
2858
|
+
console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$1 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
|
|
2859
|
+
} finally {
|
|
2860
|
+
clearTimeout(silence);
|
|
2861
|
+
}
|
|
2862
|
+
if (listener !== controller) return;
|
|
2863
|
+
connection = reconnect();
|
|
2864
|
+
connection.catch(() => void 0);
|
|
2865
|
+
}
|
|
2866
|
+
async function reconnect() {
|
|
2867
|
+
let failure;
|
|
2868
|
+
for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
|
|
2869
|
+
await setTimeout$1(RECONNECT_DELAY);
|
|
2870
|
+
let client;
|
|
2871
|
+
try {
|
|
2872
|
+
client = await connect();
|
|
2873
|
+
} catch (error) {
|
|
2874
|
+
failure = error;
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
listen(client);
|
|
2878
|
+
await catchUp(client).catch((error) => console.warn(`could not catch up with OpenCode: ${error}`));
|
|
2879
|
+
return client;
|
|
2780
2880
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2881
|
+
console.warn(`could not reconnect to OpenCode: ${failure}`);
|
|
2882
|
+
connection = void 0;
|
|
2883
|
+
for (const run of openCodeRuns()) run.exited();
|
|
2884
|
+
refreshSessions("opencode");
|
|
2885
|
+
throw failure;
|
|
2886
|
+
}
|
|
2887
|
+
async function catchUp(client) {
|
|
2888
|
+
const active = await client.session.active();
|
|
2889
|
+
for (const run of openCodeRuns()) {
|
|
2890
|
+
const [permissions, forms] = await Promise.all([client.permission.request.list({ location: { directory: run.cwd } }), client.session.form.list({ sessionID: run.sessionId })]);
|
|
2891
|
+
run.reconcile(client, run.sessionId in active, permissions.data.filter((permission) => permission.sessionID === run.sessionId), forms);
|
|
2787
2892
|
}
|
|
2788
|
-
|
|
2789
|
-
function sessionSettings(session, settings) {
|
|
2790
|
-
const variant = session.model?.variant;
|
|
2791
|
-
return {
|
|
2792
|
-
...settings,
|
|
2793
|
-
model: session.model === void 0 ? null : `${session.model.providerID}/${session.model.id}`,
|
|
2794
|
-
effort: variant === void 0 || variant === "default" ? null : variant,
|
|
2795
|
-
mode: session.agent ?? null
|
|
2796
|
-
};
|
|
2893
|
+
refreshSessions("opencode");
|
|
2797
2894
|
}
|
|
2798
|
-
function
|
|
2799
|
-
|
|
2895
|
+
function route(event) {
|
|
2896
|
+
if (listChanges.has(event.type)) refreshSessions("opencode");
|
|
2897
|
+
const sessionId = sessionOf(event);
|
|
2898
|
+
if (sessionId === null) return;
|
|
2899
|
+
const run = runs.get(sessionId);
|
|
2900
|
+
if (run?.provider === "opencode") run.receive(event);
|
|
2901
|
+
}
|
|
2902
|
+
function sessionOf(event) {
|
|
2903
|
+
if (event.type === "form.created") return event.data.form.sessionID;
|
|
2904
|
+
const data = "data" in event ? event.data : null;
|
|
2905
|
+
if (isRecord(data) && typeof data.sessionID === "string") return data.sessionID;
|
|
2906
|
+
return null;
|
|
2907
|
+
}
|
|
2908
|
+
function openCodeRuns() {
|
|
2909
|
+
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
2800
2910
|
}
|
|
2801
2911
|
//#endregion
|
|
2802
2912
|
//#region src/services/accounts.ts
|
|
@@ -2809,7 +2919,36 @@ function providerName(provider) {
|
|
|
2809
2919
|
return providerNames[provider];
|
|
2810
2920
|
}
|
|
2811
2921
|
function storedAccounts() {
|
|
2812
|
-
return readJson("accounts.json") ?? []
|
|
2922
|
+
return (readJson("accounts.json") ?? []).map((item) => ({
|
|
2923
|
+
id: item.id,
|
|
2924
|
+
provider: item.provider,
|
|
2925
|
+
name: item.name,
|
|
2926
|
+
configDir: item.configDir,
|
|
2927
|
+
enabled: item.enabled ?? true,
|
|
2928
|
+
hiddenModels: item.hiddenModels ?? {}
|
|
2929
|
+
}));
|
|
2930
|
+
}
|
|
2931
|
+
function assertEnabled(provider, accountId) {
|
|
2932
|
+
if (!readConfig().enabledProviders[provider]) throw new Error(`${providerName(provider)} is disabled in Settings`);
|
|
2933
|
+
if (provider === "opencode") return;
|
|
2934
|
+
if (!storedAccount(provider, accountId).enabled) throw new Error("That account is disabled in Settings");
|
|
2935
|
+
}
|
|
2936
|
+
async function stopProvider(provider) {
|
|
2937
|
+
const stopping = [...runs.values()].filter((run) => run.provider === provider);
|
|
2938
|
+
for (const run of stopping) run.stop();
|
|
2939
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2940
|
+
if (provider === "codex") stopAppServers();
|
|
2941
|
+
if (provider === "opencode") disconnectOpenCode();
|
|
2942
|
+
if (provider === "claude") {
|
|
2943
|
+
for (const account of storedAccounts()) if (account.provider === "claude") forgetClaude(account.id);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
async function stopAccount(account) {
|
|
2947
|
+
const stopping = [...runs.values()].filter((run) => run.settings.accountId === account.id);
|
|
2948
|
+
for (const run of stopping) run.stop();
|
|
2949
|
+
await Promise.all(stopping.map((run) => run.closed));
|
|
2950
|
+
if (account.provider === "codex") stopAppServer(account.configDir);
|
|
2951
|
+
if (account.provider === "claude") forgetClaude(account.id);
|
|
2813
2952
|
}
|
|
2814
2953
|
function storedAccount(provider, accountId) {
|
|
2815
2954
|
if (accountId === null) throw new Error(`Add a ${providerName(provider)} account in Settings → Accounts`);
|
|
@@ -2820,49 +2959,92 @@ function storedAccount(provider, accountId) {
|
|
|
2820
2959
|
}
|
|
2821
2960
|
async function accounts() {
|
|
2822
2961
|
const stored = await Promise.all(storedAccounts().map((account) => {
|
|
2962
|
+
if (!account.enabled || !readConfig().enabledProviders[account.provider]) return disabledAccount(account);
|
|
2823
2963
|
switch (account.provider) {
|
|
2824
2964
|
case "claude": return claudeAccount(account);
|
|
2825
2965
|
case "codex": return codexAccount(account);
|
|
2826
|
-
case "opencode": throw new Error("OpenCode has no stored accounts");
|
|
2827
2966
|
}
|
|
2828
2967
|
}));
|
|
2829
|
-
return
|
|
2968
|
+
return {
|
|
2969
|
+
claudeAccounts: stored.filter((item) => item.provider === "claude"),
|
|
2970
|
+
codexAccounts: stored.filter((item) => item.provider === "codex"),
|
|
2971
|
+
openCode: { available: openCodeInstalled() && readConfig().enabledProviders.opencode }
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
function disabledAccount(account) {
|
|
2975
|
+
return {
|
|
2976
|
+
...account,
|
|
2977
|
+
email: null,
|
|
2978
|
+
plan: null,
|
|
2979
|
+
error: null,
|
|
2980
|
+
limits: [],
|
|
2981
|
+
linked: true,
|
|
2982
|
+
valid: false
|
|
2983
|
+
};
|
|
2830
2984
|
}
|
|
2831
2985
|
async function publishAccounts() {
|
|
2832
2986
|
try {
|
|
2833
2987
|
broadcast({
|
|
2834
2988
|
type: "accounts",
|
|
2835
|
-
|
|
2989
|
+
...await accounts()
|
|
2836
2990
|
});
|
|
2837
2991
|
} catch (error) {
|
|
2838
2992
|
console.warn(`could not list accounts: ${error}`);
|
|
2839
2993
|
}
|
|
2840
2994
|
}
|
|
2841
|
-
function capabilities(accountId, cwd) {
|
|
2842
|
-
|
|
2843
|
-
const
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2995
|
+
async function capabilities(provider, accountId, cwd) {
|
|
2996
|
+
assertEnabled(provider, accountId);
|
|
2997
|
+
const result = await providerCapabilities(provider, accountId, cwd);
|
|
2998
|
+
const settings = readConfig().models[provider];
|
|
2999
|
+
const overrides = provider === "opencode" ? {} : storedAccount(provider, accountId).hiddenModels;
|
|
3000
|
+
return {
|
|
3001
|
+
...result,
|
|
3002
|
+
defaultModel: defaultModels[provider],
|
|
3003
|
+
models: result.models.map((model) => {
|
|
3004
|
+
const deprecated = deprecatedModels[provider].includes(model.value);
|
|
3005
|
+
const setting = settings[model.value] ?? {
|
|
3006
|
+
hidden: deprecated,
|
|
3007
|
+
effort: null
|
|
3008
|
+
};
|
|
3009
|
+
return {
|
|
3010
|
+
...model,
|
|
3011
|
+
deprecated,
|
|
3012
|
+
hidden: overrides[model.value] ?? setting.hidden,
|
|
3013
|
+
effort: setting.effort
|
|
3014
|
+
};
|
|
3015
|
+
})
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
function providerCapabilities(provider, accountId, cwd) {
|
|
3019
|
+
switch (provider) {
|
|
3020
|
+
case "claude": return claudeCapabilities(storedAccount(provider, accountId), cwd);
|
|
3021
|
+
case "codex": return codexCapabilities(storedAccount(provider, accountId), cwd);
|
|
3022
|
+
case "opencode":
|
|
3023
|
+
if (accountId !== null) throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
3024
|
+
return openCodeCapabilities(cwd);
|
|
2849
3025
|
}
|
|
2850
3026
|
}
|
|
2851
3027
|
function addAccount(provider, name, configDir) {
|
|
2852
|
-
if (provider === "opencode") throw new Error("OpenCode has no accounts. Connect providers in OpenCode");
|
|
2853
3028
|
saveAccounts([...storedAccounts(), {
|
|
2854
3029
|
id: randomUUID(),
|
|
2855
3030
|
provider,
|
|
2856
|
-
...validAccount(name, configDir)
|
|
3031
|
+
...validAccount(name, configDir),
|
|
3032
|
+
enabled: true,
|
|
3033
|
+
hiddenModels: {}
|
|
2857
3034
|
}]);
|
|
2858
3035
|
}
|
|
2859
|
-
function updateAccount(
|
|
2860
|
-
const
|
|
2861
|
-
|
|
3036
|
+
async function updateAccount(update) {
|
|
3037
|
+
const before = storedAccounts().find((item) => item.id === update.id);
|
|
3038
|
+
if (before === void 0) throw new Error("That account was removed");
|
|
3039
|
+
const account = validAccount(update.name, update.configDir);
|
|
3040
|
+
saveAccounts(storedAccounts().map((item) => item.id === update.id ? {
|
|
2862
3041
|
...item,
|
|
2863
|
-
...account
|
|
3042
|
+
...account,
|
|
3043
|
+
enabled: update.enabled,
|
|
3044
|
+
hiddenModels: update.hiddenModels
|
|
2864
3045
|
} : item));
|
|
2865
|
-
forgetClaude(id);
|
|
3046
|
+
forgetClaude(update.id);
|
|
3047
|
+
if (before.enabled && !update.enabled) await stopAccount(before);
|
|
2866
3048
|
}
|
|
2867
3049
|
function removeAccount(id) {
|
|
2868
3050
|
saveAccounts(storedAccounts().filter((item) => item.id !== id));
|
|
@@ -2885,81 +3067,6 @@ function saveAccounts(items) {
|
|
|
2885
3067
|
writeJson("accounts.json", items);
|
|
2886
3068
|
}
|
|
2887
3069
|
//#endregion
|
|
2888
|
-
//#region src/lib/claude/content.ts
|
|
2889
|
-
function toMessage(type, item, time) {
|
|
2890
|
-
return {
|
|
2891
|
-
type,
|
|
2892
|
-
uuid: item.uuid,
|
|
2893
|
-
time,
|
|
2894
|
-
content: isRecord(item.message) ? toBlocks(item.message.content, answersOf(item.tool_use_result)) : [],
|
|
2895
|
-
parent: item.parent_tool_use_id
|
|
2896
|
-
};
|
|
2897
|
-
}
|
|
2898
|
-
function contextTokens(message) {
|
|
2899
|
-
if (!isRecord(message) || !isRecord(message.usage)) return null;
|
|
2900
|
-
const usage = message.usage;
|
|
2901
|
-
return [
|
|
2902
|
-
usage.input_tokens,
|
|
2903
|
-
usage.cache_creation_input_tokens,
|
|
2904
|
-
usage.cache_read_input_tokens,
|
|
2905
|
-
usage.output_tokens
|
|
2906
|
-
].reduce((total, value) => total + (typeof value === "number" ? value : 0), 0);
|
|
2907
|
-
}
|
|
2908
|
-
function answersOf(output) {
|
|
2909
|
-
if (!isRecord(output) || !isRecord(output.answers)) return null;
|
|
2910
|
-
const answers = {};
|
|
2911
|
-
for (const [question, answer] of Object.entries(output.answers)) {
|
|
2912
|
-
if (typeof answer !== "string") return null;
|
|
2913
|
-
answers[question] = answer;
|
|
2914
|
-
}
|
|
2915
|
-
return answers;
|
|
2916
|
-
}
|
|
2917
|
-
function toBlocks(content, answers) {
|
|
2918
|
-
if (typeof content === "string") return [{
|
|
2919
|
-
type: "text",
|
|
2920
|
-
text: content
|
|
2921
|
-
}];
|
|
2922
|
-
if (!Array.isArray(content)) return [];
|
|
2923
|
-
return content.flatMap((block) => {
|
|
2924
|
-
if (!isRecord(block)) return [];
|
|
2925
|
-
switch (block.type) {
|
|
2926
|
-
case "text": return typeof block.text === "string" && !block.text.startsWith("<system-reminder>") && !block.text.startsWith("<local-command-caveat>") ? [{
|
|
2927
|
-
type: "text",
|
|
2928
|
-
text: block.text
|
|
2929
|
-
}] : [];
|
|
2930
|
-
case "thinking": return typeof block.thinking === "string" && block.thinking !== "" ? [{
|
|
2931
|
-
type: "thinking",
|
|
2932
|
-
thinking: block.thinking
|
|
2933
|
-
}] : [];
|
|
2934
|
-
case "image": {
|
|
2935
|
-
const source = block.source;
|
|
2936
|
-
return isRecord(source) && source.type === "base64" && typeof source.media_type === "string" && typeof source.data === "string" ? [{
|
|
2937
|
-
type: "image",
|
|
2938
|
-
mediaType: source.media_type,
|
|
2939
|
-
data: source.data
|
|
2940
|
-
}] : [];
|
|
2941
|
-
}
|
|
2942
|
-
case "tool_use": return typeof block.id === "string" && typeof block.name === "string" && isRecord(block.input) ? [{
|
|
2943
|
-
type: "tool_use",
|
|
2944
|
-
id: block.id,
|
|
2945
|
-
name: block.name,
|
|
2946
|
-
input: block.input
|
|
2947
|
-
}] : [];
|
|
2948
|
-
case "tool_result": return typeof block.tool_use_id === "string" ? [{
|
|
2949
|
-
type: "tool_result",
|
|
2950
|
-
toolUseId: block.tool_use_id,
|
|
2951
|
-
content: toBlocks(block.content, null).filter((item) => item.type === "text"),
|
|
2952
|
-
isError: block.is_error === true,
|
|
2953
|
-
answers
|
|
2954
|
-
}] : [];
|
|
2955
|
-
default: return [];
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
}
|
|
2959
|
-
function isRecord(value) {
|
|
2960
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2961
|
-
}
|
|
2962
|
-
//#endregion
|
|
2963
3070
|
//#region src/lib/claude/claude.ts
|
|
2964
3071
|
const efforts = [
|
|
2965
3072
|
"low",
|
|
@@ -3002,13 +3109,15 @@ function claudeMode(value) {
|
|
|
3002
3109
|
}
|
|
3003
3110
|
function claudeOptions(accountId, cwd) {
|
|
3004
3111
|
const account = storedAccount("claude", accountId);
|
|
3112
|
+
const env = {
|
|
3113
|
+
...process.env,
|
|
3114
|
+
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
3115
|
+
CLAUDE_CONFIG_DIR: account.configDir
|
|
3116
|
+
};
|
|
3117
|
+
if (path.resolve(account.configDir) === path.join(os.homedir(), ".claude")) delete env.CLAUDE_CONFIG_DIR;
|
|
3005
3118
|
return {
|
|
3006
3119
|
cwd,
|
|
3007
|
-
env
|
|
3008
|
-
...process.env,
|
|
3009
|
-
CLAUDE_CODE_ENTRYPOINT: "offsite",
|
|
3010
|
-
CLAUDE_CONFIG_DIR: account.configDir
|
|
3011
|
-
},
|
|
3120
|
+
env,
|
|
3012
3121
|
pathToClaudeCodeExecutable: executable$1,
|
|
3013
3122
|
systemPrompt: {
|
|
3014
3123
|
type: "preset",
|
|
@@ -3017,7 +3126,7 @@ function claudeOptions(accountId, cwd) {
|
|
|
3017
3126
|
hooks: { PostToolUse: [{
|
|
3018
3127
|
matcher: "PushNotification",
|
|
3019
3128
|
hooks: [async (input) => {
|
|
3020
|
-
if (input.hook_event_name !== "PostToolUse" ||
|
|
3129
|
+
if (input.hook_event_name !== "PostToolUse" || !isRecord(input.tool_input) || typeof input.tool_input.message !== "string") throw new Error("Unexpected PushNotification hook input");
|
|
3021
3130
|
broadcast({
|
|
3022
3131
|
type: "notification",
|
|
3023
3132
|
sessionId: input.session_id,
|
|
@@ -3038,12 +3147,15 @@ function claudeOptions(accountId, cwd) {
|
|
|
3038
3147
|
async function claudeCapabilities(account, cwd) {
|
|
3039
3148
|
const result = await initialization(account.id, cwd);
|
|
3040
3149
|
return {
|
|
3041
|
-
models: result.models.map((model) =>
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3150
|
+
models: result.models.filter((model) => model.value !== "default").map((model) => {
|
|
3151
|
+
if (model.resolvedModel === void 0) throw new Error(`Claude did not resolve the ${model.value} model`);
|
|
3152
|
+
return {
|
|
3153
|
+
value: model.resolvedModel,
|
|
3154
|
+
name: model.displayName,
|
|
3155
|
+
description: model.description,
|
|
3156
|
+
efforts: model.supportedEffortLevels ?? []
|
|
3157
|
+
};
|
|
3158
|
+
}),
|
|
3047
3159
|
commands: result.commands.map((command) => ({
|
|
3048
3160
|
name: command.name,
|
|
3049
3161
|
description: command.description,
|
|
@@ -3104,6 +3216,28 @@ function recordLimit(accountId, info) {
|
|
|
3104
3216
|
limits.set(accountId, current);
|
|
3105
3217
|
return accountLimits(accountId);
|
|
3106
3218
|
}
|
|
3219
|
+
async function recordUsage(accountId, probe) {
|
|
3220
|
+
try {
|
|
3221
|
+
const usage = await probe.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET({ skipBehaviors: true });
|
|
3222
|
+
if (usage.rate_limits === null) return;
|
|
3223
|
+
const current = limits.get(accountId) ?? /* @__PURE__ */ new Map();
|
|
3224
|
+
const windows = {
|
|
3225
|
+
five_hour: usage.rate_limits.five_hour,
|
|
3226
|
+
seven_day: usage.rate_limits.seven_day,
|
|
3227
|
+
seven_day_opus: usage.rate_limits.seven_day_opus,
|
|
3228
|
+
seven_day_sonnet: usage.rate_limits.seven_day_sonnet
|
|
3229
|
+
};
|
|
3230
|
+
for (const [name, window] of Object.entries(windows)) if (window !== void 0 && window !== null) current.set(name, {
|
|
3231
|
+
window: name,
|
|
3232
|
+
utilization: window.utilization === null ? null : window.utilization / 100,
|
|
3233
|
+
resetsAt: window.resets_at === null ? null : Date.parse(window.resets_at) / 1e3,
|
|
3234
|
+
rejected: window.utilization !== null && window.utilization >= 100
|
|
3235
|
+
});
|
|
3236
|
+
limits.set(accountId, current);
|
|
3237
|
+
} catch (error) {
|
|
3238
|
+
console.warn(`could not read Claude usage limits: ${error}`);
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3107
3241
|
function isLinked(configDir) {
|
|
3108
3242
|
const base = readConfig().claudeBaseDir;
|
|
3109
3243
|
try {
|
|
@@ -3133,7 +3267,9 @@ async function runProbe(accountId, cwd) {
|
|
|
3133
3267
|
options: claudeOptions(accountId, cwd)
|
|
3134
3268
|
});
|
|
3135
3269
|
try {
|
|
3136
|
-
|
|
3270
|
+
const result = await probe.initializationResult();
|
|
3271
|
+
await recordUsage(accountId, probe);
|
|
3272
|
+
return result;
|
|
3137
3273
|
} finally {
|
|
3138
3274
|
closed.resolve();
|
|
3139
3275
|
probe.close();
|
|
@@ -3192,14 +3328,15 @@ function toItems(messages) {
|
|
|
3192
3328
|
}
|
|
3193
3329
|
if (message.type === "assistant") {
|
|
3194
3330
|
message.content.forEach((block, index) => {
|
|
3195
|
-
const
|
|
3196
|
-
|
|
3331
|
+
const shared = {
|
|
3332
|
+
id: `${message.uuid}:${index}`,
|
|
3333
|
+
parent: message.parent,
|
|
3334
|
+
time: message.time
|
|
3335
|
+
};
|
|
3197
3336
|
switch (block.type) {
|
|
3198
3337
|
case "text":
|
|
3199
3338
|
if (block.text.trim() !== "") items.push({
|
|
3200
|
-
|
|
3201
|
-
parent,
|
|
3202
|
-
time: message.time,
|
|
3339
|
+
...shared,
|
|
3203
3340
|
kind: "assistant",
|
|
3204
3341
|
text: block.text,
|
|
3205
3342
|
message: message.uuid
|
|
@@ -3207,9 +3344,7 @@ function toItems(messages) {
|
|
|
3207
3344
|
break;
|
|
3208
3345
|
case "thinking":
|
|
3209
3346
|
if (block.thinking.trim() !== "") items.push({
|
|
3210
|
-
|
|
3211
|
-
parent,
|
|
3212
|
-
time: message.time,
|
|
3347
|
+
...shared,
|
|
3213
3348
|
kind: "reasoning",
|
|
3214
3349
|
text: block.thinking
|
|
3215
3350
|
});
|
|
@@ -3221,7 +3356,7 @@ function toItems(messages) {
|
|
|
3221
3356
|
}
|
|
3222
3357
|
if (block.name === "TaskStop") stops.set(block.id, text$1(block.input.task_id));
|
|
3223
3358
|
tools.set(block.id, items.length);
|
|
3224
|
-
items.push(toolItem(block, parent, message.time, (to) => {
|
|
3359
|
+
items.push(toolItem(block, message.parent, message.time, (to) => {
|
|
3225
3360
|
const owner = owners.get(to);
|
|
3226
3361
|
return owner === void 0 ? null : items[owner].id;
|
|
3227
3362
|
}));
|
|
@@ -3725,12 +3860,6 @@ async function agentMessages(sessionId, cwd, agentId, parent) {
|
|
|
3725
3860
|
//#endregion
|
|
3726
3861
|
//#region src/lib/claude/run.ts
|
|
3727
3862
|
const IDLE_LIMIT = 6e5;
|
|
3728
|
-
const imageTypes = [
|
|
3729
|
-
"image/png",
|
|
3730
|
-
"image/jpeg",
|
|
3731
|
-
"image/gif",
|
|
3732
|
-
"image/webp"
|
|
3733
|
-
];
|
|
3734
3863
|
var Run = class {
|
|
3735
3864
|
provider = "claude";
|
|
3736
3865
|
sessionId;
|
|
@@ -3860,42 +3989,10 @@ var Run = class {
|
|
|
3860
3989
|
}
|
|
3861
3990
|
push(prompt, priority) {
|
|
3862
3991
|
clearTimeout(this.idleTimer);
|
|
3863
|
-
for (const image of prompt.images) if (!imageTypes.includes(image.mediaType)) throw new Error(`${image.mediaType} images are not supported`);
|
|
3864
3992
|
const uuid = randomUUID();
|
|
3865
|
-
this.inbox.push(
|
|
3866
|
-
type: "user",
|
|
3867
|
-
message: {
|
|
3868
|
-
role: "user",
|
|
3869
|
-
content: [...prompt.images.map((image) => ({
|
|
3870
|
-
type: "image",
|
|
3871
|
-
source: {
|
|
3872
|
-
type: "base64",
|
|
3873
|
-
media_type: image.mediaType,
|
|
3874
|
-
data: image.data
|
|
3875
|
-
}
|
|
3876
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3877
|
-
type: "text",
|
|
3878
|
-
text: prompt.text
|
|
3879
|
-
}]]
|
|
3880
|
-
},
|
|
3881
|
-
parent_tool_use_id: null,
|
|
3882
|
-
uuid,
|
|
3883
|
-
...priority === void 0 ? {} : { priority }
|
|
3884
|
-
});
|
|
3993
|
+
this.inbox.push(toUserInput(prompt, uuid, priority));
|
|
3885
3994
|
this.wake?.();
|
|
3886
|
-
this.add(
|
|
3887
|
-
type: "user",
|
|
3888
|
-
uuid,
|
|
3889
|
-
time: Date.now(),
|
|
3890
|
-
content: [...prompt.images.map((image) => ({
|
|
3891
|
-
type: "image",
|
|
3892
|
-
...image
|
|
3893
|
-
})), ...prompt.text === "" ? [] : [{
|
|
3894
|
-
type: "text",
|
|
3895
|
-
text: prompt.text
|
|
3896
|
-
}]],
|
|
3897
|
-
parent: null
|
|
3898
|
-
});
|
|
3995
|
+
this.add(promptMessage(prompt, uuid));
|
|
3899
3996
|
this.running = true;
|
|
3900
3997
|
this.prompted = true;
|
|
3901
3998
|
broadcastActivity();
|
|
@@ -4089,6 +4186,7 @@ var Run = class {
|
|
|
4089
4186
|
request
|
|
4090
4187
|
});
|
|
4091
4188
|
broadcastActivity();
|
|
4189
|
+
scheduleAutoResolve(this.sessionId, request);
|
|
4092
4190
|
options.signal.addEventListener("abort", () => this.settle(request.id, {
|
|
4093
4191
|
behavior: "deny",
|
|
4094
4192
|
message: "Cancelled"
|
|
@@ -4139,21 +4237,13 @@ var Run = class {
|
|
|
4139
4237
|
});
|
|
4140
4238
|
}
|
|
4141
4239
|
};
|
|
4142
|
-
function notice(text) {
|
|
4143
|
-
return {
|
|
4144
|
-
type: "notice",
|
|
4145
|
-
uuid: randomUUID(),
|
|
4146
|
-
time: Date.now(),
|
|
4147
|
-
text,
|
|
4148
|
-
isError: true
|
|
4149
|
-
};
|
|
4150
|
-
}
|
|
4151
4240
|
//#endregion
|
|
4152
4241
|
//#region src/services/git.ts
|
|
4153
4242
|
const DIFF_LIMIT = 8e4;
|
|
4154
4243
|
const UNTRACKED_LIMIT = 100;
|
|
4155
4244
|
const NEW_FILE_LIMIT = 262144;
|
|
4156
4245
|
const REPO_DEPTH = 3;
|
|
4246
|
+
const run$1 = promisify(execFile);
|
|
4157
4247
|
async function gitStatus(cwd) {
|
|
4158
4248
|
let output;
|
|
4159
4249
|
try {
|
|
@@ -4207,20 +4297,24 @@ async function push$1(cwd) {
|
|
|
4207
4297
|
return gitStatus(cwd);
|
|
4208
4298
|
}
|
|
4209
4299
|
async function createPullRequest(cwd) {
|
|
4210
|
-
const
|
|
4300
|
+
const url = (await run$1("gh", [
|
|
4211
4301
|
"pr",
|
|
4212
4302
|
"create",
|
|
4213
4303
|
"--fill"
|
|
4214
|
-
], { cwd });
|
|
4215
|
-
const url = stdout.trim().split("\n").at(-1);
|
|
4304
|
+
], { cwd })).stdout.trim().split("\n").at(-1);
|
|
4216
4305
|
if (url === void 0) throw new Error("gh did not return a pull request URL");
|
|
4217
4306
|
return { url };
|
|
4218
4307
|
}
|
|
4219
|
-
async function suggestCommitMessage(
|
|
4220
|
-
const
|
|
4308
|
+
async function suggestCommitMessage(cwd) {
|
|
4309
|
+
const settings = readConfig().commitMessage;
|
|
4310
|
+
const provider = settings.provider;
|
|
4311
|
+
const accountId = settings.accountId;
|
|
4312
|
+
const model = settings.model;
|
|
4313
|
+
if (provider === null || model === null) throw new Error("Choose a commit message model in Settings → Git");
|
|
4314
|
+
assertEnabled(provider, accountId);
|
|
4315
|
+
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4316
|
+
const diff = (await repoDiff(cwd, "")).slice(0, DIFF_LIMIT);
|
|
4221
4317
|
if (diff === "") throw new Error("There is nothing to commit");
|
|
4222
|
-
const model = readConfig().commitModels[provider];
|
|
4223
|
-
if (model === null) throw new Error(`Choose a ${providerName(provider)} commit model in Settings → Accounts`);
|
|
4224
4318
|
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}`;
|
|
4225
4319
|
if (provider === "codex") return codexCommitMessage(accountId, cwd, model, prompt);
|
|
4226
4320
|
if (provider === "opencode") return openCodeCommitMessage(cwd, model, prompt);
|
|
@@ -4243,10 +4337,6 @@ async function suggestCommitMessage(provider, accountId, cwd) {
|
|
|
4243
4337
|
}
|
|
4244
4338
|
throw new Error("Claude did not reply");
|
|
4245
4339
|
}
|
|
4246
|
-
async function workingDiff(cwd) {
|
|
4247
|
-
if (!await isRepo(cwd)) throw new Error(`${cwd} is not a git repository`);
|
|
4248
|
-
return repoDiff(cwd, "");
|
|
4249
|
-
}
|
|
4250
4340
|
async function projectDiff(cwd) {
|
|
4251
4341
|
if (await isRepo(cwd)) return repoDiff(cwd, "");
|
|
4252
4342
|
const repos = childRepos(cwd, "", REPO_DEPTH);
|
|
@@ -4254,7 +4344,7 @@ async function projectDiff(cwd) {
|
|
|
4254
4344
|
return (await Promise.all(repos.map((repo) => repoDiff(path.join(cwd, repo), `${repo}/`)))).join("");
|
|
4255
4345
|
}
|
|
4256
4346
|
function git(cwd, args) {
|
|
4257
|
-
return
|
|
4347
|
+
return run$1("git", args, {
|
|
4258
4348
|
cwd,
|
|
4259
4349
|
maxBuffer: 67108864
|
|
4260
4350
|
});
|
|
@@ -4403,7 +4493,7 @@ async function taskOutput(sessionId, file) {
|
|
|
4403
4493
|
try {
|
|
4404
4494
|
stats = await lstat(file);
|
|
4405
4495
|
} catch (error) {
|
|
4406
|
-
if (error
|
|
4496
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
4407
4497
|
throw error;
|
|
4408
4498
|
}
|
|
4409
4499
|
if (!stats.isFile()) throw new Error("Background task output is not a regular file");
|
|
@@ -4447,42 +4537,57 @@ const TIME_TO_LIVE = 900;
|
|
|
4447
4537
|
const SEND_TIMEOUT = 1e4;
|
|
4448
4538
|
const SUBJECT = "https://github.com/NazmusSayad/offsite";
|
|
4449
4539
|
let server$1 = "";
|
|
4540
|
+
let notifier = null;
|
|
4450
4541
|
function startPush(address) {
|
|
4451
4542
|
server$1 = address;
|
|
4452
4543
|
}
|
|
4453
4544
|
function pushActive() {
|
|
4454
|
-
return
|
|
4545
|
+
return targets().length > 0;
|
|
4546
|
+
}
|
|
4547
|
+
function notifierClient() {
|
|
4548
|
+
return notifier === null ? null : notifier.client;
|
|
4455
4549
|
}
|
|
4456
|
-
function
|
|
4457
|
-
|
|
4550
|
+
function claimNotifier(client, endpoint) {
|
|
4551
|
+
notifier = {
|
|
4552
|
+
client,
|
|
4553
|
+
endpoint
|
|
4554
|
+
};
|
|
4555
|
+
for (const item of clients) item.send({
|
|
4556
|
+
type: "notifier",
|
|
4557
|
+
active: item === client
|
|
4558
|
+
});
|
|
4458
4559
|
}
|
|
4459
|
-
function subscribe(
|
|
4460
|
-
if (new URL(subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4560
|
+
function subscribe(registration, client) {
|
|
4561
|
+
if (new URL(registration.subscription.endpoint).protocol !== "https:") throw new Error("the endpoint must be an https URL");
|
|
4461
4562
|
const values = {
|
|
4462
|
-
endpoint: subscription.endpoint,
|
|
4463
|
-
p256dh: subscription.keys.p256dh,
|
|
4464
|
-
auth: subscription.keys.auth,
|
|
4563
|
+
endpoint: registration.subscription.endpoint,
|
|
4564
|
+
p256dh: registration.subscription.keys.p256dh,
|
|
4565
|
+
auth: registration.subscription.keys.auth,
|
|
4566
|
+
publicKey: registration.publicKey,
|
|
4567
|
+
privateKey: registration.privateKey,
|
|
4568
|
+
machine: registration.machine,
|
|
4465
4569
|
server: server$1
|
|
4466
4570
|
};
|
|
4467
4571
|
db.insert(pushSubscriptions).values(values).onConflictDoUpdate({
|
|
4468
4572
|
target: pushSubscriptions.endpoint,
|
|
4469
4573
|
set: values
|
|
4470
4574
|
}).run();
|
|
4575
|
+
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4471
4576
|
console.log("a browser subscribed to push notifications");
|
|
4472
4577
|
}
|
|
4473
4578
|
function unsubscribe(endpoint) {
|
|
4474
4579
|
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4580
|
+
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4475
4581
|
console.log("a browser unsubscribed from push notifications");
|
|
4476
4582
|
}
|
|
4477
4583
|
async function sendPush(message) {
|
|
4478
|
-
const subscriptions =
|
|
4584
|
+
const subscriptions = targets();
|
|
4479
4585
|
if (subscriptions.length === 0) return;
|
|
4480
|
-
const
|
|
4481
|
-
const payload = JSON.stringify({
|
|
4586
|
+
const trimmed = {
|
|
4482
4587
|
...message,
|
|
4483
4588
|
title: [...message.title].slice(0, TITLE_LIMIT).join(""),
|
|
4484
4589
|
body: [...message.body].slice(0, BODY_LIMIT).join("")
|
|
4485
|
-
}
|
|
4590
|
+
};
|
|
4486
4591
|
const gone = [];
|
|
4487
4592
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4488
4593
|
try {
|
|
@@ -4492,11 +4597,14 @@ async function sendPush(message) {
|
|
|
4492
4597
|
p256dh: subscription.p256dh,
|
|
4493
4598
|
auth: subscription.auth
|
|
4494
4599
|
}
|
|
4495
|
-
},
|
|
4600
|
+
}, JSON.stringify({
|
|
4601
|
+
...trimmed,
|
|
4602
|
+
machineId: subscription.machine
|
|
4603
|
+
}), {
|
|
4496
4604
|
vapidDetails: {
|
|
4497
4605
|
subject: SUBJECT,
|
|
4498
|
-
publicKey:
|
|
4499
|
-
privateKey:
|
|
4606
|
+
publicKey: subscription.publicKey,
|
|
4607
|
+
privateKey: subscription.privateKey
|
|
4500
4608
|
},
|
|
4501
4609
|
TTL: TIME_TO_LIVE,
|
|
4502
4610
|
urgency: "high",
|
|
@@ -4520,8 +4628,9 @@ async function sendPush(message) {
|
|
|
4520
4628
|
if (gone.length === 0) return;
|
|
4521
4629
|
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4522
4630
|
}
|
|
4523
|
-
function
|
|
4524
|
-
|
|
4631
|
+
function targets() {
|
|
4632
|
+
if (notifier === null || notifier.endpoint === null) return [];
|
|
4633
|
+
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4525
4634
|
}
|
|
4526
4635
|
//#endregion
|
|
4527
4636
|
//#region src/services/runs.ts
|
|
@@ -4595,6 +4704,7 @@ async function open$1(client, sessionId, projectId) {
|
|
|
4595
4704
|
client.send(event);
|
|
4596
4705
|
}
|
|
4597
4706
|
async function send$1(client, request) {
|
|
4707
|
+
assertEnabled(request.settings.provider, request.settings.accountId);
|
|
4598
4708
|
const cwd = projectPath(request.projectId);
|
|
4599
4709
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`${cwd} is not a folder`);
|
|
4600
4710
|
if (request.sessionId === null) {
|
|
@@ -4618,11 +4728,7 @@ async function sendExisting(client, sessionId, cwd, request) {
|
|
|
4618
4728
|
const provider = providerOf(sessionId);
|
|
4619
4729
|
if (request.settings.provider !== provider) throw new Error(`This session belongs to ${providerName(provider)}`);
|
|
4620
4730
|
const existing = runs.get(sessionId);
|
|
4621
|
-
if (existing?.provider === "claude" && !existing.running && existing.backgroundTasks === 0 && await changedElsewhere(existing, cwd)) {
|
|
4622
|
-
existing.stop();
|
|
4623
|
-
await existing.closed;
|
|
4624
|
-
}
|
|
4625
|
-
if (existing?.provider === "codex" && !existing.running && existing.pending.size === 0 && (listedUpdatedAt("codex", sessionId) ?? 0) > existing.activeAt) {
|
|
4731
|
+
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) {
|
|
4626
4732
|
existing.stop();
|
|
4627
4733
|
await existing.closed;
|
|
4628
4734
|
}
|
|
@@ -4772,7 +4878,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
4772
4878
|
}
|
|
4773
4879
|
//#endregion
|
|
4774
4880
|
//#region src/services/terminal/shell-integration.ts
|
|
4775
|
-
const scripts = path.join(serverDir, "src
|
|
4881
|
+
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
4776
4882
|
const folder = path.join(home, "shell-integration");
|
|
4777
4883
|
function prepare(env, command, shell) {
|
|
4778
4884
|
if (shellIntegrationIssue(command, platform(), shell) !== null) return null;
|
|
@@ -4989,7 +5095,7 @@ function launch() {
|
|
|
4989
5095
|
offsite: [
|
|
4990
5096
|
"offsite",
|
|
4991
5097
|
"offsite",
|
|
4992
|
-
|
|
5098
|
+
"0.0.0"
|
|
4993
5099
|
],
|
|
4994
5100
|
ghostty: [
|
|
4995
5101
|
"ghostty",
|
|
@@ -5457,7 +5563,7 @@ function scan(id, terminal, data) {
|
|
|
5457
5563
|
return;
|
|
5458
5564
|
}
|
|
5459
5565
|
const alert = terminal.scanner.scan(data);
|
|
5460
|
-
if (alert === null ||
|
|
5566
|
+
if (alert === null || notifierWatching()) return;
|
|
5461
5567
|
if (!(alert.type === "bell" ? terminal.alerts.startBell() : terminal.alerts.startNotification())) return;
|
|
5462
5568
|
const windowTitle = terminal.scanner.title;
|
|
5463
5569
|
setTimeout(() => push(id, alert, windowTitle), 500);
|
|
@@ -5474,7 +5580,7 @@ function push(id, alert, windowTitle) {
|
|
|
5474
5580
|
title = alert.title;
|
|
5475
5581
|
body = alert.body;
|
|
5476
5582
|
}
|
|
5477
|
-
if (
|
|
5583
|
+
if (notifierWatching()) return;
|
|
5478
5584
|
const entry = [...projects].find(([, project]) => project.tabs.some((tab) => tab.id === id));
|
|
5479
5585
|
if (entry === void 0) return;
|
|
5480
5586
|
const tab = findTab(id);
|
|
@@ -5482,19 +5588,17 @@ function push(id, alert, windowTitle) {
|
|
|
5482
5588
|
sendPush({
|
|
5483
5589
|
title,
|
|
5484
5590
|
body,
|
|
5485
|
-
machineId: machineInfo().id,
|
|
5486
5591
|
projectId: entry[0],
|
|
5487
5592
|
tabId: id
|
|
5488
5593
|
});
|
|
5489
5594
|
}
|
|
5490
|
-
function
|
|
5491
|
-
|
|
5595
|
+
function notifierWatching() {
|
|
5596
|
+
const client = notifierClient();
|
|
5597
|
+
if (client === null || !connections.has(client)) return false;
|
|
5598
|
+
return Date.now() - client.heardAt < SILENCE_LIMIT;
|
|
5492
5599
|
}
|
|
5493
5600
|
function findTab(id) {
|
|
5494
|
-
|
|
5495
|
-
const tab = project.tabs.find((item) => item.id === id);
|
|
5496
|
-
if (tab !== void 0) return tab;
|
|
5497
|
-
}
|
|
5601
|
+
return [...projects.values()].flatMap((project) => project.tabs).find((tab) => tab.id === id);
|
|
5498
5602
|
}
|
|
5499
5603
|
function nearestVisible(tabs, index) {
|
|
5500
5604
|
for (let position = index; position < tabs.length; position++) if (!tabs[position].minimized) return position;
|
|
@@ -5527,8 +5631,6 @@ const handlers = {
|
|
|
5527
5631
|
refreshAll();
|
|
5528
5632
|
return {
|
|
5529
5633
|
...configInfo(),
|
|
5530
|
-
protocol: 3,
|
|
5531
|
-
machineId: machineInfo().id,
|
|
5532
5634
|
hostname: os.hostname(),
|
|
5533
5635
|
projects: projects$1(),
|
|
5534
5636
|
sessions: sessions(),
|
|
@@ -5537,10 +5639,15 @@ const handlers = {
|
|
|
5537
5639
|
};
|
|
5538
5640
|
},
|
|
5539
5641
|
"config.get": () => configInfo(),
|
|
5540
|
-
"config.save": (params) => {
|
|
5541
|
-
const before = readConfig()
|
|
5642
|
+
"config.save": async (params) => {
|
|
5643
|
+
const before = readConfig();
|
|
5542
5644
|
const saved = saveConfig(params);
|
|
5543
|
-
|
|
5645
|
+
for (const provider of [
|
|
5646
|
+
"claude",
|
|
5647
|
+
"codex",
|
|
5648
|
+
"opencode"
|
|
5649
|
+
]) if (before.enabledProviders[provider] && !saved.enabledProviders[provider]) await stopProvider(provider);
|
|
5650
|
+
if (saved.claudeBaseDir === before.claudeBaseDir && JSON.stringify(saved.enabledProviders) === JSON.stringify(before.enabledProviders)) return saved;
|
|
5544
5651
|
watchSessions();
|
|
5545
5652
|
refreshAll();
|
|
5546
5653
|
publishAccounts();
|
|
@@ -5630,8 +5737,8 @@ const handlers = {
|
|
|
5630
5737
|
publishAccounts();
|
|
5631
5738
|
return null;
|
|
5632
5739
|
},
|
|
5633
|
-
"accounts.update": (params) => {
|
|
5634
|
-
updateAccount(params
|
|
5740
|
+
"accounts.update": async (params) => {
|
|
5741
|
+
await updateAccount(params);
|
|
5635
5742
|
publishAccounts();
|
|
5636
5743
|
return null;
|
|
5637
5744
|
},
|
|
@@ -5640,7 +5747,7 @@ const handlers = {
|
|
|
5640
5747
|
publishAccounts();
|
|
5641
5748
|
return null;
|
|
5642
5749
|
},
|
|
5643
|
-
capabilities: (params) => capabilities(params.accountId, projectPath(params.projectId)),
|
|
5750
|
+
capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
|
|
5644
5751
|
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
5645
5752
|
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
5646
5753
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
@@ -5650,16 +5757,19 @@ const handlers = {
|
|
|
5650
5757
|
"git.commit": (params) => commit(projectPath(params.projectId), params.message),
|
|
5651
5758
|
"git.push": (params) => push$1(projectPath(params.projectId)),
|
|
5652
5759
|
"git.pullRequest": (params) => createPullRequest(projectPath(params.projectId)),
|
|
5653
|
-
"git.message": (params) => suggestCommitMessage(
|
|
5654
|
-
"push.
|
|
5655
|
-
|
|
5656
|
-
subscribe(params);
|
|
5760
|
+
"git.message": (params) => suggestCommitMessage(projectPath(params.projectId)),
|
|
5761
|
+
"push.subscribe": (params, client) => {
|
|
5762
|
+
subscribe(params, client);
|
|
5657
5763
|
return null;
|
|
5658
5764
|
},
|
|
5659
5765
|
"push.unsubscribe": (params) => {
|
|
5660
5766
|
unsubscribe(params.endpoint);
|
|
5661
5767
|
return null;
|
|
5662
5768
|
},
|
|
5769
|
+
"notifications.claim": (params, client) => {
|
|
5770
|
+
claimNotifier(client, params.endpoint);
|
|
5771
|
+
return null;
|
|
5772
|
+
},
|
|
5663
5773
|
terminal: async (params, client) => {
|
|
5664
5774
|
await terminalRequest(client, params);
|
|
5665
5775
|
return null;
|
|
@@ -5717,38 +5827,58 @@ async function answer(client, text) {
|
|
|
5717
5827
|
}
|
|
5718
5828
|
}
|
|
5719
5829
|
//#endregion
|
|
5720
|
-
//#region src/storage/
|
|
5721
|
-
|
|
5722
|
-
|
|
5830
|
+
//#region src/storage/auth.ts
|
|
5831
|
+
const CODE_LIFETIME = 3e5;
|
|
5832
|
+
const DEVICE_LIFETIME = 2592e6;
|
|
5833
|
+
function addCode(name) {
|
|
5834
|
+
const code = randomBytes(16).toString("base64url");
|
|
5835
|
+
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
5836
|
+
db.insert(pairingCodes).values({
|
|
5837
|
+
hash: hash(code),
|
|
5838
|
+
name,
|
|
5839
|
+
expiresAt: Date.now() + CODE_LIFETIME
|
|
5840
|
+
}).run();
|
|
5841
|
+
return code;
|
|
5723
5842
|
}
|
|
5724
|
-
function
|
|
5843
|
+
function redeemCode(code) {
|
|
5844
|
+
const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
|
|
5845
|
+
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
5725
5846
|
const token = randomBytes(32).toString("base64url");
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5847
|
+
const now = Date.now();
|
|
5848
|
+
db.insert(devices).values({
|
|
5849
|
+
hash: hash(token),
|
|
5850
|
+
name: found.name,
|
|
5851
|
+
createdAt: now,
|
|
5852
|
+
expiresAt: now + DEVICE_LIFETIME
|
|
5730
5853
|
}).run();
|
|
5731
5854
|
return token;
|
|
5732
5855
|
}
|
|
5733
|
-
function
|
|
5734
|
-
db.
|
|
5856
|
+
function findDevice(token) {
|
|
5857
|
+
const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
|
|
5858
|
+
return found === void 0 ? null : found.hash;
|
|
5735
5859
|
}
|
|
5736
|
-
function
|
|
5737
|
-
return db.select().from(
|
|
5860
|
+
function isPaired(device) {
|
|
5861
|
+
return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
|
|
5738
5862
|
}
|
|
5739
|
-
function
|
|
5740
|
-
|
|
5741
|
-
return db.select().from(tokens).where(eq(tokens.hash, hashToken(given))).get() !== void 0;
|
|
5863
|
+
function hasDevices() {
|
|
5864
|
+
return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
|
|
5742
5865
|
}
|
|
5743
|
-
function
|
|
5744
|
-
db.
|
|
5866
|
+
function readDevices() {
|
|
5867
|
+
return db.select().from(devices).orderBy(devices.createdAt).all();
|
|
5745
5868
|
}
|
|
5746
|
-
function
|
|
5747
|
-
|
|
5869
|
+
function removeDevices(hashes) {
|
|
5870
|
+
db.delete(devices).where(inArray(devices.hash, hashes)).run();
|
|
5871
|
+
}
|
|
5872
|
+
function recordUse(device) {
|
|
5873
|
+
db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
|
|
5874
|
+
}
|
|
5875
|
+
function hash(secret) {
|
|
5876
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
5748
5877
|
}
|
|
5749
5878
|
//#endregion
|
|
5750
5879
|
//#region src/main.ts
|
|
5751
5880
|
const UNPAIR_CHECK = 5e3;
|
|
5881
|
+
const TICKET_LIFETIME = 6e4;
|
|
5752
5882
|
const contentTypes = {
|
|
5753
5883
|
".html": "text/html; charset=utf-8",
|
|
5754
5884
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -5766,17 +5896,10 @@ if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
|
5766
5896
|
process.exit(1);
|
|
5767
5897
|
}
|
|
5768
5898
|
const address = `http://${args.host}:${port}`;
|
|
5769
|
-
const fallback = args.fallback === void 0 ? null : readFallback(args.fallback);
|
|
5770
5899
|
if (args.command === "pair") {
|
|
5771
|
-
const links = pairingLinks(
|
|
5772
|
-
console.log(
|
|
5773
|
-
console.log("");
|
|
5774
|
-
console.log(renderUnicodeCompact(links.hosted, { border: 2 }));
|
|
5775
|
-
console.log("");
|
|
5776
|
-
console.log("Or open one of these links in the browser to pair:");
|
|
5777
|
-
console.log(` ${links.hosted}`);
|
|
5900
|
+
const links = pairingLinks(addCode(args.name));
|
|
5901
|
+
console.log("Open this link in a browser within 5 minutes to pair it:");
|
|
5778
5902
|
console.log(` ${links.local}`);
|
|
5779
|
-
if (fallback !== null) console.log(`Fallback: ${fallback}`);
|
|
5780
5903
|
process.exit(0);
|
|
5781
5904
|
}
|
|
5782
5905
|
if (args.command === "unpair") {
|
|
@@ -5792,23 +5915,17 @@ if (findClaude() === null) {
|
|
|
5792
5915
|
process.exit(1);
|
|
5793
5916
|
}
|
|
5794
5917
|
const publicDir = path.join(import.meta.dirname, "..", "..", "web", "dist");
|
|
5795
|
-
const
|
|
5918
|
+
const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
|
|
5919
|
+
const tickets = /* @__PURE__ */ new Map();
|
|
5796
5920
|
const server = createServer((request, response) => {
|
|
5797
5921
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5798
|
-
if (url.pathname
|
|
5799
|
-
|
|
5800
|
-
response.setHeader("content-type", "application/json");
|
|
5801
|
-
response.end(JSON.stringify({
|
|
5802
|
-
version,
|
|
5803
|
-
protocol: 3,
|
|
5804
|
-
machineId: machineInfo().id,
|
|
5805
|
-
authorized: authorized(url)
|
|
5806
|
-
}));
|
|
5922
|
+
if (url.pathname.startsWith("/api/")) {
|
|
5923
|
+
answerApi(url.pathname, request, response);
|
|
5807
5924
|
return;
|
|
5808
5925
|
}
|
|
5809
5926
|
if (args.command === "serve") {
|
|
5810
5927
|
response.statusCode = 404;
|
|
5811
|
-
response.end(
|
|
5928
|
+
response.end("offsite serve has no web app");
|
|
5812
5929
|
return;
|
|
5813
5930
|
}
|
|
5814
5931
|
serveFile(url.pathname, response);
|
|
@@ -5817,34 +5934,31 @@ const sockets = new WebSocketServer({ noServer: true });
|
|
|
5817
5934
|
const paired = /* @__PURE__ */ new Map();
|
|
5818
5935
|
server.on("upgrade", (request, socket, head) => {
|
|
5819
5936
|
const url = new URL(request.url ?? "/", "http://localhost");
|
|
5820
|
-
const
|
|
5821
|
-
if (url.pathname !== "/ws" ||
|
|
5937
|
+
const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
|
|
5938
|
+
if (url.pathname !== "/ws" || args.auth && device === null) {
|
|
5822
5939
|
socket.destroy();
|
|
5823
5940
|
return;
|
|
5824
5941
|
}
|
|
5825
5942
|
sockets.handleUpgrade(request, socket, head, (client) => {
|
|
5826
5943
|
connectClient(client);
|
|
5827
|
-
if (
|
|
5828
|
-
paired.set(client,
|
|
5944
|
+
if (device === null) return;
|
|
5945
|
+
paired.set(client, device);
|
|
5829
5946
|
client.on("close", () => paired.delete(client));
|
|
5830
|
-
recordUse(
|
|
5947
|
+
recordUse(device);
|
|
5831
5948
|
});
|
|
5832
5949
|
});
|
|
5833
5950
|
if (args.auth) setInterval(() => {
|
|
5834
|
-
for (const [client,
|
|
5951
|
+
for (const [client, device] of paired) if (!isPaired(device)) client.close();
|
|
5952
|
+
for (const [ticket, item] of tickets) if (item.expiresAt <= Date.now()) tickets.delete(ticket);
|
|
5835
5953
|
}, UNPAIR_CHECK);
|
|
5836
5954
|
server.listen(port, args.host, () => {
|
|
5837
5955
|
startPush(`${args.host}:${port}`);
|
|
5838
|
-
if (args.pair && (
|
|
5839
|
-
const links = pairingLinks(
|
|
5956
|
+
if (args.pair && (code !== null || !args.auth)) {
|
|
5957
|
+
const links = pairingLinks(code);
|
|
5840
5958
|
if (args.command === "web") console.log(`offsite is running at ${links.local}`);
|
|
5841
|
-
console.log(`Open it on ${HOSTED_APP}: ${links.hosted}`);
|
|
5842
|
-
} else if (args.pair) {
|
|
5843
|
-
console.log(`offsite is running at ${address}`);
|
|
5844
|
-
console.log("Run offsite pair to pair another browser");
|
|
5845
5959
|
} else {
|
|
5846
5960
|
console.log(`offsite is running at ${address}`);
|
|
5847
|
-
console.log(
|
|
5961
|
+
if (args.pair) console.log("Run offsite pair to pair another browser");
|
|
5848
5962
|
}
|
|
5849
5963
|
if (!args.auth) console.warn("auth is off; anyone who can reach this port can control this machine");
|
|
5850
5964
|
});
|
|
@@ -5854,12 +5968,67 @@ function stop() {
|
|
|
5854
5968
|
stopAll();
|
|
5855
5969
|
process.exit(0);
|
|
5856
5970
|
}
|
|
5857
|
-
function
|
|
5858
|
-
|
|
5971
|
+
function answerApi(pathname, request, response) {
|
|
5972
|
+
response.setHeader("access-control-allow-origin", "*");
|
|
5973
|
+
if (request.method === "OPTIONS") {
|
|
5974
|
+
response.setHeader("access-control-allow-methods", "POST");
|
|
5975
|
+
response.setHeader("access-control-allow-headers", "authorization");
|
|
5976
|
+
response.statusCode = 204;
|
|
5977
|
+
response.end();
|
|
5978
|
+
return;
|
|
5979
|
+
}
|
|
5980
|
+
if (request.method !== "POST") {
|
|
5981
|
+
response.statusCode = 405;
|
|
5982
|
+
response.end("use POST");
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
const given = request.headers.authorization?.match(/^Bearer (.+)$/)?.[1] ?? null;
|
|
5986
|
+
if (pathname === "/api/pair") {
|
|
5987
|
+
const token = given === null ? null : redeemCode(given);
|
|
5988
|
+
if (token === null) {
|
|
5989
|
+
response.statusCode = 401;
|
|
5990
|
+
response.end("That pairing code expired or was already used");
|
|
5991
|
+
return;
|
|
5992
|
+
}
|
|
5993
|
+
answerJson(response, { token });
|
|
5994
|
+
return;
|
|
5995
|
+
}
|
|
5996
|
+
if (pathname === "/api/ticket") {
|
|
5997
|
+
if (!args.auth) {
|
|
5998
|
+
answerJson(response, { ticket: null });
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
const device = given === null ? null : findDevice(given);
|
|
6002
|
+
if (device === null) {
|
|
6003
|
+
response.statusCode = 401;
|
|
6004
|
+
response.end("This browser needs pairing");
|
|
6005
|
+
return;
|
|
6006
|
+
}
|
|
6007
|
+
const ticket = randomBytes(32).toString("base64url");
|
|
6008
|
+
tickets.set(ticket, {
|
|
6009
|
+
device,
|
|
6010
|
+
expiresAt: Date.now() + TICKET_LIFETIME
|
|
6011
|
+
});
|
|
6012
|
+
answerJson(response, { ticket });
|
|
6013
|
+
return;
|
|
6014
|
+
}
|
|
6015
|
+
response.statusCode = 404;
|
|
6016
|
+
response.end("not found");
|
|
6017
|
+
}
|
|
6018
|
+
function answerJson(response, body) {
|
|
6019
|
+
response.setHeader("content-type", "application/json");
|
|
6020
|
+
response.end(JSON.stringify(body));
|
|
6021
|
+
}
|
|
6022
|
+
function redeemTicket(ticket) {
|
|
6023
|
+
if (ticket === null) return null;
|
|
6024
|
+
const found = tickets.get(ticket);
|
|
6025
|
+
tickets.delete(ticket);
|
|
6026
|
+
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
6027
|
+
return found.device;
|
|
5859
6028
|
}
|
|
5860
6029
|
async function unpair() {
|
|
5861
|
-
const
|
|
5862
|
-
if (
|
|
6030
|
+
const devices = readDevices();
|
|
6031
|
+
if (devices.length === 0) {
|
|
5863
6032
|
console.log("No browsers are paired");
|
|
5864
6033
|
return;
|
|
5865
6034
|
}
|
|
@@ -5871,11 +6040,12 @@ async function unpair() {
|
|
|
5871
6040
|
type: "multiselect",
|
|
5872
6041
|
required: false,
|
|
5873
6042
|
cancel: "null",
|
|
5874
|
-
options:
|
|
6043
|
+
options: devices.map((item) => ({
|
|
5875
6044
|
value: item.hash,
|
|
5876
6045
|
label: [
|
|
5877
6046
|
item.name ?? "Unnamed",
|
|
5878
6047
|
`paired ${new Date(item.createdAt).toLocaleString()}`,
|
|
6048
|
+
`${item.expiresAt <= Date.now() ? "expired" : "expires"} ${new Date(item.expiresAt).toLocaleString()}`,
|
|
5879
6049
|
item.lastUsedAt === null ? "never used" : `last used ${new Date(item.lastUsedAt).toLocaleString()}`
|
|
5880
6050
|
].join(" · ")
|
|
5881
6051
|
}))
|
|
@@ -5884,35 +6054,21 @@ async function unpair() {
|
|
|
5884
6054
|
console.log("Nothing unpaired");
|
|
5885
6055
|
return;
|
|
5886
6056
|
}
|
|
5887
|
-
|
|
6057
|
+
removeDevices(picked);
|
|
5888
6058
|
console.log(`Unpaired ${picked.length} ${picked.length === 1 ? "browser" : "browsers"}; a running offsite disconnects them within seconds`);
|
|
5889
6059
|
}
|
|
5890
|
-
function
|
|
5891
|
-
|
|
5892
|
-
try {
|
|
5893
|
-
url = new URL(link);
|
|
5894
|
-
} catch {
|
|
5895
|
-
console.error(`invalid fallback URL ${link}`);
|
|
5896
|
-
process.exit(1);
|
|
5897
|
-
}
|
|
5898
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
5899
|
-
console.error(`the fallback URL ${link} needs http:// or https://`);
|
|
5900
|
-
process.exit(1);
|
|
5901
|
-
}
|
|
5902
|
-
return url.origin;
|
|
5903
|
-
}
|
|
5904
|
-
function pairingLinks(token) {
|
|
5905
|
-
const hosted = new URLSearchParams({
|
|
6060
|
+
function pairingLinks(code) {
|
|
6061
|
+
return { local: `${address}/pair#${new URLSearchParams({
|
|
5906
6062
|
machine: address,
|
|
5907
|
-
...
|
|
5908
|
-
|
|
5909
|
-
});
|
|
5910
|
-
return {
|
|
5911
|
-
local: token === null ? address : `${address}/pair#${new URLSearchParams({ token })}`,
|
|
5912
|
-
hosted: `${HOSTED_APP}/pair#${hosted}`
|
|
5913
|
-
};
|
|
6063
|
+
...code !== null && { code }
|
|
6064
|
+
})}` };
|
|
5914
6065
|
}
|
|
5915
6066
|
function serveFile(pathname, response) {
|
|
6067
|
+
if (!args.pwa && pathname === "/manifest.webmanifest") {
|
|
6068
|
+
response.statusCode = 404;
|
|
6069
|
+
response.end("offsite runs without --pwa");
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
5916
6072
|
const requested = path.join(publicDir, path.normalize(pathname));
|
|
5917
6073
|
const file = requested.startsWith(publicDir + path.sep) && statSync(requested, { throwIfNoEntry: false })?.isFile() ? requested : path.join(publicDir, "index.html");
|
|
5918
6074
|
if (!existsSync(file)) {
|
|
@@ -5921,6 +6077,10 @@ function serveFile(pathname, response) {
|
|
|
5921
6077
|
return;
|
|
5922
6078
|
}
|
|
5923
6079
|
response.setHeader("content-type", contentTypes[path.extname(file)] ?? "application/octet-stream");
|
|
6080
|
+
if (!args.pwa && path.basename(file) === "index.html") {
|
|
6081
|
+
response.end(readFileSync(file, "utf8").replace(/<link rel="manifest"[^>]*>/, ""));
|
|
6082
|
+
return;
|
|
6083
|
+
}
|
|
5924
6084
|
createReadStream(file).pipe(response);
|
|
5925
6085
|
}
|
|
5926
6086
|
//#endregion
|