offsite 0.0.85 → 0.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/client/dist/assets/XIcon-gbQKQqpC.js +67 -0
- package/apps/client/dist/assets/{diff-panel-DHx8Y0XH.js → diff-panel-CSL-MBoH.js} +1 -1
- package/apps/client/dist/assets/{editor-dialog-mkPUdzR1.js → editor-dialog-BZY0-avL.js} +34 -34
- package/apps/client/dist/assets/{image-viewer-CplYkb8K.js → image-viewer-CVERQTJS.js} +1 -1
- package/apps/client/dist/assets/index-BjqaTniD.js +72 -0
- package/apps/client/dist/assets/{index-CSQLBowu.css → index-D8k0XOju.css} +1 -1
- package/apps/client/dist/assets/{preview-dialog-DKOvcSoH.js → preview-dialog-sVMONOFM.js} +1 -1
- package/apps/client/dist/assets/use-editor-shortcut-Bqc1G-T5.js +39 -0
- package/apps/client/dist/assets/{video-viewer-DBy-bf6x.js → video-viewer-DX49MZpf.js} +1 -1
- package/apps/client/dist/index.html +6 -6
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +588 -264
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/drizzle/20261104195246_session_provider/migration.sql +1 -0
- package/apps/server/drizzle/20261104195246_session_provider/snapshot.json +398 -0
- package/apps/server/drizzle/20261104222101_last_used_models/migration.sql +6 -0
- package/apps/server/drizzle/20261104222101_last_used_models/snapshot.json +451 -0
- package/package.json +1 -1
- package/apps/client/dist/assets/XIcon-CLtJk0IZ.js +0 -57
- package/apps/client/dist/assets/index-ymO9RTtq.js +0 -72
- package/apps/client/dist/assets/use-editor-shortcut-7fooP7Td.js +0 -39
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import http, { createServer } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import zlib, { createGzip } from "node:zlib";
|
|
6
|
-
import { consola } from "consola";
|
|
6
|
+
import { LogLevels, consola, createConsola } from "consola";
|
|
7
7
|
import { WebSocketServer } from "ws";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import os, { tmpdir } from "node:os";
|
|
10
10
|
import { deleteSession, forkSession, getSessionInfo, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
+
import { formatWithOptions, promisify } from "node:util";
|
|
12
|
+
import { Command } from "@commander-js/extra-typings";
|
|
11
13
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
12
14
|
import readline from "node:readline";
|
|
13
|
-
import { Command } from "@commander-js/extra-typings";
|
|
14
15
|
import { lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
15
16
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
16
17
|
import { OpenCode, isSessionNotFoundError } from "@opencode/client";
|
|
17
18
|
import { ensure, headers } from "@opencode/client/service";
|
|
18
|
-
import { and, eq, gt, inArray, lte, max, sql } from "drizzle-orm";
|
|
19
|
+
import { and, eq, gt, inArray, isNull, lte, max, sql } from "drizzle-orm";
|
|
19
20
|
import { pathToFileURL } from "node:url";
|
|
20
21
|
import { sumBy } from "es-toolkit";
|
|
21
22
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -23,7 +24,6 @@ import { drizzle } from "drizzle-orm/node-sqlite";
|
|
|
23
24
|
import { migrate } from "drizzle-orm/node-sqlite/migrator";
|
|
24
25
|
import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
|
|
25
26
|
import sharp from "sharp";
|
|
26
|
-
import { promisify } from "node:util";
|
|
27
27
|
import webPush from "web-push";
|
|
28
28
|
import dns from "node:dns";
|
|
29
29
|
import { BlockList, isIP } from "node:net";
|
|
@@ -88,9 +88,73 @@ function publish$2(sessionId, event) {
|
|
|
88
88
|
function broadcast(event) {
|
|
89
89
|
for (const client of clients) client.send(event);
|
|
90
90
|
}
|
|
91
|
+
// src/args.ts
|
|
92
|
+
let command = "web";
|
|
93
|
+
let name = null;
|
|
94
|
+
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").option("--connect <addresses>", "Add this machine to the web app with these comma separated addresses; needs --no-auth").configureHelp({ showGlobalOptions: true });
|
|
95
|
+
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
96
|
+
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
97
|
+
command = "serve";
|
|
98
|
+
});
|
|
99
|
+
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) => {
|
|
100
|
+
command = "pair";
|
|
101
|
+
name = options.name ?? null;
|
|
102
|
+
});
|
|
103
|
+
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
104
|
+
command = "unpair";
|
|
105
|
+
});
|
|
106
|
+
program.parse();
|
|
107
|
+
const options = program.opts();
|
|
108
|
+
if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
|
|
109
|
+
if (options.connect !== void 0 && options.auth) program.error("error: --connect needs --no-auth");
|
|
110
|
+
if (options.connect !== void 0 && command === "serve") program.error("error: --connect can't be used with offsite serve");
|
|
111
|
+
const connect$1 = options.connect === void 0 ? null : options.connect.split(",").map((address) => {
|
|
112
|
+
const url = URL.parse(address);
|
|
113
|
+
if (url === null || url.protocol !== "http:" && url.protocol !== "https:") return program.error(`error: --connect address ${address} needs http:// or https://, like https://example.com`);
|
|
114
|
+
return url.origin;
|
|
115
|
+
});
|
|
116
|
+
const args = {
|
|
117
|
+
...options,
|
|
118
|
+
dir: path.resolve(options.dir),
|
|
119
|
+
command,
|
|
120
|
+
name,
|
|
121
|
+
connect: connect$1,
|
|
122
|
+
portGiven: program.getOptionValueSource("port") === "cli"
|
|
123
|
+
};
|
|
124
|
+
const serverDir = path.join(import.meta.dirname, "..");
|
|
125
|
+
// src/lib/log.ts
|
|
126
|
+
const folder$1 = path.join(args.dir, "logs", `${Date.now()}-${process.pid}`);
|
|
127
|
+
const files$1 = /* @__PURE__ */ new Map();
|
|
128
|
+
function write(file, entry) {
|
|
129
|
+
let descriptor = files$1.get(file);
|
|
130
|
+
if (descriptor === void 0) {
|
|
131
|
+
mkdirSync(folder$1, {
|
|
132
|
+
recursive: true,
|
|
133
|
+
mode: 448
|
|
134
|
+
});
|
|
135
|
+
descriptor = openSync(path.join(folder$1, file), "a", 384);
|
|
136
|
+
files$1.set(file, descriptor);
|
|
137
|
+
}
|
|
138
|
+
const tag = entry.tag === "" ? "" : ` [${entry.tag}]`;
|
|
139
|
+
const message = formatWithOptions({
|
|
140
|
+
colors: false,
|
|
141
|
+
breakLength: Infinity
|
|
142
|
+
}, ...entry.args);
|
|
143
|
+
writeSync(descriptor, `${entry.date.toISOString()} ${entry.type.toUpperCase()}${tag} ${message}\n`);
|
|
144
|
+
}
|
|
145
|
+
function fileLogger(file) {
|
|
146
|
+
return createConsola({
|
|
147
|
+
level: LogLevels.verbose,
|
|
148
|
+
throttle: 0,
|
|
149
|
+
reporters: [{ log: (entry) => write(file, entry) }]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const logger = fileLogger("server.log");
|
|
153
|
+
const browserLogger = fileLogger("browser.log");
|
|
91
154
|
// src/services/usage.ts
|
|
92
155
|
const freshFor = 6e4;
|
|
93
156
|
const forcedFreshFor = 1e4;
|
|
157
|
+
const log$20 = logger.withTag("usage");
|
|
94
158
|
const entries = /* @__PURE__ */ new Map();
|
|
95
159
|
const fetching = /* @__PURE__ */ new Map();
|
|
96
160
|
function cachedUsage(accountId) {
|
|
@@ -108,6 +172,7 @@ function recordLimits(accountId, limits) {
|
|
|
108
172
|
limits: [...current.limits.map((limit) => limits.find((item) => item.window === limit.window) ?? limit), ...limits.filter((item) => !current.limits.some((limit) => limit.window === item.window))],
|
|
109
173
|
updatedAt: Date.now()
|
|
110
174
|
});
|
|
175
|
+
log$20.debug(`account ${accountId} reported ${limits.map((limit) => limit.window).join(", ")} limits from a running session`);
|
|
111
176
|
broadcast({
|
|
112
177
|
type: "limits",
|
|
113
178
|
accountId,
|
|
@@ -116,9 +181,15 @@ function recordLimits(accountId, limits) {
|
|
|
116
181
|
}
|
|
117
182
|
async function readUsage(accountId, force) {
|
|
118
183
|
const current = cachedUsage(accountId);
|
|
119
|
-
if (current.fetchedAt !== null && Date.now() - current.fetchedAt < (force ? forcedFreshFor : freshFor))
|
|
184
|
+
if (current.fetchedAt !== null && Date.now() - current.fetchedAt < (force ? forcedFreshFor : freshFor)) {
|
|
185
|
+
log$20.debug(`account ${accountId} served from cache, ${Math.round((Date.now() - current.fetchedAt) / 1e3)}s old${force ? " (forced)" : ""}`);
|
|
186
|
+
return current;
|
|
187
|
+
}
|
|
120
188
|
const pending = fetching.get(accountId);
|
|
121
|
-
if (pending !== void 0)
|
|
189
|
+
if (pending !== void 0) {
|
|
190
|
+
log$20.debug(`account ${accountId} joined the fetch in progress`);
|
|
191
|
+
return pending;
|
|
192
|
+
}
|
|
122
193
|
const next = fetchUsage(accountId);
|
|
123
194
|
fetching.set(accountId, next);
|
|
124
195
|
try {
|
|
@@ -134,15 +205,19 @@ async function fetchUsage(accountId) {
|
|
|
134
205
|
const account = storedAccounts().find((item) => item.id === accountId);
|
|
135
206
|
if (account === void 0) throw new Error("That account was removed");
|
|
136
207
|
let usage;
|
|
208
|
+
const started = performance.now();
|
|
209
|
+
log$20.debug(`fetching limits for ${account.name} (${account.provider})`);
|
|
137
210
|
try {
|
|
211
|
+
const limits = await providerLimits(account);
|
|
212
|
+
log$20.debug(`fetched ${limits.length} limits for ${account.name} in ${Math.round(performance.now() - started)}ms`);
|
|
138
213
|
usage = {
|
|
139
|
-
limits
|
|
214
|
+
limits,
|
|
140
215
|
fetchedAt: Date.now(),
|
|
141
216
|
updatedAt: Date.now(),
|
|
142
217
|
error: null
|
|
143
218
|
};
|
|
144
219
|
} catch (error) {
|
|
145
|
-
|
|
220
|
+
log$20.warn(`could not read usage limits for ${account.name}:`, error);
|
|
146
221
|
usage = {
|
|
147
222
|
...cachedUsage(accountId),
|
|
148
223
|
fetchedAt: Date.now(),
|
|
@@ -290,44 +365,11 @@ function toBlocks(content, answers) {
|
|
|
290
365
|
function isRecord(value) {
|
|
291
366
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
292
367
|
}
|
|
293
|
-
// src/args.ts
|
|
294
|
-
let command = "web";
|
|
295
|
-
let name = null;
|
|
296
|
-
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").option("--connect <addresses>", "Add this machine to the web app with these comma separated addresses; needs --no-auth").configureHelp({ showGlobalOptions: true });
|
|
297
|
-
program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
|
|
298
|
-
program.command("serve").description("Starts only the server, without the web app").action(() => {
|
|
299
|
-
command = "serve";
|
|
300
|
-
});
|
|
301
|
-
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) => {
|
|
302
|
-
command = "pair";
|
|
303
|
-
name = options.name ?? null;
|
|
304
|
-
});
|
|
305
|
-
program.command("unpair").description("Picks paired browsers to unpair").action(() => {
|
|
306
|
-
command = "unpair";
|
|
307
|
-
});
|
|
308
|
-
program.parse();
|
|
309
|
-
const options = program.opts();
|
|
310
|
-
if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
|
|
311
|
-
if (options.connect !== void 0 && options.auth) program.error("error: --connect needs --no-auth");
|
|
312
|
-
if (options.connect !== void 0 && command === "serve") program.error("error: --connect can't be used with offsite serve");
|
|
313
|
-
const connect$1 = options.connect === void 0 ? null : options.connect.split(",").map((address) => {
|
|
314
|
-
const url = URL.parse(address);
|
|
315
|
-
if (url === null || url.protocol !== "http:" && url.protocol !== "https:") return program.error(`error: --connect address ${address} needs http:// or https://, like https://example.com`);
|
|
316
|
-
return url.origin;
|
|
317
|
-
});
|
|
318
|
-
const args = {
|
|
319
|
-
...options,
|
|
320
|
-
dir: path.resolve(options.dir),
|
|
321
|
-
command,
|
|
322
|
-
name,
|
|
323
|
-
connect: connect$1,
|
|
324
|
-
portGiven: program.getOptionValueSource("port") === "cli"
|
|
325
|
-
};
|
|
326
|
-
const serverDir = path.join(import.meta.dirname, "..");
|
|
327
368
|
// src/storage/storage.ts
|
|
328
369
|
const home = args.dir;
|
|
329
370
|
const lockFile = path.join(home, "server.pid");
|
|
330
371
|
const savedServer = z.object({ port: z.number() }).nullable();
|
|
372
|
+
const log$19 = logger.withTag("storage");
|
|
331
373
|
mkdirSync(home, { recursive: true });
|
|
332
374
|
const scratchAvailable = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: home }).status !== 0;
|
|
333
375
|
function listenPort() {
|
|
@@ -359,7 +401,8 @@ function lock() {
|
|
|
359
401
|
const validPid = Number.isInteger(pid) && pid > 0;
|
|
360
402
|
const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
|
|
361
403
|
if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
|
|
362
|
-
|
|
404
|
+
console.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
405
|
+
log$19.error(`offsite is already running for ${home} (pid ${pid})`);
|
|
363
406
|
process.exit(1);
|
|
364
407
|
}
|
|
365
408
|
unlinkSync(lockFile);
|
|
@@ -392,7 +435,7 @@ function readSettings(name, schema, defaults) {
|
|
|
392
435
|
try {
|
|
393
436
|
value = readJson(name);
|
|
394
437
|
} catch (error) {
|
|
395
|
-
|
|
438
|
+
log$19.warn(`invalid ${file}, using the defaults:`, error);
|
|
396
439
|
return structuredClone(defaults);
|
|
397
440
|
}
|
|
398
441
|
if (value === null) return structuredClone(defaults);
|
|
@@ -404,10 +447,10 @@ function readSettings(name, schema, defaults) {
|
|
|
404
447
|
for (const target of targets) {
|
|
405
448
|
const discarded = discard(value, target);
|
|
406
449
|
if (discarded === null) {
|
|
407
|
-
|
|
450
|
+
log$19.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
|
|
408
451
|
return structuredClone(defaults);
|
|
409
452
|
}
|
|
410
|
-
|
|
453
|
+
log$19.warn(`${file}: discarded ${discarded.map(String).join(".")} (${issue.message} at ${target.map(String).join(".")})`);
|
|
411
454
|
}
|
|
412
455
|
result = schema.safeParse(value);
|
|
413
456
|
}
|
|
@@ -475,6 +518,7 @@ function quote(value) {
|
|
|
475
518
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
476
519
|
}
|
|
477
520
|
// src/lib/codex/app-server.ts
|
|
521
|
+
const log$18 = logger.withTag("codex");
|
|
478
522
|
const IDLE_LIMIT$3 = 18e5;
|
|
479
523
|
const START_TIMEOUT = 15e3;
|
|
480
524
|
const REQUEST_TIMEOUT = 3e4;
|
|
@@ -515,6 +559,7 @@ var AppServer = class {
|
|
|
515
559
|
const executable = findCodex();
|
|
516
560
|
if (executable === null) throw new Error("Codex is not installed or not on the PATH");
|
|
517
561
|
this.home = home;
|
|
562
|
+
log$18.info(`starting the Codex app server for ${home}`);
|
|
518
563
|
this.child = spawn(executable, ["app-server"], {
|
|
519
564
|
env: {
|
|
520
565
|
...process.env,
|
|
@@ -541,6 +586,7 @@ var AppServer = class {
|
|
|
541
586
|
requestAttestation: false
|
|
542
587
|
}
|
|
543
588
|
}, START_TIMEOUT).then(() => this.write({ method: "initialized" }), (error) => {
|
|
589
|
+
log$18.warn(`the Codex app server for ${home} did not start:`, error);
|
|
544
590
|
this.child.kill();
|
|
545
591
|
throw error;
|
|
546
592
|
});
|
|
@@ -549,7 +595,15 @@ var AppServer = class {
|
|
|
549
595
|
async request(method, params) {
|
|
550
596
|
await this.ready;
|
|
551
597
|
this.armIdleTimer();
|
|
552
|
-
|
|
598
|
+
const started = performance.now();
|
|
599
|
+
try {
|
|
600
|
+
const result = await this.call(method, params, REQUEST_TIMEOUT);
|
|
601
|
+
log$18.debug(`${method} took ${Math.round(performance.now() - started)}ms`);
|
|
602
|
+
return result;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
log$18.debug(`${method} failed after ${Math.round(performance.now() - started)}ms: ${error}`);
|
|
605
|
+
throw error;
|
|
606
|
+
}
|
|
553
607
|
}
|
|
554
608
|
reply(id, result) {
|
|
555
609
|
this.write({
|
|
@@ -567,6 +621,7 @@ var AppServer = class {
|
|
|
567
621
|
});
|
|
568
622
|
}
|
|
569
623
|
stop() {
|
|
624
|
+
log$18.info(`stopping the Codex app server for ${this.home}`);
|
|
570
625
|
clearTimeout(this.idleTimer);
|
|
571
626
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
572
627
|
this.child.stdin.end();
|
|
@@ -607,7 +662,7 @@ var AppServer = class {
|
|
|
607
662
|
try {
|
|
608
663
|
message = JSON.parse(line);
|
|
609
664
|
} catch {
|
|
610
|
-
|
|
665
|
+
log$18.warn(`ignoring a Codex line that is not JSON: ${line}`);
|
|
611
666
|
return;
|
|
612
667
|
}
|
|
613
668
|
if (message.method === void 0) {
|
|
@@ -657,6 +712,7 @@ var AppServer = class {
|
|
|
657
712
|
exit(error) {
|
|
658
713
|
if (this.exited !== null) return;
|
|
659
714
|
this.exited = error;
|
|
715
|
+
log$18.info(`the Codex app server for ${this.home} exited:`, error.message);
|
|
660
716
|
clearTimeout(this.idleTimer);
|
|
661
717
|
if (servers.get(this.home) === this) servers.delete(this.home);
|
|
662
718
|
for (const waiter of this.waiting.values()) waiter.reject(error);
|
|
@@ -666,6 +722,7 @@ var AppServer = class {
|
|
|
666
722
|
}
|
|
667
723
|
};
|
|
668
724
|
// src/lib/codex/accounts.ts
|
|
725
|
+
const log$17 = logger.withTag("codex");
|
|
669
726
|
const privateEntries = [
|
|
670
727
|
"auth.json",
|
|
671
728
|
"models_cache.json",
|
|
@@ -682,6 +739,7 @@ async function codexAccount(account) {
|
|
|
682
739
|
email: null,
|
|
683
740
|
displayName: null,
|
|
684
741
|
organization: null,
|
|
742
|
+
identity: null,
|
|
685
743
|
plan: null,
|
|
686
744
|
linked
|
|
687
745
|
};
|
|
@@ -696,7 +754,7 @@ async function codexAccount(account) {
|
|
|
696
754
|
case "chatgpt": return {
|
|
697
755
|
...base,
|
|
698
756
|
email: result.account.email,
|
|
699
|
-
|
|
757
|
+
...codexLogin(configDir),
|
|
700
758
|
organization: result.account.workspaceName,
|
|
701
759
|
plan: result.account.planType,
|
|
702
760
|
error: null,
|
|
@@ -768,21 +826,40 @@ function toLimits(snapshot) {
|
|
|
768
826
|
rejected: window.usedPercent >= 100
|
|
769
827
|
}]);
|
|
770
828
|
}
|
|
771
|
-
function
|
|
829
|
+
function codexLogin(home) {
|
|
772
830
|
let raw;
|
|
773
831
|
try {
|
|
774
832
|
raw = JSON.parse(readFileSync(path.join(home, "auth.json"), "utf8"));
|
|
775
833
|
} catch (error) {
|
|
776
|
-
if (error.code !== "ENOENT")
|
|
777
|
-
return
|
|
834
|
+
if (error.code !== "ENOENT") log$17.warn(`could not read the Codex account:`, error);
|
|
835
|
+
return {
|
|
836
|
+
displayName: null,
|
|
837
|
+
identity: null
|
|
838
|
+
};
|
|
778
839
|
}
|
|
779
|
-
if (!isRecord(raw) || !isRecord(raw.tokens) || typeof raw.tokens.id_token !== "string") return
|
|
840
|
+
if (!isRecord(raw) || !isRecord(raw.tokens) || typeof raw.tokens.id_token !== "string") return {
|
|
841
|
+
displayName: null,
|
|
842
|
+
identity: null
|
|
843
|
+
};
|
|
780
844
|
const payload = raw.tokens.id_token.split(".").at(1);
|
|
781
|
-
if (payload === void 0) return
|
|
845
|
+
if (payload === void 0) return {
|
|
846
|
+
displayName: null,
|
|
847
|
+
identity: null
|
|
848
|
+
};
|
|
782
849
|
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
783
|
-
if (!isRecord(claims)
|
|
784
|
-
|
|
785
|
-
|
|
850
|
+
if (!isRecord(claims)) return {
|
|
851
|
+
displayName: null,
|
|
852
|
+
identity: null
|
|
853
|
+
};
|
|
854
|
+
const name = typeof claims.name === "string" ? claims.name.trim() : "";
|
|
855
|
+
const auth = claims["https://api.openai.com/auth"];
|
|
856
|
+
return {
|
|
857
|
+
displayName: name === "" ? null : name,
|
|
858
|
+
identity: isRecord(auth) && typeof auth.chatgpt_user_id === "string" && typeof auth.chatgpt_account_id === "string" ? {
|
|
859
|
+
accountId: auth.chatgpt_user_id,
|
|
860
|
+
organizationId: auth.chatgpt_account_id
|
|
861
|
+
} : null
|
|
862
|
+
};
|
|
786
863
|
}
|
|
787
864
|
function isMirror(dir) {
|
|
788
865
|
try {
|
|
@@ -1069,19 +1146,6 @@ const configSchema = z.strictObject({
|
|
|
1069
1146
|
codex: z.boolean().default(true),
|
|
1070
1147
|
opencode: z.boolean().default(true)
|
|
1071
1148
|
}).prefault({}),
|
|
1072
|
-
newThreads: z.strictObject({
|
|
1073
|
-
provider: z.enum([
|
|
1074
|
-
"claude",
|
|
1075
|
-
"codex",
|
|
1076
|
-
"opencode"
|
|
1077
|
-
]).default("claude"),
|
|
1078
|
-
accountId: z.string().nullable().default(null),
|
|
1079
|
-
models: z.strictObject({
|
|
1080
|
-
claude: z.string().nullable().default(null),
|
|
1081
|
-
codex: z.string().nullable().default(null),
|
|
1082
|
-
opencode: z.string().nullable().default(null)
|
|
1083
|
-
}).prefault({})
|
|
1084
|
-
}).prefault({}),
|
|
1085
1149
|
commitMessage: generationModel,
|
|
1086
1150
|
titleGeneration: generationModel,
|
|
1087
1151
|
models: z.strictObject({
|
|
@@ -1091,7 +1155,11 @@ const configSchema = z.strictObject({
|
|
|
1091
1155
|
}).prefault({}),
|
|
1092
1156
|
delivery: z.enum(["queue", "steer"]).default("steer"),
|
|
1093
1157
|
autoSkipQuestionTimeout: z.number().positive().nullable().default(180),
|
|
1094
|
-
autoPermissionMode: z.enum([
|
|
1158
|
+
autoPermissionMode: z.enum([
|
|
1159
|
+
"bypass",
|
|
1160
|
+
"allow",
|
|
1161
|
+
"deny"
|
|
1162
|
+
]).default("bypass"),
|
|
1095
1163
|
autoPermissionTimeout: z.number().positive().nullable().default(15),
|
|
1096
1164
|
terminal: z.strictObject({
|
|
1097
1165
|
shell: z.strictObject({
|
|
@@ -1153,8 +1221,10 @@ function saveConfig(value) {
|
|
|
1153
1221
|
return result.data;
|
|
1154
1222
|
}
|
|
1155
1223
|
// src/lib/live.ts
|
|
1224
|
+
const log$16 = logger.withTag("runs");
|
|
1156
1225
|
const runs = /* @__PURE__ */ new Map();
|
|
1157
1226
|
const titleJobs = /* @__PURE__ */ new Set();
|
|
1227
|
+
let states = /* @__PURE__ */ new Map();
|
|
1158
1228
|
function activity() {
|
|
1159
1229
|
const result = {};
|
|
1160
1230
|
for (const run of runs.values()) result[run.sessionId] = {
|
|
@@ -1170,6 +1240,19 @@ function activity() {
|
|
|
1170
1240
|
return result;
|
|
1171
1241
|
}
|
|
1172
1242
|
function broadcastActivity() {
|
|
1243
|
+
const next = /* @__PURE__ */ new Map();
|
|
1244
|
+
for (const run of runs.values()) {
|
|
1245
|
+
const state = [
|
|
1246
|
+
run.running ? "running" : "idle",
|
|
1247
|
+
...run.pending.size > 0 ? ["waiting for an answer"] : [],
|
|
1248
|
+
...run.compacting ? ["compacting"] : []
|
|
1249
|
+
].join(", ");
|
|
1250
|
+
next.set(run.sessionId, state);
|
|
1251
|
+
if (!states.has(run.sessionId)) log$16.info(`${run.provider} session ${run.sessionId} opened in ${run.cwd} with account ${run.settings.accountId}`);
|
|
1252
|
+
if (states.get(run.sessionId) !== state) log$16.debug(`session ${run.sessionId} is ${state}`);
|
|
1253
|
+
}
|
|
1254
|
+
for (const sessionId of states.keys()) if (!next.has(sessionId)) log$16.info(`session ${sessionId} closed`);
|
|
1255
|
+
states = next;
|
|
1173
1256
|
broadcast({
|
|
1174
1257
|
type: "activity",
|
|
1175
1258
|
activity: activity()
|
|
@@ -1178,16 +1261,18 @@ function broadcastActivity() {
|
|
|
1178
1261
|
function scheduleAutoResolve(sessionId, request) {
|
|
1179
1262
|
if (request.kind === "plan") return request;
|
|
1180
1263
|
const config = readConfig();
|
|
1181
|
-
const
|
|
1264
|
+
const run = runs.get(sessionId);
|
|
1265
|
+
const immediate = config.autoPermissionMode === "bypass" && (run?.provider === "claude" && run.settings.mode === "bypassPermissions" || run?.provider === "codex" && run.settings.mode === "full-access");
|
|
1266
|
+
const seconds = request.kind === "question" ? config.autoSkipQuestionTimeout : immediate ? 0 : config.autoPermissionTimeout;
|
|
1182
1267
|
if (seconds === null) return request;
|
|
1183
|
-
const decision = request.kind === "question" ? "allow" : config.autoPermissionMode;
|
|
1268
|
+
const decision = request.kind === "question" || config.autoPermissionMode === "bypass" ? "allow" : config.autoPermissionMode;
|
|
1184
1269
|
setTimeout(async () => {
|
|
1185
1270
|
const run = runs.get(sessionId);
|
|
1186
1271
|
if (run === void 0) return;
|
|
1187
1272
|
try {
|
|
1188
1273
|
await run.respond(request.id, decision, request.kind === "question" ? {} : null);
|
|
1189
1274
|
} catch (error) {
|
|
1190
|
-
|
|
1275
|
+
log$16.warn(`could not auto-resolve ${request.id}:`, error);
|
|
1191
1276
|
}
|
|
1192
1277
|
}, seconds * 1e3);
|
|
1193
1278
|
return {
|
|
@@ -1328,6 +1413,7 @@ function reply(method, params, decision, answers) {
|
|
|
1328
1413
|
throw new Error(`offsite cannot answer ${method}`);
|
|
1329
1414
|
}
|
|
1330
1415
|
// src/lib/codex/run.ts
|
|
1416
|
+
const log$15 = logger.withTag("codex");
|
|
1331
1417
|
const IDLE_LIMIT$2 = 18e5;
|
|
1332
1418
|
function codexServer(settings) {
|
|
1333
1419
|
return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
|
|
@@ -1452,9 +1538,10 @@ var CodexRun = class CodexRun {
|
|
|
1452
1538
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1453
1539
|
this.server.threads.delete(this.sessionId);
|
|
1454
1540
|
clearTimeout(this.idleTimer);
|
|
1455
|
-
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) =>
|
|
1541
|
+
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => log$15.warn(`could not unload Codex thread:`, error)).finally(() => this.end());
|
|
1456
1542
|
}
|
|
1457
1543
|
exited() {
|
|
1544
|
+
log$15.warn(`session ${this.sessionId} lost its Codex app server`);
|
|
1458
1545
|
this.put(errorItem(randomUUID(), this.pending.size === 0 ? "Codex stopped" : "Codex stopped, its open requests expired"));
|
|
1459
1546
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1460
1547
|
this.end();
|
|
@@ -1658,6 +1745,7 @@ var CodexRun = class CodexRun {
|
|
|
1658
1745
|
});
|
|
1659
1746
|
}
|
|
1660
1747
|
fail(error) {
|
|
1748
|
+
log$15.warn(`session ${this.sessionId} failed:`, error);
|
|
1661
1749
|
this.put(errorItem(randomUUID(), error instanceof Error ? error.message : String(error)));
|
|
1662
1750
|
}
|
|
1663
1751
|
armIdleTimer() {
|
|
@@ -2524,6 +2612,7 @@ function sessionSettings(session, settings) {
|
|
|
2524
2612
|
};
|
|
2525
2613
|
}
|
|
2526
2614
|
// src/lib/opencode/run.ts
|
|
2615
|
+
const log$14 = logger.withTag("opencode");
|
|
2527
2616
|
const IDLE_LIMIT$1 = 18e5;
|
|
2528
2617
|
const RECENT = 30;
|
|
2529
2618
|
const replies = {
|
|
@@ -2651,6 +2740,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2651
2740
|
this.end();
|
|
2652
2741
|
}
|
|
2653
2742
|
exited() {
|
|
2743
|
+
log$14.warn(`session ${this.sessionId} lost the connection to OpenCode`);
|
|
2654
2744
|
this.put([errorItem(randomUUID(), this.pending.size === 0 ? "Lost the connection to OpenCode" : "Lost the connection to OpenCode, its open requests expired")]);
|
|
2655
2745
|
this.stop();
|
|
2656
2746
|
}
|
|
@@ -2718,7 +2808,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2718
2808
|
}
|
|
2719
2809
|
refresh(client) {
|
|
2720
2810
|
clearTimeout(this.refreshTimer);
|
|
2721
|
-
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) =>
|
|
2811
|
+
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => log$14.warn(`could not load OpenCode messages:`, error));
|
|
2722
2812
|
}
|
|
2723
2813
|
async load(client) {
|
|
2724
2814
|
const messages = (await client.message.list({
|
|
@@ -2835,6 +2925,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2835
2925
|
});
|
|
2836
2926
|
}
|
|
2837
2927
|
fail(error) {
|
|
2928
|
+
log$14.warn(`session ${this.sessionId} failed:`, error);
|
|
2838
2929
|
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2839
2930
|
}
|
|
2840
2931
|
armIdleTimer() {
|
|
@@ -2885,6 +2976,11 @@ const projects$1 = snakeCase.table("projects", {
|
|
|
2885
2976
|
});
|
|
2886
2977
|
const sessions$1 = snakeCase.table("sessions", {
|
|
2887
2978
|
sessionId: text().primaryKey(),
|
|
2979
|
+
provider: text({ enum: [
|
|
2980
|
+
"claude",
|
|
2981
|
+
"codex",
|
|
2982
|
+
"opencode"
|
|
2983
|
+
] }),
|
|
2888
2984
|
interactedAt: integer(),
|
|
2889
2985
|
status: text({ enum: [
|
|
2890
2986
|
"active",
|
|
@@ -2920,6 +3016,16 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2920
3016
|
machine: text().notNull(),
|
|
2921
3017
|
server: text().notNull()
|
|
2922
3018
|
});
|
|
3019
|
+
const lastUsedModels = snakeCase.table("last_used_models", {
|
|
3020
|
+
provider: text({ enum: [
|
|
3021
|
+
"claude",
|
|
3022
|
+
"codex",
|
|
3023
|
+
"opencode"
|
|
3024
|
+
] }).primaryKey(),
|
|
3025
|
+
accountId: text(),
|
|
3026
|
+
model: text().notNull(),
|
|
3027
|
+
usedAt: integer().notNull()
|
|
3028
|
+
});
|
|
2923
3029
|
// src/storage/auth.ts
|
|
2924
3030
|
const CODE_LIFETIME = 3e5;
|
|
2925
3031
|
const DEVICE_LIFETIME = 2592e6;
|
|
@@ -3969,10 +4075,145 @@ async function find(provider, paths) {
|
|
|
3969
4075
|
});
|
|
3970
4076
|
}
|
|
3971
4077
|
}
|
|
4078
|
+
// src/services/threads.ts
|
|
4079
|
+
const log$13 = logger.withTag("threads");
|
|
4080
|
+
const allProviders$2 = [
|
|
4081
|
+
"claude",
|
|
4082
|
+
"codex",
|
|
4083
|
+
"opencode"
|
|
4084
|
+
];
|
|
4085
|
+
const threads = {
|
|
4086
|
+
claude: /* @__PURE__ */ new Map(),
|
|
4087
|
+
codex: /* @__PURE__ */ new Map(),
|
|
4088
|
+
opencode: /* @__PURE__ */ new Map()
|
|
4089
|
+
};
|
|
4090
|
+
const changed = {
|
|
4091
|
+
claude: /* @__PURE__ */ new Set(),
|
|
4092
|
+
codex: /* @__PURE__ */ new Set(),
|
|
4093
|
+
opencode: /* @__PURE__ */ new Set()
|
|
4094
|
+
};
|
|
4095
|
+
const failures = /* @__PURE__ */ new Set();
|
|
4096
|
+
let built = Promise.resolve();
|
|
4097
|
+
let builtFor = "";
|
|
4098
|
+
function rebuildThreads() {
|
|
4099
|
+
const config = readConfig();
|
|
4100
|
+
const key = JSON.stringify([
|
|
4101
|
+
projectPaths$1(),
|
|
4102
|
+
config.enabledProviders,
|
|
4103
|
+
config.claudeBaseDir
|
|
4104
|
+
]);
|
|
4105
|
+
if (key === builtFor) return;
|
|
4106
|
+
builtFor = key;
|
|
4107
|
+
built = relistAll();
|
|
4108
|
+
}
|
|
4109
|
+
function threadsReady() {
|
|
4110
|
+
return built;
|
|
4111
|
+
}
|
|
4112
|
+
function threadFailed(provider) {
|
|
4113
|
+
return failures.has(provider);
|
|
4114
|
+
}
|
|
4115
|
+
function knownThreads() {
|
|
4116
|
+
const known = new Set(projects().map((project) => project.id));
|
|
4117
|
+
return allProviders$2.filter((provider) => readConfig().enabledProviders[provider]).flatMap((provider) => [...threads[provider].values()]).filter((item) => known.has(item.projectId));
|
|
4118
|
+
}
|
|
4119
|
+
async function providerThreads(provider) {
|
|
4120
|
+
await built;
|
|
4121
|
+
if (failures.has(provider)) throw new Error(`could not list ${providerName(provider)} sessions`);
|
|
4122
|
+
return knownThreads().filter((item) => item.provider === provider);
|
|
4123
|
+
}
|
|
4124
|
+
function threadOf(provider, id) {
|
|
4125
|
+
return threads[provider].get(id);
|
|
4126
|
+
}
|
|
4127
|
+
function threadProvider(id) {
|
|
4128
|
+
return allProviders$2.find((provider) => threads[provider].has(id));
|
|
4129
|
+
}
|
|
4130
|
+
function markChanged(provider, id) {
|
|
4131
|
+
changed[provider].add(id);
|
|
4132
|
+
}
|
|
4133
|
+
function fileChanged(provider, filename) {
|
|
4134
|
+
if (filename === null) return;
|
|
4135
|
+
if (provider === "claude") {
|
|
4136
|
+
const parts = filename.split(path.sep);
|
|
4137
|
+
if (parts.length === 2 && parts[1].endsWith(".jsonl")) changed.claude.add(path.basename(parts[1], ".jsonl"));
|
|
4138
|
+
return;
|
|
4139
|
+
}
|
|
4140
|
+
const id = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(filename)?.[1];
|
|
4141
|
+
if (id !== void 0) changed.codex.add(id);
|
|
4142
|
+
}
|
|
4143
|
+
function forgetThread(provider, id) {
|
|
4144
|
+
threads[provider].delete(id);
|
|
4145
|
+
broadcast({ type: "threads" });
|
|
4146
|
+
}
|
|
4147
|
+
async function updateThreads(provider) {
|
|
4148
|
+
if (provider === "opencode") {
|
|
4149
|
+
await relist(provider);
|
|
4150
|
+
return;
|
|
4151
|
+
}
|
|
4152
|
+
const ids = [...changed[provider]];
|
|
4153
|
+
changed[provider].clear();
|
|
4154
|
+
if (ids.length === 0) return;
|
|
4155
|
+
await built;
|
|
4156
|
+
const paths = new Map(projectPaths$1());
|
|
4157
|
+
const results = await Promise.all(ids.map(async (id) => {
|
|
4158
|
+
try {
|
|
4159
|
+
return {
|
|
4160
|
+
id,
|
|
4161
|
+
item: await timeLimit(lookup$1(provider, id, paths), `${providerName(provider)} did not find session ${id} in time`)
|
|
4162
|
+
};
|
|
4163
|
+
} catch (error) {
|
|
4164
|
+
log$13.warn(`could not update ${providerName(provider)} session:`, error);
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
}));
|
|
4168
|
+
let updated = false;
|
|
4169
|
+
for (const result of results) {
|
|
4170
|
+
if (result === null) continue;
|
|
4171
|
+
const before = threads[provider].get(result.id);
|
|
4172
|
+
if (result.item === null) {
|
|
4173
|
+
updated ||= before !== void 0;
|
|
4174
|
+
threads[provider].delete(result.id);
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
4177
|
+
updated ||= JSON.stringify(before) !== JSON.stringify(result.item);
|
|
4178
|
+
threads[provider].set(result.id, result.item);
|
|
4179
|
+
}
|
|
4180
|
+
if (updated) broadcast({ type: "threads" });
|
|
4181
|
+
}
|
|
4182
|
+
async function lookup$1(provider, id, paths) {
|
|
4183
|
+
const known = threads[provider].get(id);
|
|
4184
|
+
const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
|
|
4185
|
+
if (knownPath === void 0) return info(provider, id, paths);
|
|
4186
|
+
return await info(provider, id, new Map([knownPath])) ?? info(provider, id, paths);
|
|
4187
|
+
}
|
|
4188
|
+
async function relistAll() {
|
|
4189
|
+
await Promise.all(allProviders$2.map((provider) => relist(provider)));
|
|
4190
|
+
}
|
|
4191
|
+
async function relist(provider) {
|
|
4192
|
+
if (!readConfig().enabledProviders[provider]) {
|
|
4193
|
+
threads[provider] = /* @__PURE__ */ new Map();
|
|
4194
|
+
failures.delete(provider);
|
|
4195
|
+
broadcast({ type: "threads" });
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
try {
|
|
4199
|
+
const found = await timeLimit(find(provider, new Map(projectPaths$1())), `${providerName(provider)} did not list its sessions in time`) ?? [];
|
|
4200
|
+
threads[provider] = new Map(found.map((item) => [item.id, item]));
|
|
4201
|
+
failures.delete(provider);
|
|
4202
|
+
} catch (error) {
|
|
4203
|
+
log$13.warn(`could not list all ${providerName(provider)} sessions:`, error);
|
|
4204
|
+
failures.add(provider);
|
|
4205
|
+
builtFor = "";
|
|
4206
|
+
}
|
|
4207
|
+
broadcast({ type: "threads" });
|
|
4208
|
+
}
|
|
4209
|
+
function projectPaths$1() {
|
|
4210
|
+
return projects().map((project) => [project.path, project.id]);
|
|
4211
|
+
}
|
|
3972
4212
|
// src/services/sessions.ts
|
|
4213
|
+
const log$12 = logger.withTag("sessions");
|
|
3973
4214
|
const day = 864e5;
|
|
3974
4215
|
const REFRESH_DELAY = 1e3;
|
|
3975
|
-
const allProviders = [
|
|
4216
|
+
const allProviders$1 = [
|
|
3976
4217
|
"claude",
|
|
3977
4218
|
"codex",
|
|
3978
4219
|
"opencode"
|
|
@@ -4005,61 +4246,35 @@ const providers = /* @__PURE__ */ new Map();
|
|
|
4005
4246
|
let watchers = [];
|
|
4006
4247
|
function sessions() {
|
|
4007
4248
|
const known = new Set(projects().map((project) => project.id));
|
|
4008
|
-
return withStatus(allProviders.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
|
|
4249
|
+
return withStatus(allProviders$1.flatMap((provider) => listed[provider]).filter((item) => known.has(item.projectId))).filter((session) => session.status !== "inactive");
|
|
4009
4250
|
}
|
|
4010
|
-
async function inactiveSessions(
|
|
4011
|
-
|
|
4251
|
+
async function inactiveSessions(params) {
|
|
4252
|
+
await threadsReady();
|
|
4253
|
+
const scope = new Set(projects().filter((project) => params.projectId === null || project.id === params.projectId || params.projectId === "no-project" && project.scratch).map((project) => project.id));
|
|
4254
|
+
const found = withStatus(knownThreads().filter((item) => scope.has(item.projectId))).filter((session) => session.status === "inactive").sort((a, b) => b.updatedAt - a.updatedAt);
|
|
4255
|
+
return {
|
|
4256
|
+
sessions: found.slice(0, params.limit),
|
|
4257
|
+
total: found.length
|
|
4258
|
+
};
|
|
4012
4259
|
}
|
|
4013
4260
|
async function searchableSessions() {
|
|
4014
|
-
|
|
4015
|
-
const
|
|
4016
|
-
try {
|
|
4017
|
-
return {
|
|
4018
|
-
provider,
|
|
4019
|
-
items: await timeLimit(find(provider, new Map(paths)), `${providerName(provider)} did not list its sessions in time`) ?? [],
|
|
4020
|
-
failed: false
|
|
4021
|
-
};
|
|
4022
|
-
} catch (error) {
|
|
4023
|
-
console.warn(`could not search ${providerName(provider)}: ${error}`);
|
|
4024
|
-
return {
|
|
4025
|
-
provider,
|
|
4026
|
-
items: [],
|
|
4027
|
-
failed: true
|
|
4028
|
-
};
|
|
4029
|
-
}
|
|
4030
|
-
}));
|
|
4031
|
-
const found = lists.flatMap((list) => list.items);
|
|
4032
|
-
for (const item of found) providers.set(item.id, item.provider);
|
|
4261
|
+
await threadsReady();
|
|
4262
|
+
const found = knownThreads();
|
|
4033
4263
|
const opens = new Map(db.select().from(sessionOpens).all().map((record) => [record.sessionId, record]));
|
|
4034
4264
|
return {
|
|
4035
4265
|
sessions: withStatus(found).map((session) => ({
|
|
4036
4266
|
...session,
|
|
4037
4267
|
opens: opens.get(session.id) ?? null
|
|
4038
4268
|
})),
|
|
4039
|
-
failed:
|
|
4269
|
+
failed: enabledProviders().filter((provider) => threadFailed(provider))
|
|
4040
4270
|
};
|
|
4041
4271
|
}
|
|
4042
4272
|
async function findSession(id) {
|
|
4043
|
-
|
|
4273
|
+
if (ownerOf(id) === void 0) await threadsReady();
|
|
4044
4274
|
const owner = ownerOf(id);
|
|
4045
|
-
if (owner
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
}
|
|
4049
|
-
for (const provider of allProviders) {
|
|
4050
|
-
if (!readConfig().enabledProviders[provider]) continue;
|
|
4051
|
-
const item = await info(provider, id, paths).catch(() => null);
|
|
4052
|
-
if (item !== null) {
|
|
4053
|
-
providers.set(id, provider);
|
|
4054
|
-
return withStatus([item])[0];
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
return null;
|
|
4058
|
-
}
|
|
4059
|
-
async function everything(paths) {
|
|
4060
|
-
const found = (await Promise.all(enabledProviders().map((provider) => find(provider, new Map(paths))))).flatMap((items) => items ?? []);
|
|
4061
|
-
for (const item of found) providers.set(item.id, item.provider);
|
|
4062
|
-
return withStatus(found);
|
|
4275
|
+
if (owner === void 0) return null;
|
|
4276
|
+
const item = await info(owner, id, new Map(projectPaths()));
|
|
4277
|
+
return item === null ? null : withStatus([item])[0];
|
|
4063
4278
|
}
|
|
4064
4279
|
function withStatus(items) {
|
|
4065
4280
|
const records = new Map(db.select().from(sessions$1).all().map((record) => [record.sessionId, record]));
|
|
@@ -4074,16 +4289,16 @@ function withStatus(items) {
|
|
|
4074
4289
|
});
|
|
4075
4290
|
}
|
|
4076
4291
|
function enabledProviders() {
|
|
4077
|
-
return allProviders.filter((provider) => readConfig().enabledProviders[provider]);
|
|
4292
|
+
return allProviders$1.filter((provider) => readConfig().enabledProviders[provider]);
|
|
4078
4293
|
}
|
|
4079
4294
|
function projectPaths() {
|
|
4080
4295
|
return projects().map((project) => [project.path, project.id]);
|
|
4081
4296
|
}
|
|
4082
4297
|
function ownerOf(sessionId) {
|
|
4083
|
-
return runs.get(sessionId)?.provider ?? providers.get(sessionId);
|
|
4298
|
+
return runs.get(sessionId)?.provider ?? db.select({ provider: sessions$1.provider }).from(sessions$1).where(eq(sessions$1.sessionId, sessionId)).get()?.provider ?? providers.get(sessionId) ?? threadProvider(sessionId);
|
|
4084
4299
|
}
|
|
4085
4300
|
function providerStatuses() {
|
|
4086
|
-
return allProviders.map((provider) => statuses[provider]);
|
|
4301
|
+
return allProviders$1.map((provider) => statuses[provider]);
|
|
4087
4302
|
}
|
|
4088
4303
|
function listedUpdatedAt(provider, sessionId) {
|
|
4089
4304
|
return listed[provider].find((item) => item.id === sessionId)?.updatedAt ?? null;
|
|
@@ -4093,14 +4308,16 @@ function providerOf(sessionId) {
|
|
|
4093
4308
|
if (provider === void 0) throw new Error("offsite does not know this session yet, reload the page");
|
|
4094
4309
|
return provider;
|
|
4095
4310
|
}
|
|
4096
|
-
function recordInteraction(id) {
|
|
4311
|
+
function recordInteraction(id, provider) {
|
|
4097
4312
|
db.insert(sessions$1).values({
|
|
4098
4313
|
sessionId: id,
|
|
4314
|
+
provider,
|
|
4099
4315
|
interactedAt: Date.now(),
|
|
4100
4316
|
status: "active"
|
|
4101
4317
|
}).onConflictDoUpdate({
|
|
4102
4318
|
target: sessions$1.sessionId,
|
|
4103
4319
|
set: {
|
|
4320
|
+
provider,
|
|
4104
4321
|
interactedAt: Date.now(),
|
|
4105
4322
|
status: sql`CASE ${sessions$1.status} WHEN 'pinned' THEN 'pinned' WHEN 'snoozed' THEN 'snoozed' WHEN 'minimized' THEN 'minimized' ELSE 'active' END`
|
|
4106
4323
|
}
|
|
@@ -4123,13 +4340,15 @@ function publishSessions() {
|
|
|
4123
4340
|
});
|
|
4124
4341
|
}
|
|
4125
4342
|
function refreshAll() {
|
|
4126
|
-
|
|
4343
|
+
rebuildThreads();
|
|
4344
|
+
for (const provider of allProviders$1) refresh(provider);
|
|
4127
4345
|
}
|
|
4128
4346
|
function refreshSessions(provider) {
|
|
4129
4347
|
if (timers.has(provider)) return;
|
|
4130
4348
|
timers.set(provider, setTimeout(() => {
|
|
4131
4349
|
timers.delete(provider);
|
|
4132
4350
|
refresh(provider);
|
|
4351
|
+
updateThreads(provider);
|
|
4133
4352
|
}, REFRESH_DELAY));
|
|
4134
4353
|
}
|
|
4135
4354
|
async function refresh(provider) {
|
|
@@ -4178,7 +4397,7 @@ async function list(provider) {
|
|
|
4178
4397
|
}
|
|
4179
4398
|
} catch (error) {
|
|
4180
4399
|
const message = error instanceof Error ? error.message : String(error);
|
|
4181
|
-
|
|
4400
|
+
log$12.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
4182
4401
|
statuses[provider] = {
|
|
4183
4402
|
provider,
|
|
4184
4403
|
state: "error",
|
|
@@ -4194,15 +4413,10 @@ async function list(provider) {
|
|
|
4194
4413
|
}
|
|
4195
4414
|
async function tracked(provider, paths) {
|
|
4196
4415
|
if (provider === "codex" && !codexInstalled() || provider === "opencode" && !openCodeInstalled()) return null;
|
|
4197
|
-
return (await Promise.all(db.select().from(sessions$1).all().map((record) =>
|
|
4198
|
-
const owner = ownerOf(record.sessionId);
|
|
4199
|
-
if (owner === provider) return lookup(provider, record.sessionId, paths);
|
|
4200
|
-
if (owner === void 0) return lookup(provider, record.sessionId, paths).catch(() => null);
|
|
4201
|
-
return null;
|
|
4202
|
-
}))).filter((item) => item !== null);
|
|
4416
|
+
return (await Promise.all(db.select().from(sessions$1).all().filter((record) => record.provider === provider).map((record) => lookup(provider, record.sessionId, paths)))).filter((item) => item !== null);
|
|
4203
4417
|
}
|
|
4204
4418
|
async function lookup(provider, sessionId, paths) {
|
|
4205
|
-
const known = listed[provider].find((item) => item.id === sessionId);
|
|
4419
|
+
const known = threadOf(provider, sessionId) ?? listed[provider].find((item) => item.id === sessionId);
|
|
4206
4420
|
const knownPath = known === void 0 ? void 0 : [...paths].find((entry) => entry[1] === known.projectId);
|
|
4207
4421
|
if (knownPath === void 0) return info(provider, sessionId, paths);
|
|
4208
4422
|
return await info(provider, sessionId, new Map([knownPath])) ?? info(provider, sessionId, paths);
|
|
@@ -4212,11 +4426,16 @@ function watchSessions() {
|
|
|
4212
4426
|
watchers = [["claude", path.join(claudeBaseDir(), "projects")], ["codex", path.join(codexBaseDir, "sessions")]].flatMap(([provider, dir]) => {
|
|
4213
4427
|
if (!readConfig().enabledProviders[provider]) return [];
|
|
4214
4428
|
try {
|
|
4215
|
-
const watcher = watch(dir, { recursive: true }, () =>
|
|
4216
|
-
|
|
4429
|
+
const watcher = watch(dir, { recursive: true }, (_, filename) => {
|
|
4430
|
+
fileChanged(provider, filename);
|
|
4431
|
+
refreshSessions(provider);
|
|
4432
|
+
});
|
|
4433
|
+
watcher.on("error", (error) => log$12.warn(`stopped watching ${dir}:`, error));
|
|
4434
|
+
log$12.debug(`watching ${dir}`);
|
|
4217
4435
|
return [watcher];
|
|
4218
4436
|
} catch (error) {
|
|
4219
4437
|
if (error.code !== "ENOENT") throw error;
|
|
4438
|
+
log$12.debug(`not watching ${dir}, it does not exist`);
|
|
4220
4439
|
return [];
|
|
4221
4440
|
}
|
|
4222
4441
|
});
|
|
@@ -4250,16 +4469,24 @@ async function updateSession(id, patch) {
|
|
|
4250
4469
|
status: patch.status,
|
|
4251
4470
|
interactedAt: Date.now()
|
|
4252
4471
|
} : { status: patch.status };
|
|
4472
|
+
const provider = providerOf(id);
|
|
4253
4473
|
db.insert(sessions$1).values({
|
|
4254
4474
|
sessionId: id,
|
|
4475
|
+
provider,
|
|
4255
4476
|
...set
|
|
4256
4477
|
}).onConflictDoUpdate({
|
|
4257
4478
|
target: sessions$1.sessionId,
|
|
4258
|
-
set
|
|
4479
|
+
set: {
|
|
4480
|
+
provider,
|
|
4481
|
+
...set
|
|
4482
|
+
}
|
|
4259
4483
|
}).run();
|
|
4260
|
-
await refresh(
|
|
4484
|
+
await refresh(provider);
|
|
4485
|
+
}
|
|
4486
|
+
if (patch.title !== void 0) {
|
|
4487
|
+
markChanged(providerOf(id), id);
|
|
4488
|
+
refreshSessions(providerOf(id));
|
|
4261
4489
|
}
|
|
4262
|
-
if (patch.title !== void 0) refreshSessions(providerOf(id));
|
|
4263
4490
|
}
|
|
4264
4491
|
async function removeSession(id, projectId) {
|
|
4265
4492
|
const provider = providerOf(id);
|
|
@@ -4280,19 +4507,19 @@ async function removeSession(id, projectId) {
|
|
|
4280
4507
|
db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
|
|
4281
4508
|
db.delete(sessionOpens).where(eq(sessionOpens.sessionId, id)).run();
|
|
4282
4509
|
listed[provider] = listed[provider].filter((item) => item.id !== id);
|
|
4510
|
+
forgetThread(provider, id);
|
|
4283
4511
|
const project = projects().find((item) => item.id === projectId);
|
|
4284
4512
|
if (project === void 0 || !project.scratch) return;
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
console.warn(`could not check other chats in ${project.path}: ${error}`);
|
|
4289
|
-
}
|
|
4513
|
+
await threadsReady();
|
|
4514
|
+
if (enabledProviders().some((owner) => threadFailed(owner))) return;
|
|
4515
|
+
if (!knownThreads().some((item) => item.projectId === project.id)) removeProject(project.id);
|
|
4290
4516
|
}
|
|
4291
4517
|
async function fork(sessionId, projectId, messageId) {
|
|
4292
4518
|
const provider = providerOf(sessionId);
|
|
4293
4519
|
const result = await forkWith(provider, sessionId, projectPath(projectId), messageId);
|
|
4294
4520
|
providers.set(result.sessionId, provider);
|
|
4295
|
-
recordInteraction(result.sessionId);
|
|
4521
|
+
recordInteraction(result.sessionId, provider);
|
|
4522
|
+
markChanged(provider, result.sessionId);
|
|
4296
4523
|
refreshSessions(provider);
|
|
4297
4524
|
return result;
|
|
4298
4525
|
}
|
|
@@ -4306,6 +4533,7 @@ async function forkWith(provider, sessionId, cwd, messageId) {
|
|
|
4306
4533
|
})).sessionId };
|
|
4307
4534
|
}
|
|
4308
4535
|
// src/lib/opencode/client.ts
|
|
4536
|
+
const log$11 = logger.withTag("opencode");
|
|
4309
4537
|
const listChanges = /* @__PURE__ */ new Set([
|
|
4310
4538
|
"session.created",
|
|
4311
4539
|
"session.renamed",
|
|
@@ -4351,6 +4579,7 @@ async function start() {
|
|
|
4351
4579
|
}
|
|
4352
4580
|
async function connect() {
|
|
4353
4581
|
const endpoint = await ensure();
|
|
4582
|
+
log$11.info(`connected to OpenCode at ${endpoint.url}`);
|
|
4354
4583
|
return OpenCode.make({
|
|
4355
4584
|
baseUrl: endpoint.url,
|
|
4356
4585
|
headers: headers(endpoint)
|
|
@@ -4365,10 +4594,11 @@ async function listen(client) {
|
|
|
4365
4594
|
signal: controller.signal,
|
|
4366
4595
|
onActivity: () => silence.refresh()
|
|
4367
4596
|
})) route(event);
|
|
4368
|
-
|
|
4597
|
+
log$11.warn("the OpenCode event stream ended");
|
|
4369
4598
|
} catch (error) {
|
|
4370
4599
|
if (listener !== controller) return;
|
|
4371
|
-
|
|
4600
|
+
if (controller.signal.aborted) log$11.warn(`the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds`);
|
|
4601
|
+
else log$11.warn("lost the OpenCode event stream:", error);
|
|
4372
4602
|
} finally {
|
|
4373
4603
|
clearTimeout(silence);
|
|
4374
4604
|
}
|
|
@@ -4384,14 +4614,15 @@ async function reconnect() {
|
|
|
4384
4614
|
try {
|
|
4385
4615
|
client = await connect();
|
|
4386
4616
|
} catch (error) {
|
|
4617
|
+
log$11.debug(`reconnect attempt ${attempt + 1} to OpenCode failed:`, error);
|
|
4387
4618
|
failure = error;
|
|
4388
4619
|
continue;
|
|
4389
4620
|
}
|
|
4390
4621
|
listen(client);
|
|
4391
|
-
await catchUp(client).catch((error) =>
|
|
4622
|
+
await catchUp(client).catch((error) => log$11.warn(`could not catch up with OpenCode:`, error));
|
|
4392
4623
|
return client;
|
|
4393
4624
|
}
|
|
4394
|
-
|
|
4625
|
+
log$11.warn(`could not reconnect to OpenCode:`, failure);
|
|
4395
4626
|
connection = void 0;
|
|
4396
4627
|
for (const run of openCodeRuns()) run.exited();
|
|
4397
4628
|
refreshSessions("opencode");
|
|
@@ -4422,6 +4653,7 @@ function openCodeRuns() {
|
|
|
4422
4653
|
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
4423
4654
|
}
|
|
4424
4655
|
// src/services/accounts.ts
|
|
4656
|
+
const log$10 = logger.withTag("accounts");
|
|
4425
4657
|
const providerNames = {
|
|
4426
4658
|
claude: "Claude",
|
|
4427
4659
|
codex: "Codex",
|
|
@@ -4489,6 +4721,7 @@ function disabledAccount(account) {
|
|
|
4489
4721
|
email: null,
|
|
4490
4722
|
displayName: null,
|
|
4491
4723
|
organization: null,
|
|
4724
|
+
identity: null,
|
|
4492
4725
|
plan: null,
|
|
4493
4726
|
error: null,
|
|
4494
4727
|
linked: true,
|
|
@@ -4502,7 +4735,7 @@ async function publishAccounts() {
|
|
|
4502
4735
|
...await accounts()
|
|
4503
4736
|
});
|
|
4504
4737
|
} catch (error) {
|
|
4505
|
-
|
|
4738
|
+
log$10.warn(`could not list accounts:`, error);
|
|
4506
4739
|
}
|
|
4507
4740
|
}
|
|
4508
4741
|
async function capabilities(provider, accountId, cwd) {
|
|
@@ -4609,6 +4842,7 @@ function validAccount(name, configDir) {
|
|
|
4609
4842
|
throw new Error("Use an absolute config directory or one starting with ~/");
|
|
4610
4843
|
}
|
|
4611
4844
|
// src/services/push.ts
|
|
4845
|
+
const log$9 = logger.withTag("push");
|
|
4612
4846
|
const TITLE_LIMIT = 100;
|
|
4613
4847
|
const BODY_LIMIT = 500;
|
|
4614
4848
|
const TIME_TO_LIVE = 900;
|
|
@@ -4656,12 +4890,12 @@ function subscribe(registration, client) {
|
|
|
4656
4890
|
set: values
|
|
4657
4891
|
}).run();
|
|
4658
4892
|
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4659
|
-
|
|
4893
|
+
log$9.info("a browser subscribed to push notifications");
|
|
4660
4894
|
}
|
|
4661
4895
|
function unsubscribe(endpoint) {
|
|
4662
4896
|
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4663
4897
|
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4664
|
-
|
|
4898
|
+
log$9.info("a browser unsubscribed from push notifications");
|
|
4665
4899
|
}
|
|
4666
4900
|
async function sendPush(message) {
|
|
4667
4901
|
const subscriptions = targets();
|
|
@@ -4672,7 +4906,7 @@ async function sendPush(message) {
|
|
|
4672
4906
|
body: [...message.body].slice(0, BODY_LIMIT).join("")
|
|
4673
4907
|
};
|
|
4674
4908
|
const gone = [];
|
|
4675
|
-
let
|
|
4909
|
+
let delivered = 0;
|
|
4676
4910
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4677
4911
|
try {
|
|
4678
4912
|
await webPush.sendNotification({
|
|
@@ -4694,7 +4928,7 @@ async function sendPush(message) {
|
|
|
4694
4928
|
urgency: "high",
|
|
4695
4929
|
timeout: SEND_TIMEOUT
|
|
4696
4930
|
});
|
|
4697
|
-
|
|
4931
|
+
delivered += 1;
|
|
4698
4932
|
} catch (error) {
|
|
4699
4933
|
const status = error.statusCode;
|
|
4700
4934
|
if (status !== void 0 && [
|
|
@@ -4703,22 +4937,24 @@ async function sendPush(message) {
|
|
|
4703
4937
|
404,
|
|
4704
4938
|
410
|
|
4705
4939
|
].includes(status)) {
|
|
4706
|
-
|
|
4940
|
+
log$9.info(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
|
|
4707
4941
|
gone.push(subscription.endpoint);
|
|
4708
4942
|
return;
|
|
4709
4943
|
}
|
|
4710
|
-
|
|
4944
|
+
log$9.warn(`could not send a push to ${subscription.endpoint}:`, error);
|
|
4711
4945
|
}
|
|
4712
4946
|
}));
|
|
4713
|
-
|
|
4947
|
+
log$9.debug(`sent a push to ${delivered} of ${subscriptions.length} browsers`);
|
|
4948
|
+
if (gone.length === 0) return delivered > 0;
|
|
4714
4949
|
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4715
|
-
return
|
|
4950
|
+
return delivered > 0;
|
|
4716
4951
|
}
|
|
4717
4952
|
function targets() {
|
|
4718
4953
|
if (notifier === null || notifier.endpoint === null) return [];
|
|
4719
4954
|
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4720
4955
|
}
|
|
4721
4956
|
// src/lib/claude/claude.ts
|
|
4957
|
+
const log$8 = logger.withTag("claude");
|
|
4722
4958
|
const efforts = [
|
|
4723
4959
|
"low",
|
|
4724
4960
|
"medium",
|
|
@@ -4857,7 +5093,7 @@ async function claudeAccount(account) {
|
|
|
4857
5093
|
return {
|
|
4858
5094
|
...account,
|
|
4859
5095
|
email: info.email ?? null,
|
|
4860
|
-
|
|
5096
|
+
...claudeLogin(configDir),
|
|
4861
5097
|
organization: info.organization ?? null,
|
|
4862
5098
|
plan: info.subscriptionType ?? null,
|
|
4863
5099
|
error: null,
|
|
@@ -4870,6 +5106,7 @@ async function claudeAccount(account) {
|
|
|
4870
5106
|
email: null,
|
|
4871
5107
|
displayName: null,
|
|
4872
5108
|
organization: null,
|
|
5109
|
+
identity: null,
|
|
4873
5110
|
plan: null,
|
|
4874
5111
|
error: error instanceof Error ? error.message : String(error),
|
|
4875
5112
|
linked,
|
|
@@ -4948,18 +5185,31 @@ async function claudeLimits(account) {
|
|
|
4948
5185
|
probe.close();
|
|
4949
5186
|
}
|
|
4950
5187
|
}
|
|
4951
|
-
function
|
|
5188
|
+
function claudeLogin(configDir) {
|
|
4952
5189
|
const file = path.resolve(configDir) === path.join(os.homedir(), ".claude") ? path.join(os.homedir(), ".claude.json") : path.join(configDir, ".claude.json");
|
|
4953
5190
|
let raw;
|
|
4954
5191
|
try {
|
|
4955
5192
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
4956
5193
|
} catch (error) {
|
|
4957
|
-
if (error.code !== "ENOENT")
|
|
4958
|
-
return
|
|
5194
|
+
if (error.code !== "ENOENT") log$8.warn(`could not read the Claude account:`, error);
|
|
5195
|
+
return {
|
|
5196
|
+
displayName: null,
|
|
5197
|
+
identity: null
|
|
5198
|
+
};
|
|
4959
5199
|
}
|
|
4960
|
-
if (!isRecord(raw) || !isRecord(raw.oauthAccount)
|
|
4961
|
-
|
|
4962
|
-
|
|
5200
|
+
if (!isRecord(raw) || !isRecord(raw.oauthAccount)) return {
|
|
5201
|
+
displayName: null,
|
|
5202
|
+
identity: null
|
|
5203
|
+
};
|
|
5204
|
+
const login = raw.oauthAccount;
|
|
5205
|
+
const name = typeof login.displayName === "string" ? login.displayName.trim() : "";
|
|
5206
|
+
return {
|
|
5207
|
+
displayName: name === "" ? null : name,
|
|
5208
|
+
identity: typeof login.accountUuid === "string" && typeof login.organizationUuid === "string" ? {
|
|
5209
|
+
accountId: login.accountUuid,
|
|
5210
|
+
organizationId: login.organizationUuid
|
|
5211
|
+
} : null
|
|
5212
|
+
};
|
|
4963
5213
|
}
|
|
4964
5214
|
function isLinked(configDir) {
|
|
4965
5215
|
const base = claudeBaseDir();
|
|
@@ -5735,7 +5985,10 @@ var Run = class {
|
|
|
5735
5985
|
try {
|
|
5736
5986
|
for await (const message of this.query) this.receive(message);
|
|
5737
5987
|
} catch (error) {
|
|
5738
|
-
if (!this.stopping)
|
|
5988
|
+
if (!this.stopping) {
|
|
5989
|
+
logger.withTag("claude").warn(`${this.sessionId} failed:`, error);
|
|
5990
|
+
this.add(notice(error instanceof Error ? error.message : String(error)));
|
|
5991
|
+
}
|
|
5739
5992
|
}
|
|
5740
5993
|
clearTimeout(this.idleTimer);
|
|
5741
5994
|
for (const id of this.pending.keys()) this.settle(id, {
|
|
@@ -6166,7 +6419,7 @@ async function submitFirst(client, run, request, projectId) {
|
|
|
6166
6419
|
from: request.projectId
|
|
6167
6420
|
});
|
|
6168
6421
|
await run.submit(request.prompt, request.delivery);
|
|
6169
|
-
recordInteraction(sessionId);
|
|
6422
|
+
recordInteraction(sessionId, run.provider);
|
|
6170
6423
|
}
|
|
6171
6424
|
async function sendExisting(client, sessionId, cwd, request) {
|
|
6172
6425
|
const provider = providerOf(sessionId);
|
|
@@ -6180,7 +6433,7 @@ async function sendExisting(client, sessionId, cwd, request) {
|
|
|
6180
6433
|
if (run === existing) await run.configure(request.settings);
|
|
6181
6434
|
else await open$1(client, sessionId, request.projectId);
|
|
6182
6435
|
await run.submit(request.prompt, request.delivery);
|
|
6183
|
-
recordInteraction(sessionId);
|
|
6436
|
+
recordInteraction(sessionId, provider);
|
|
6184
6437
|
if (emptied.has(sessionId) && request.prompt.text.trim() !== "") {
|
|
6185
6438
|
emptied.delete(sessionId);
|
|
6186
6439
|
await updateSession(sessionId, { title: request.prompt.text.trim() });
|
|
@@ -6353,6 +6606,7 @@ function fenced(language, text) {
|
|
|
6353
6606
|
return `${fence}${language}\n${text}\n${fence}`;
|
|
6354
6607
|
}
|
|
6355
6608
|
// src/services/message-search.ts
|
|
6609
|
+
const log$7 = logger.withTag("search");
|
|
6356
6610
|
const READ_LIMIT = 30;
|
|
6357
6611
|
const CODEX_READ_LIMIT = 10;
|
|
6358
6612
|
const RG_TIME_LIMIT = 5e3;
|
|
@@ -6363,7 +6617,6 @@ async function searchMessages(query) {
|
|
|
6363
6617
|
matches: [],
|
|
6364
6618
|
failed: []
|
|
6365
6619
|
};
|
|
6366
|
-
const paths = new Map(projects().map((project) => [project.path, project.id]));
|
|
6367
6620
|
const folders = new Map(projects().map((project) => [project.id, project.path]));
|
|
6368
6621
|
const results = await Promise.all([
|
|
6369
6622
|
"claude",
|
|
@@ -6373,11 +6626,11 @@ async function searchMessages(query) {
|
|
|
6373
6626
|
try {
|
|
6374
6627
|
return {
|
|
6375
6628
|
provider,
|
|
6376
|
-
matches: await timeLimit(search(provider, query,
|
|
6629
|
+
matches: await timeLimit(search(provider, query, folders), `${providerName(provider)} did not search in time`),
|
|
6377
6630
|
failed: false
|
|
6378
6631
|
};
|
|
6379
6632
|
} catch (error) {
|
|
6380
|
-
|
|
6633
|
+
log$7.warn(`could not search ${providerName(provider)} messages:`, error);
|
|
6381
6634
|
return {
|
|
6382
6635
|
provider,
|
|
6383
6636
|
matches: [],
|
|
@@ -6390,10 +6643,8 @@ async function searchMessages(query) {
|
|
|
6390
6643
|
failed: results.filter((result) => result.failed).map((result) => result.provider)
|
|
6391
6644
|
};
|
|
6392
6645
|
}
|
|
6393
|
-
async function search(provider, query,
|
|
6394
|
-
const
|
|
6395
|
-
if (found === null) return [];
|
|
6396
|
-
const sessions = newestFirst(found);
|
|
6646
|
+
async function search(provider, query, folders) {
|
|
6647
|
+
const sessions = newestFirst(await providerThreads(provider));
|
|
6397
6648
|
switch (provider) {
|
|
6398
6649
|
case "claude": return searchClaude(query, sessions, folders);
|
|
6399
6650
|
case "codex": return searchCodex(query, sessions);
|
|
@@ -6427,7 +6678,7 @@ async function claudeCandidates(query) {
|
|
|
6427
6678
|
const lists = await Promise.all(words.map((word) => sessionsWith(word)));
|
|
6428
6679
|
return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
|
|
6429
6680
|
} catch (error) {
|
|
6430
|
-
|
|
6681
|
+
log$7.warn(`could not narrow Claude sessions with rg:`, error);
|
|
6431
6682
|
return null;
|
|
6432
6683
|
}
|
|
6433
6684
|
}
|
|
@@ -6989,6 +7240,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
6989
7240
|
return "Available for bash, zsh and fish";
|
|
6990
7241
|
}
|
|
6991
7242
|
// src/services/terminal/shell-integration.ts
|
|
7243
|
+
const log$6 = logger.withTag("terminal");
|
|
6992
7244
|
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
6993
7245
|
const folder = path.join(home, "shell-integration");
|
|
6994
7246
|
function prepare(env, command, shell) {
|
|
@@ -7031,7 +7283,7 @@ function zshGlobalsSupported() {
|
|
|
7031
7283
|
contents = readFileSync(global, "utf8");
|
|
7032
7284
|
} catch (error) {
|
|
7033
7285
|
if (error.code === "ENOENT") continue;
|
|
7034
|
-
|
|
7286
|
+
log$6.warn(`could not read ${global}, zsh starts without shell integration:`, error);
|
|
7035
7287
|
return false;
|
|
7036
7288
|
}
|
|
7037
7289
|
for (const rawLine of contents.split("\n")) {
|
|
@@ -7076,11 +7328,12 @@ function writeScript(target, source) {
|
|
|
7076
7328
|
renameSync(`${target}.tmp`, target);
|
|
7077
7329
|
return true;
|
|
7078
7330
|
} catch (error) {
|
|
7079
|
-
|
|
7331
|
+
log$6.warn(`could not write ${target}, the shell starts without shell integration:`, error);
|
|
7080
7332
|
return false;
|
|
7081
7333
|
}
|
|
7082
7334
|
}
|
|
7083
7335
|
// src/services/terminal/process.ts
|
|
7336
|
+
const log$5 = logger.withTag("terminal");
|
|
7084
7337
|
const run = promisify(execFile);
|
|
7085
7338
|
const iterm2Version = "3.6.8";
|
|
7086
7339
|
const ghosttyVersion = "1.3.1";
|
|
@@ -7176,7 +7429,7 @@ async function busyProgram(pid) {
|
|
|
7176
7429
|
try {
|
|
7177
7430
|
return await programName(group);
|
|
7178
7431
|
} catch (error) {
|
|
7179
|
-
|
|
7432
|
+
log$5.warn(`could not read the name of process ${group}:`, error);
|
|
7180
7433
|
return `process ${group}`;
|
|
7181
7434
|
}
|
|
7182
7435
|
}
|
|
@@ -7418,6 +7671,7 @@ var Alerts = class {
|
|
|
7418
7671
|
}
|
|
7419
7672
|
};
|
|
7420
7673
|
// src/services/terminal/terminal.ts
|
|
7674
|
+
const log$4 = logger.withTag("terminal");
|
|
7421
7675
|
const HISTORY_LIMIT = 2097152;
|
|
7422
7676
|
const OUTPUT_LIMIT = 1048576;
|
|
7423
7677
|
const STALL_LIMIT = 1e4;
|
|
@@ -7452,7 +7706,7 @@ async function terminalRequest(client, message) {
|
|
|
7452
7706
|
function terminalInput(client, frame) {
|
|
7453
7707
|
const connection = connections.get(client);
|
|
7454
7708
|
if (connection === void 0) {
|
|
7455
|
-
|
|
7709
|
+
log$4.warn("terminal input from a browser that is not attached");
|
|
7456
7710
|
return;
|
|
7457
7711
|
}
|
|
7458
7712
|
input(connection, frame);
|
|
@@ -7469,7 +7723,7 @@ async function busyTerminal(id) {
|
|
|
7469
7723
|
try {
|
|
7470
7724
|
return { program: await busyProgram(terminal.pty.pid) };
|
|
7471
7725
|
} catch (error) {
|
|
7472
|
-
|
|
7726
|
+
log$4.warn(`could not tell if terminal ${id} is busy:`, error);
|
|
7473
7727
|
return { error: String(error) };
|
|
7474
7728
|
}
|
|
7475
7729
|
}
|
|
@@ -7538,12 +7792,12 @@ function input(connection, bytes) {
|
|
|
7538
7792
|
try {
|
|
7539
7793
|
frame = decodeFrame(bytes);
|
|
7540
7794
|
} catch (error) {
|
|
7541
|
-
|
|
7795
|
+
log$4.warn(`ignoring terminal input:`, error);
|
|
7542
7796
|
return;
|
|
7543
7797
|
}
|
|
7544
7798
|
const terminal = terminals.get(frame.id);
|
|
7545
7799
|
if (terminal === void 0) {
|
|
7546
|
-
|
|
7800
|
+
log$4.warn(`input for terminal ${frame.id}, which does not exist`);
|
|
7547
7801
|
return;
|
|
7548
7802
|
}
|
|
7549
7803
|
if (frame.kind === 1) {
|
|
@@ -7555,7 +7809,7 @@ function input(connection, bytes) {
|
|
|
7555
7809
|
terminal.pty.resize(size.cols, size.rows);
|
|
7556
7810
|
}
|
|
7557
7811
|
} else {
|
|
7558
|
-
|
|
7812
|
+
log$4.warn(`ignoring terminal input of unknown kind ${frame.kind}`);
|
|
7559
7813
|
return;
|
|
7560
7814
|
}
|
|
7561
7815
|
terminal.pty.write(Buffer.from(frame.data));
|
|
@@ -7563,7 +7817,7 @@ function input(connection, bytes) {
|
|
|
7563
7817
|
async function createTab(projectId) {
|
|
7564
7818
|
const cwd = projectId === null ? workingDirectory() : projectPath(projectId);
|
|
7565
7819
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
7566
|
-
|
|
7820
|
+
log$4.error(`${cwd} is not a folder, terminal not created`);
|
|
7567
7821
|
return;
|
|
7568
7822
|
}
|
|
7569
7823
|
const activeId = active.get(projectId ?? "global");
|
|
@@ -7572,7 +7826,7 @@ async function createTab(projectId) {
|
|
|
7572
7826
|
if (current !== void 0) try {
|
|
7573
7827
|
directory = await currentDirectory(current.pty.pid, current.scanner.directory);
|
|
7574
7828
|
} catch (error) {
|
|
7575
|
-
|
|
7829
|
+
log$4.warn(`starting in the project folder, could not read the folder of the active terminal:`, error);
|
|
7576
7830
|
}
|
|
7577
7831
|
nextTab += 1;
|
|
7578
7832
|
const id = nextTab;
|
|
@@ -7580,7 +7834,7 @@ async function createTab(projectId) {
|
|
|
7580
7834
|
try {
|
|
7581
7835
|
pty = startShell(directory);
|
|
7582
7836
|
} catch (error) {
|
|
7583
|
-
|
|
7837
|
+
log$4.error(`could not start terminal ${id}:`, error);
|
|
7584
7838
|
return;
|
|
7585
7839
|
}
|
|
7586
7840
|
const terminal = {
|
|
@@ -7591,6 +7845,7 @@ async function createTab(projectId) {
|
|
|
7591
7845
|
alerts: new Alerts(),
|
|
7592
7846
|
scanner: new Scanner()
|
|
7593
7847
|
};
|
|
7848
|
+
log$4.info(`started terminal ${id} with pid ${pty.pid} in ${directory}`);
|
|
7594
7849
|
terminals.set(id, terminal);
|
|
7595
7850
|
tabs.push({
|
|
7596
7851
|
id,
|
|
@@ -7603,7 +7858,8 @@ async function createTab(projectId) {
|
|
|
7603
7858
|
pty.pause();
|
|
7604
7859
|
held.add(pty);
|
|
7605
7860
|
}
|
|
7606
|
-
pty.onExit(() => {
|
|
7861
|
+
pty.onExit((event) => {
|
|
7862
|
+
log$4.info(`terminal ${id} exited with code ${event.exitCode}`);
|
|
7607
7863
|
terminals.delete(id);
|
|
7608
7864
|
paused.delete(pty);
|
|
7609
7865
|
held.delete(pty);
|
|
@@ -7613,6 +7869,7 @@ async function createTab(projectId) {
|
|
|
7613
7869
|
function closeTab(id) {
|
|
7614
7870
|
const tab = findTab(id);
|
|
7615
7871
|
if (tab === void 0) return false;
|
|
7872
|
+
log$4.debug(`closing terminal ${id}`);
|
|
7616
7873
|
release(tab);
|
|
7617
7874
|
tabs.splice(tabs.indexOf(tab), 1);
|
|
7618
7875
|
const terminal = terminals.get(id);
|
|
@@ -7654,7 +7911,7 @@ function drain() {
|
|
|
7654
7911
|
last.queued = queued;
|
|
7655
7912
|
progress.set(connection, last);
|
|
7656
7913
|
if (now - last.at > STALL_LIMIT) {
|
|
7657
|
-
|
|
7914
|
+
log$4.warn("a browser is not reading fast enough, disconnecting it");
|
|
7658
7915
|
connection.client.socket.terminate();
|
|
7659
7916
|
continue;
|
|
7660
7917
|
}
|
|
@@ -7744,6 +8001,36 @@ function send(connection, event) {
|
|
|
7744
8001
|
event
|
|
7745
8002
|
});
|
|
7746
8003
|
}
|
|
8004
|
+
// src/storage/last-used.ts
|
|
8005
|
+
const lastUsedSchema = z.discriminatedUnion("provider", [z.strictObject({
|
|
8006
|
+
provider: z.enum(["claude", "codex"]),
|
|
8007
|
+
accountId: z.string(),
|
|
8008
|
+
model: z.string()
|
|
8009
|
+
}), z.strictObject({
|
|
8010
|
+
provider: z.literal("opencode"),
|
|
8011
|
+
accountId: z.null(),
|
|
8012
|
+
model: z.string()
|
|
8013
|
+
})]);
|
|
8014
|
+
function readLastUsed() {
|
|
8015
|
+
return db.select().from(lastUsedModels).all();
|
|
8016
|
+
}
|
|
8017
|
+
function saveLastUsed(value) {
|
|
8018
|
+
const result = lastUsedSchema.safeParse(value);
|
|
8019
|
+
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
8020
|
+
const row = {
|
|
8021
|
+
...result.data,
|
|
8022
|
+
usedAt: Date.now()
|
|
8023
|
+
};
|
|
8024
|
+
db.insert(lastUsedModels).values(row).onConflictDoUpdate({
|
|
8025
|
+
target: lastUsedModels.provider,
|
|
8026
|
+
set: row
|
|
8027
|
+
}).run();
|
|
8028
|
+
broadcast({
|
|
8029
|
+
type: "lastUsed",
|
|
8030
|
+
lastUsed: readLastUsed()
|
|
8031
|
+
});
|
|
8032
|
+
return null;
|
|
8033
|
+
}
|
|
7747
8034
|
// ../core/src/app-icons.ts
|
|
7748
8035
|
const appIcons = [
|
|
7749
8036
|
{
|
|
@@ -7814,77 +8101,14 @@ const appIcons = [
|
|
|
7814
8101
|
}
|
|
7815
8102
|
];
|
|
7816
8103
|
// src/storage/state.ts
|
|
7817
|
-
const stateSchema = z.strictObject({
|
|
7818
|
-
composer: z.strictObject({
|
|
7819
|
-
provider: z.enum([
|
|
7820
|
-
"claude",
|
|
7821
|
-
"codex",
|
|
7822
|
-
"opencode"
|
|
7823
|
-
]).default("claude"),
|
|
7824
|
-
claude: z.strictObject({
|
|
7825
|
-
accountId: z.string().nullable().default(null),
|
|
7826
|
-
model: z.string().nullable().default(null),
|
|
7827
|
-
effort: z.enum([
|
|
7828
|
-
"low",
|
|
7829
|
-
"medium",
|
|
7830
|
-
"high",
|
|
7831
|
-
"xhigh",
|
|
7832
|
-
"max"
|
|
7833
|
-
]).nullable().default(null),
|
|
7834
|
-
mode: z.enum([
|
|
7835
|
-
"default",
|
|
7836
|
-
"acceptEdits",
|
|
7837
|
-
"bypassPermissions",
|
|
7838
|
-
"plan",
|
|
7839
|
-
"dontAsk",
|
|
7840
|
-
"auto"
|
|
7841
|
-
]).nullable().default("bypassPermissions"),
|
|
7842
|
-
plan: z.boolean().default(false)
|
|
7843
|
-
}).prefault({}),
|
|
7844
|
-
codex: z.strictObject({
|
|
7845
|
-
accountId: z.string().nullable().default(null),
|
|
7846
|
-
model: z.string().nullable().default(null),
|
|
7847
|
-
effort: z.string().nullable().default(null),
|
|
7848
|
-
mode: z.enum([
|
|
7849
|
-
"read-only",
|
|
7850
|
-
"auto",
|
|
7851
|
-
"full-access"
|
|
7852
|
-
]).nullable().default("auto"),
|
|
7853
|
-
plan: z.boolean().default(false)
|
|
7854
|
-
}).prefault({}),
|
|
7855
|
-
opencode: z.strictObject({
|
|
7856
|
-
accountId: z.null().default(null),
|
|
7857
|
-
model: z.string().nullable().default(null),
|
|
7858
|
-
effort: z.string().nullable().default(null),
|
|
7859
|
-
mode: z.string().nullable().default(null),
|
|
7860
|
-
plan: z.boolean().default(false)
|
|
7861
|
-
}).prefault({})
|
|
7862
|
-
}).prefault({}),
|
|
7863
|
-
appIcon: z.custom((value) => appIcons.some((icon) => icon.id === value)).default("original")
|
|
7864
|
-
});
|
|
8104
|
+
const stateSchema = z.strictObject({ appIcon: z.custom((value) => appIcons.some((icon) => icon.id === value)).default("original") });
|
|
7865
8105
|
const defaults = stateSchema.parse({});
|
|
7866
8106
|
function readState() {
|
|
7867
8107
|
return readSettings("state.json", stateSchema, defaults);
|
|
7868
8108
|
}
|
|
7869
|
-
function readComposer() {
|
|
7870
|
-
return readState().composer;
|
|
7871
|
-
}
|
|
7872
8109
|
function readAppIcon() {
|
|
7873
8110
|
return readState().appIcon;
|
|
7874
8111
|
}
|
|
7875
|
-
function saveComposer(value) {
|
|
7876
|
-
const result = stateSchema.safeParse({
|
|
7877
|
-
...readState(),
|
|
7878
|
-
composer: value
|
|
7879
|
-
});
|
|
7880
|
-
if (!result.success) throw new Error(z.prettifyError(result.error));
|
|
7881
|
-
writeJson("state.json", result.data);
|
|
7882
|
-
broadcast({
|
|
7883
|
-
type: "composer",
|
|
7884
|
-
composer: result.data.composer
|
|
7885
|
-
});
|
|
7886
|
-
return result.data.composer;
|
|
7887
|
-
}
|
|
7888
8112
|
function saveAppIcon(value) {
|
|
7889
8113
|
const result = stateSchema.safeParse({
|
|
7890
8114
|
...readState(),
|
|
@@ -7899,13 +8123,14 @@ function saveAppIcon(value) {
|
|
|
7899
8123
|
return result.data.appIcon;
|
|
7900
8124
|
}
|
|
7901
8125
|
// src/api/socket.ts
|
|
8126
|
+
const log$3 = logger.withTag("socket");
|
|
7902
8127
|
const handlers = {
|
|
7903
8128
|
ping: () => null,
|
|
7904
8129
|
sync: () => {
|
|
7905
8130
|
refreshAll();
|
|
7906
8131
|
return {
|
|
7907
8132
|
...configInfo(),
|
|
7908
|
-
|
|
8133
|
+
lastUsed: readLastUsed(),
|
|
7909
8134
|
hostname: os.hostname(),
|
|
7910
8135
|
projects: projects(),
|
|
7911
8136
|
sessions: sessions(),
|
|
@@ -7916,8 +8141,7 @@ const handlers = {
|
|
|
7916
8141
|
};
|
|
7917
8142
|
},
|
|
7918
8143
|
"config.get": () => configInfo(),
|
|
7919
|
-
"
|
|
7920
|
-
"composer.save": (params) => saveComposer(params),
|
|
8144
|
+
"lastUsed.save": (params) => saveLastUsed(params),
|
|
7921
8145
|
"appIcon.save": (params) => saveAppIcon(params),
|
|
7922
8146
|
"config.save": async (params) => {
|
|
7923
8147
|
const before = readConfig();
|
|
@@ -7973,7 +8197,7 @@ const handlers = {
|
|
|
7973
8197
|
if (params.opened && session !== null) recordOpen(params.sessionId, params.query);
|
|
7974
8198
|
return session;
|
|
7975
8199
|
},
|
|
7976
|
-
"sessions.inactive": (params) => inactiveSessions(params
|
|
8200
|
+
"sessions.inactive": (params) => inactiveSessions(params),
|
|
7977
8201
|
"sessions.searchable": () => searchableSessions(),
|
|
7978
8202
|
"sessions.searchMessages": (params) => searchMessages(params.query),
|
|
7979
8203
|
"sessions.earlier": (params) => earlier(params.sessionId, params.before),
|
|
@@ -8014,7 +8238,7 @@ const handlers = {
|
|
|
8014
8238
|
const run = runs.get(params.sessionId);
|
|
8015
8239
|
if (run === void 0) return null;
|
|
8016
8240
|
await run.respond(params.requestId, params.decision, params.answers);
|
|
8017
|
-
recordInteraction(params.sessionId);
|
|
8241
|
+
recordInteraction(params.sessionId, run.provider);
|
|
8018
8242
|
return null;
|
|
8019
8243
|
},
|
|
8020
8244
|
"sessions.rewind": (params) => rewind(params),
|
|
@@ -8128,6 +8352,7 @@ function connectClient(socket, device, address) {
|
|
|
8128
8352
|
send: (message) => socket.send(JSON.stringify(message))
|
|
8129
8353
|
};
|
|
8130
8354
|
clients.add(client);
|
|
8355
|
+
log$3.info(`a browser connected from ${address}`);
|
|
8131
8356
|
socket.on("message", (data, isBinary) => {
|
|
8132
8357
|
client.heardAt = Date.now();
|
|
8133
8358
|
if (isBinary) {
|
|
@@ -8136,8 +8361,9 @@ function connectClient(socket, device, address) {
|
|
|
8136
8361
|
}
|
|
8137
8362
|
answer(client, String(data));
|
|
8138
8363
|
});
|
|
8139
|
-
socket.on("error", (error) =>
|
|
8140
|
-
socket.on("close", () => {
|
|
8364
|
+
socket.on("error", (error) => log$3.warn(`a browser connection from ${address} failed:`, error));
|
|
8365
|
+
socket.on("close", (code) => {
|
|
8366
|
+
log$3.info(`a browser from ${address} disconnected (code ${code})`);
|
|
8141
8367
|
clients.delete(client);
|
|
8142
8368
|
detachTerminal(client);
|
|
8143
8369
|
});
|
|
@@ -8147,9 +8373,10 @@ async function answer(client, text) {
|
|
|
8147
8373
|
try {
|
|
8148
8374
|
call = JSON.parse(text);
|
|
8149
8375
|
} catch (error) {
|
|
8150
|
-
|
|
8376
|
+
log$3.warn(`ignoring a message from ${client.address} that is not JSON:`, error);
|
|
8151
8377
|
return;
|
|
8152
8378
|
}
|
|
8379
|
+
const started = performance.now();
|
|
8153
8380
|
try {
|
|
8154
8381
|
if (!Object.hasOwn(handlers, call.method)) throw new Error(`unknown method ${call.method}`);
|
|
8155
8382
|
const handler = handlers[call.method];
|
|
@@ -8160,11 +8387,15 @@ async function answer(client, text) {
|
|
|
8160
8387
|
result
|
|
8161
8388
|
});
|
|
8162
8389
|
} catch (error) {
|
|
8390
|
+
log$3.warn(`${call.method} from ${client.address} failed:`, error);
|
|
8163
8391
|
client.send({
|
|
8164
8392
|
type: "reply",
|
|
8165
8393
|
id: call.id,
|
|
8166
8394
|
error: error instanceof Error ? error.message : String(error)
|
|
8167
8395
|
});
|
|
8396
|
+
} finally {
|
|
8397
|
+
const took = Math.round(performance.now() - started);
|
|
8398
|
+
if (call.method !== "ping") log$3.debug(`${call.method} from ${client.address} took ${took}ms`);
|
|
8168
8399
|
}
|
|
8169
8400
|
}
|
|
8170
8401
|
// src/lib/proxy/css.ts
|
|
@@ -9337,6 +9568,7 @@ function fail(response, status, message) {
|
|
|
9337
9568
|
response.end(message);
|
|
9338
9569
|
}
|
|
9339
9570
|
// src/services/app-icon.ts
|
|
9571
|
+
const log$2 = logger.withTag("app-icon");
|
|
9340
9572
|
const pngs = /* @__PURE__ */ new Map([
|
|
9341
9573
|
["icon-192.png", {
|
|
9342
9574
|
size: 192,
|
|
@@ -9369,7 +9601,7 @@ async function serveAppIcon(publicDir, pathname, response) {
|
|
|
9369
9601
|
response.setHeader("cache-control", "no-cache");
|
|
9370
9602
|
response.end(icon.body);
|
|
9371
9603
|
} catch (error) {
|
|
9372
|
-
|
|
9604
|
+
log$2.warn(`could not serve the app icon:`, error);
|
|
9373
9605
|
response.statusCode = 500;
|
|
9374
9606
|
response.end("could not render the app icon");
|
|
9375
9607
|
}
|
|
@@ -9401,10 +9633,83 @@ function tile(icon, maskable) {
|
|
|
9401
9633
|
if (icon.tile.shape === "circle") return `<circle cx="256" cy="256" r="256" fill="${icon.tile.color}"/>`;
|
|
9402
9634
|
throw new Error(`unknown tile shape for ${icon.id}`);
|
|
9403
9635
|
}
|
|
9636
|
+
// src/services/backfill-session-providers.ts
|
|
9637
|
+
const log$1 = logger.withTag("backfill");
|
|
9638
|
+
const allProviders = [
|
|
9639
|
+
"claude",
|
|
9640
|
+
"codex",
|
|
9641
|
+
"opencode"
|
|
9642
|
+
];
|
|
9643
|
+
async function backfillSessionProviders() {
|
|
9644
|
+
const rows = db.select().from(sessions$1).where(isNull(sessions$1.provider)).all();
|
|
9645
|
+
if (rows.length === 0) return;
|
|
9646
|
+
const paths = new Map(projects().map((project) => [project.path, project.id]));
|
|
9647
|
+
const enabled = allProviders.filter((provider) => readConfig().enabledProviders[provider]);
|
|
9648
|
+
for (const row of rows) {
|
|
9649
|
+
const answers = await Promise.all(enabled.map(async (provider) => {
|
|
9650
|
+
try {
|
|
9651
|
+
return await timeLimit(info(provider, row.sessionId, paths), `${providerName(provider)} did not answer in time`) !== null;
|
|
9652
|
+
} catch (error) {
|
|
9653
|
+
log$1.debug(`${providerName(provider)} does not know session ${row.sessionId}:`, error);
|
|
9654
|
+
return false;
|
|
9655
|
+
}
|
|
9656
|
+
}));
|
|
9657
|
+
const owners = enabled.filter((_, index) => answers[index]);
|
|
9658
|
+
if (owners.length !== 1) {
|
|
9659
|
+
log$1.warn(`${owners.length} providers know session ${row.sessionId}, it stays without a provider until the next start`);
|
|
9660
|
+
continue;
|
|
9661
|
+
}
|
|
9662
|
+
db.update(sessions$1).set({ provider: owners[0] }).where(eq(sessions$1.sessionId, row.sessionId)).run();
|
|
9663
|
+
log$1.info(`saved ${providerName(owners[0])} as the provider of session ${row.sessionId}`);
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
// src/services/browser-logs.ts
|
|
9667
|
+
const logsBody = z.array(z.object({
|
|
9668
|
+
time: z.number(),
|
|
9669
|
+
type: z.enum([
|
|
9670
|
+
"fatal",
|
|
9671
|
+
"error",
|
|
9672
|
+
"warn",
|
|
9673
|
+
"log",
|
|
9674
|
+
"info",
|
|
9675
|
+
"success",
|
|
9676
|
+
"fail",
|
|
9677
|
+
"ready",
|
|
9678
|
+
"start",
|
|
9679
|
+
"box",
|
|
9680
|
+
"debug",
|
|
9681
|
+
"trace",
|
|
9682
|
+
"verbose"
|
|
9683
|
+
]),
|
|
9684
|
+
tag: z.string(),
|
|
9685
|
+
message: z.string()
|
|
9686
|
+
}));
|
|
9687
|
+
async function saveBrowserLogs(device, request, response) {
|
|
9688
|
+
let body = "";
|
|
9689
|
+
for await (const chunk of request) body += chunk;
|
|
9690
|
+
let entries;
|
|
9691
|
+
try {
|
|
9692
|
+
entries = logsBody.parse(JSON.parse(body));
|
|
9693
|
+
} catch {
|
|
9694
|
+
response.statusCode = 400;
|
|
9695
|
+
response.end("expected [{ time, type, tag, message }]");
|
|
9696
|
+
return;
|
|
9697
|
+
}
|
|
9698
|
+
const source = device === null ? request.socket.remoteAddress ?? "unknown address" : readDevices().find((item) => item.hash === device)?.name ?? "Unnamed";
|
|
9699
|
+
for (const entry of entries) browserLogger.log({
|
|
9700
|
+
date: new Date(entry.time),
|
|
9701
|
+
type: entry.type,
|
|
9702
|
+
tag: entry.tag,
|
|
9703
|
+
args: [`${source}: ${entry.message}`]
|
|
9704
|
+
});
|
|
9705
|
+
response.statusCode = 204;
|
|
9706
|
+
response.end();
|
|
9707
|
+
}
|
|
9404
9708
|
// src/main.ts
|
|
9405
9709
|
const UNPAIR_CHECK = 5e3;
|
|
9406
9710
|
const TICKET_LIFETIME = 6e4;
|
|
9407
9711
|
const openBody = z.object({ path: z.string() });
|
|
9712
|
+
const log = logger.withTag("server");
|
|
9408
9713
|
const contentTypes = {
|
|
9409
9714
|
".html": "text/html; charset=utf-8",
|
|
9410
9715
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -9440,6 +9745,7 @@ const browseTypes = new Map([
|
|
|
9440
9745
|
const port = Number(listenPort());
|
|
9441
9746
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
9442
9747
|
console.error(`invalid port ${args.port}`);
|
|
9748
|
+
log.error(`invalid port ${args.port}`);
|
|
9443
9749
|
process.exit(1);
|
|
9444
9750
|
}
|
|
9445
9751
|
const address = `http://${args.host}:${port}`;
|
|
@@ -9454,12 +9760,15 @@ if (args.command === "unpair") {
|
|
|
9454
9760
|
process.exit(0);
|
|
9455
9761
|
}
|
|
9456
9762
|
lockHome();
|
|
9763
|
+
log.info(`starting offsite ${args.command} with pid ${process.pid} on ${address}, dir ${args.dir}, auth ${args.auth ? "on" : "off"}`);
|
|
9457
9764
|
process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
|
|
9458
9765
|
installOpen(args.host, port);
|
|
9766
|
+
await backfillSessionProviders();
|
|
9459
9767
|
watchSessions();
|
|
9460
9768
|
refreshAll();
|
|
9461
9769
|
if (findClaude() === null) {
|
|
9462
9770
|
console.error("could not find claude on PATH; install Claude Code first");
|
|
9771
|
+
log.error("could not find claude on PATH; install Claude Code first");
|
|
9463
9772
|
process.exit(1);
|
|
9464
9773
|
}
|
|
9465
9774
|
const publicDir = path.join(import.meta.dirname, "..", "..", "client", "dist");
|
|
@@ -9469,7 +9778,7 @@ const server = createServer((request, response) => {
|
|
|
9469
9778
|
try {
|
|
9470
9779
|
answerRequest(request, response);
|
|
9471
9780
|
} catch (error) {
|
|
9472
|
-
|
|
9781
|
+
log.warn("a request failed:", error);
|
|
9473
9782
|
if (response.headersSent) {
|
|
9474
9783
|
response.destroy();
|
|
9475
9784
|
return;
|
|
@@ -9559,11 +9868,16 @@ server.listen(port, args.host, () => {
|
|
|
9559
9868
|
console.log(`offsite is running at ${address}`);
|
|
9560
9869
|
if (args.auth && args.pair) console.log("Run offsite pair to pair another browser");
|
|
9561
9870
|
}
|
|
9562
|
-
|
|
9871
|
+
log.info(`listening on ${address}`);
|
|
9872
|
+
if (!args.auth) {
|
|
9873
|
+
console.warn("auth is off; anyone who can reach this port can control this machine");
|
|
9874
|
+
log.warn("auth is off; anyone who can reach this port can control this machine");
|
|
9875
|
+
}
|
|
9563
9876
|
});
|
|
9564
9877
|
process.on("SIGINT", stop);
|
|
9565
9878
|
process.on("SIGTERM", stop);
|
|
9566
|
-
function stop() {
|
|
9879
|
+
function stop(signal) {
|
|
9880
|
+
log.info(`stopping on ${signal}`);
|
|
9567
9881
|
stopAll();
|
|
9568
9882
|
process.exit(0);
|
|
9569
9883
|
}
|
|
@@ -9592,6 +9906,16 @@ function answerApi(pathname, request, response) {
|
|
|
9592
9906
|
answerJson(response, { token });
|
|
9593
9907
|
return;
|
|
9594
9908
|
}
|
|
9909
|
+
if (pathname === "/api/logs") {
|
|
9910
|
+
const device = given === null ? null : findDevice(given);
|
|
9911
|
+
if (args.auth && device === null) {
|
|
9912
|
+
response.statusCode = 401;
|
|
9913
|
+
response.end("This browser needs pairing");
|
|
9914
|
+
return;
|
|
9915
|
+
}
|
|
9916
|
+
saveBrowserLogs(device, request, response);
|
|
9917
|
+
return;
|
|
9918
|
+
}
|
|
9595
9919
|
if (pathname === "/api/ticket") {
|
|
9596
9920
|
if (!args.auth) {
|
|
9597
9921
|
answerJson(response, { ticket: null });
|