offsite 0.0.85 → 0.0.86
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-C_Axuxgf.js +67 -0
- package/apps/client/dist/assets/{diff-panel-DHx8Y0XH.js → diff-panel-C7lMMpep.js} +1 -1
- package/apps/client/dist/assets/{editor-dialog-mkPUdzR1.js → editor-dialog-BYriXnE3.js} +1 -1
- package/apps/client/dist/assets/{image-viewer-CplYkb8K.js → image-viewer-CRV8AWKt.js} +1 -1
- package/apps/client/dist/assets/index-DGntznWS.js +72 -0
- package/apps/client/dist/assets/{preview-dialog-DKOvcSoH.js → preview-dialog-9aPCvV36.js} +1 -1
- package/apps/client/dist/assets/use-editor-shortcut-CXHRRkVK.js +39 -0
- package/apps/client/dist/assets/{video-viewer-DBy-bf6x.js → video-viewer-Be4G_Ych.js} +1 -1
- package/apps/client/dist/index.html +4 -4
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +262 -93
- package/apps/server/dist/main.mjs.map +1 -1
- 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,16 +1,17 @@
|
|
|
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";
|
|
@@ -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$18 = 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$18.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$18.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$18.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$18.debug(`fetching limits for ${account.name} (${account.provider})`);
|
|
137
210
|
try {
|
|
211
|
+
const limits = await providerLimits(account);
|
|
212
|
+
log$18.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$18.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$17 = 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$17.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$17.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$17.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
|
|
408
451
|
return structuredClone(defaults);
|
|
409
452
|
}
|
|
410
|
-
|
|
453
|
+
log$17.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$16 = 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$16.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$16.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$16.debug(`${method} took ${Math.round(performance.now() - started)}ms`);
|
|
602
|
+
return result;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
log$16.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$16.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$16.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$16.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$15 = logger.withTag("codex");
|
|
669
726
|
const privateEntries = [
|
|
670
727
|
"auth.json",
|
|
671
728
|
"models_cache.json",
|
|
@@ -773,7 +830,7 @@ function codexDisplayName(home) {
|
|
|
773
830
|
try {
|
|
774
831
|
raw = JSON.parse(readFileSync(path.join(home, "auth.json"), "utf8"));
|
|
775
832
|
} catch (error) {
|
|
776
|
-
if (error.code !== "ENOENT")
|
|
833
|
+
if (error.code !== "ENOENT") log$15.warn(`could not read the Codex account name:`, error);
|
|
777
834
|
return null;
|
|
778
835
|
}
|
|
779
836
|
if (!isRecord(raw) || !isRecord(raw.tokens) || typeof raw.tokens.id_token !== "string") return null;
|
|
@@ -1153,8 +1210,10 @@ function saveConfig(value) {
|
|
|
1153
1210
|
return result.data;
|
|
1154
1211
|
}
|
|
1155
1212
|
// src/lib/live.ts
|
|
1213
|
+
const log$14 = logger.withTag("runs");
|
|
1156
1214
|
const runs = /* @__PURE__ */ new Map();
|
|
1157
1215
|
const titleJobs = /* @__PURE__ */ new Set();
|
|
1216
|
+
let states = /* @__PURE__ */ new Map();
|
|
1158
1217
|
function activity() {
|
|
1159
1218
|
const result = {};
|
|
1160
1219
|
for (const run of runs.values()) result[run.sessionId] = {
|
|
@@ -1170,6 +1229,19 @@ function activity() {
|
|
|
1170
1229
|
return result;
|
|
1171
1230
|
}
|
|
1172
1231
|
function broadcastActivity() {
|
|
1232
|
+
const next = /* @__PURE__ */ new Map();
|
|
1233
|
+
for (const run of runs.values()) {
|
|
1234
|
+
const state = [
|
|
1235
|
+
run.running ? "running" : "idle",
|
|
1236
|
+
...run.pending.size > 0 ? ["waiting for an answer"] : [],
|
|
1237
|
+
...run.compacting ? ["compacting"] : []
|
|
1238
|
+
].join(", ");
|
|
1239
|
+
next.set(run.sessionId, state);
|
|
1240
|
+
if (!states.has(run.sessionId)) log$14.info(`${run.provider} session ${run.sessionId} opened in ${run.cwd} with account ${run.settings.accountId}`);
|
|
1241
|
+
if (states.get(run.sessionId) !== state) log$14.debug(`session ${run.sessionId} is ${state}`);
|
|
1242
|
+
}
|
|
1243
|
+
for (const sessionId of states.keys()) if (!next.has(sessionId)) log$14.info(`session ${sessionId} closed`);
|
|
1244
|
+
states = next;
|
|
1173
1245
|
broadcast({
|
|
1174
1246
|
type: "activity",
|
|
1175
1247
|
activity: activity()
|
|
@@ -1187,7 +1259,7 @@ function scheduleAutoResolve(sessionId, request) {
|
|
|
1187
1259
|
try {
|
|
1188
1260
|
await run.respond(request.id, decision, request.kind === "question" ? {} : null);
|
|
1189
1261
|
} catch (error) {
|
|
1190
|
-
|
|
1262
|
+
log$14.warn(`could not auto-resolve ${request.id}:`, error);
|
|
1191
1263
|
}
|
|
1192
1264
|
}, seconds * 1e3);
|
|
1193
1265
|
return {
|
|
@@ -1328,6 +1400,7 @@ function reply(method, params, decision, answers) {
|
|
|
1328
1400
|
throw new Error(`offsite cannot answer ${method}`);
|
|
1329
1401
|
}
|
|
1330
1402
|
// src/lib/codex/run.ts
|
|
1403
|
+
const log$13 = logger.withTag("codex");
|
|
1331
1404
|
const IDLE_LIMIT$2 = 18e5;
|
|
1332
1405
|
function codexServer(settings) {
|
|
1333
1406
|
return appServer(storedAccount("codex", settings.accountId).configDir ?? codexBaseDir);
|
|
@@ -1452,9 +1525,10 @@ var CodexRun = class CodexRun {
|
|
|
1452
1525
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1453
1526
|
this.server.threads.delete(this.sessionId);
|
|
1454
1527
|
clearTimeout(this.idleTimer);
|
|
1455
|
-
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) =>
|
|
1528
|
+
this.server.request("thread/unsubscribe", { threadId: this.sessionId }).catch((error) => log$13.warn(`could not unload Codex thread:`, error)).finally(() => this.end());
|
|
1456
1529
|
}
|
|
1457
1530
|
exited() {
|
|
1531
|
+
log$13.warn(`session ${this.sessionId} lost its Codex app server`);
|
|
1458
1532
|
this.put(errorItem(randomUUID(), this.pending.size === 0 ? "Codex stopped" : "Codex stopped, its open requests expired"));
|
|
1459
1533
|
if (runs.get(this.sessionId) === this) runs.delete(this.sessionId);
|
|
1460
1534
|
this.end();
|
|
@@ -1658,6 +1732,7 @@ var CodexRun = class CodexRun {
|
|
|
1658
1732
|
});
|
|
1659
1733
|
}
|
|
1660
1734
|
fail(error) {
|
|
1735
|
+
log$13.warn(`session ${this.sessionId} failed:`, error);
|
|
1661
1736
|
this.put(errorItem(randomUUID(), error instanceof Error ? error.message : String(error)));
|
|
1662
1737
|
}
|
|
1663
1738
|
armIdleTimer() {
|
|
@@ -2524,6 +2599,7 @@ function sessionSettings(session, settings) {
|
|
|
2524
2599
|
};
|
|
2525
2600
|
}
|
|
2526
2601
|
// src/lib/opencode/run.ts
|
|
2602
|
+
const log$12 = logger.withTag("opencode");
|
|
2527
2603
|
const IDLE_LIMIT$1 = 18e5;
|
|
2528
2604
|
const RECENT = 30;
|
|
2529
2605
|
const replies = {
|
|
@@ -2651,6 +2727,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2651
2727
|
this.end();
|
|
2652
2728
|
}
|
|
2653
2729
|
exited() {
|
|
2730
|
+
log$12.warn(`session ${this.sessionId} lost the connection to OpenCode`);
|
|
2654
2731
|
this.put([errorItem(randomUUID(), this.pending.size === 0 ? "Lost the connection to OpenCode" : "Lost the connection to OpenCode, its open requests expired")]);
|
|
2655
2732
|
this.stop();
|
|
2656
2733
|
}
|
|
@@ -2718,7 +2795,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2718
2795
|
}
|
|
2719
2796
|
refresh(client) {
|
|
2720
2797
|
clearTimeout(this.refreshTimer);
|
|
2721
|
-
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) =>
|
|
2798
|
+
this.loading = this.loading.then(async () => this.load(client ?? await openCode())).catch((error) => log$12.warn(`could not load OpenCode messages:`, error));
|
|
2722
2799
|
}
|
|
2723
2800
|
async load(client) {
|
|
2724
2801
|
const messages = (await client.message.list({
|
|
@@ -2835,6 +2912,7 @@ var OpenCodeRun = class OpenCodeRun {
|
|
|
2835
2912
|
});
|
|
2836
2913
|
}
|
|
2837
2914
|
fail(error) {
|
|
2915
|
+
log$12.warn(`session ${this.sessionId} failed:`, error);
|
|
2838
2916
|
this.put([errorItem(randomUUID(), error instanceof Error ? error.message : String(error))]);
|
|
2839
2917
|
}
|
|
2840
2918
|
armIdleTimer() {
|
|
@@ -3970,6 +4048,7 @@ async function find(provider, paths) {
|
|
|
3970
4048
|
}
|
|
3971
4049
|
}
|
|
3972
4050
|
// src/services/sessions.ts
|
|
4051
|
+
const log$11 = logger.withTag("sessions");
|
|
3973
4052
|
const day = 864e5;
|
|
3974
4053
|
const REFRESH_DELAY = 1e3;
|
|
3975
4054
|
const allProviders = [
|
|
@@ -4020,7 +4099,7 @@ async function searchableSessions() {
|
|
|
4020
4099
|
failed: false
|
|
4021
4100
|
};
|
|
4022
4101
|
} catch (error) {
|
|
4023
|
-
|
|
4102
|
+
log$11.warn(`could not search ${providerName(provider)}:`, error);
|
|
4024
4103
|
return {
|
|
4025
4104
|
provider,
|
|
4026
4105
|
items: [],
|
|
@@ -4178,7 +4257,7 @@ async function list(provider) {
|
|
|
4178
4257
|
}
|
|
4179
4258
|
} catch (error) {
|
|
4180
4259
|
const message = error instanceof Error ? error.message : String(error);
|
|
4181
|
-
|
|
4260
|
+
log$11.warn(`could not list ${providerName(provider)} sessions: ${message}`);
|
|
4182
4261
|
statuses[provider] = {
|
|
4183
4262
|
provider,
|
|
4184
4263
|
state: "error",
|
|
@@ -4213,10 +4292,12 @@ function watchSessions() {
|
|
|
4213
4292
|
if (!readConfig().enabledProviders[provider]) return [];
|
|
4214
4293
|
try {
|
|
4215
4294
|
const watcher = watch(dir, { recursive: true }, () => refreshSessions(provider));
|
|
4216
|
-
watcher.on("error", (error) =>
|
|
4295
|
+
watcher.on("error", (error) => log$11.warn(`stopped watching ${dir}:`, error));
|
|
4296
|
+
log$11.debug(`watching ${dir}`);
|
|
4217
4297
|
return [watcher];
|
|
4218
4298
|
} catch (error) {
|
|
4219
4299
|
if (error.code !== "ENOENT") throw error;
|
|
4300
|
+
log$11.debug(`not watching ${dir}, it does not exist`);
|
|
4220
4301
|
return [];
|
|
4221
4302
|
}
|
|
4222
4303
|
});
|
|
@@ -4285,7 +4366,7 @@ async function removeSession(id, projectId) {
|
|
|
4285
4366
|
try {
|
|
4286
4367
|
if ((await everything([[project.path, project.id]])).filter((item) => item.id !== id).length === 0) removeProject(project.id);
|
|
4287
4368
|
} catch (error) {
|
|
4288
|
-
|
|
4369
|
+
log$11.warn(`could not check other chats in ${project.path}:`, error);
|
|
4289
4370
|
}
|
|
4290
4371
|
}
|
|
4291
4372
|
async function fork(sessionId, projectId, messageId) {
|
|
@@ -4306,6 +4387,7 @@ async function forkWith(provider, sessionId, cwd, messageId) {
|
|
|
4306
4387
|
})).sessionId };
|
|
4307
4388
|
}
|
|
4308
4389
|
// src/lib/opencode/client.ts
|
|
4390
|
+
const log$10 = logger.withTag("opencode");
|
|
4309
4391
|
const listChanges = /* @__PURE__ */ new Set([
|
|
4310
4392
|
"session.created",
|
|
4311
4393
|
"session.renamed",
|
|
@@ -4351,6 +4433,7 @@ async function start() {
|
|
|
4351
4433
|
}
|
|
4352
4434
|
async function connect() {
|
|
4353
4435
|
const endpoint = await ensure();
|
|
4436
|
+
log$10.info(`connected to OpenCode at ${endpoint.url}`);
|
|
4354
4437
|
return OpenCode.make({
|
|
4355
4438
|
baseUrl: endpoint.url,
|
|
4356
4439
|
headers: headers(endpoint)
|
|
@@ -4365,10 +4448,11 @@ async function listen(client) {
|
|
|
4365
4448
|
signal: controller.signal,
|
|
4366
4449
|
onActivity: () => silence.refresh()
|
|
4367
4450
|
})) route(event);
|
|
4368
|
-
|
|
4451
|
+
log$10.warn("the OpenCode event stream ended");
|
|
4369
4452
|
} catch (error) {
|
|
4370
4453
|
if (listener !== controller) return;
|
|
4371
|
-
|
|
4454
|
+
if (controller.signal.aborted) log$10.warn(`the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds`);
|
|
4455
|
+
else log$10.warn("lost the OpenCode event stream:", error);
|
|
4372
4456
|
} finally {
|
|
4373
4457
|
clearTimeout(silence);
|
|
4374
4458
|
}
|
|
@@ -4384,14 +4468,15 @@ async function reconnect() {
|
|
|
4384
4468
|
try {
|
|
4385
4469
|
client = await connect();
|
|
4386
4470
|
} catch (error) {
|
|
4471
|
+
log$10.debug(`reconnect attempt ${attempt + 1} to OpenCode failed:`, error);
|
|
4387
4472
|
failure = error;
|
|
4388
4473
|
continue;
|
|
4389
4474
|
}
|
|
4390
4475
|
listen(client);
|
|
4391
|
-
await catchUp(client).catch((error) =>
|
|
4476
|
+
await catchUp(client).catch((error) => log$10.warn(`could not catch up with OpenCode:`, error));
|
|
4392
4477
|
return client;
|
|
4393
4478
|
}
|
|
4394
|
-
|
|
4479
|
+
log$10.warn(`could not reconnect to OpenCode:`, failure);
|
|
4395
4480
|
connection = void 0;
|
|
4396
4481
|
for (const run of openCodeRuns()) run.exited();
|
|
4397
4482
|
refreshSessions("opencode");
|
|
@@ -4422,6 +4507,7 @@ function openCodeRuns() {
|
|
|
4422
4507
|
return [...runs.values()].flatMap((run) => run.provider === "opencode" ? [run] : []);
|
|
4423
4508
|
}
|
|
4424
4509
|
// src/services/accounts.ts
|
|
4510
|
+
const log$9 = logger.withTag("accounts");
|
|
4425
4511
|
const providerNames = {
|
|
4426
4512
|
claude: "Claude",
|
|
4427
4513
|
codex: "Codex",
|
|
@@ -4502,7 +4588,7 @@ async function publishAccounts() {
|
|
|
4502
4588
|
...await accounts()
|
|
4503
4589
|
});
|
|
4504
4590
|
} catch (error) {
|
|
4505
|
-
|
|
4591
|
+
log$9.warn(`could not list accounts:`, error);
|
|
4506
4592
|
}
|
|
4507
4593
|
}
|
|
4508
4594
|
async function capabilities(provider, accountId, cwd) {
|
|
@@ -4609,6 +4695,7 @@ function validAccount(name, configDir) {
|
|
|
4609
4695
|
throw new Error("Use an absolute config directory or one starting with ~/");
|
|
4610
4696
|
}
|
|
4611
4697
|
// src/services/push.ts
|
|
4698
|
+
const log$8 = logger.withTag("push");
|
|
4612
4699
|
const TITLE_LIMIT = 100;
|
|
4613
4700
|
const BODY_LIMIT = 500;
|
|
4614
4701
|
const TIME_TO_LIVE = 900;
|
|
@@ -4656,12 +4743,12 @@ function subscribe(registration, client) {
|
|
|
4656
4743
|
set: values
|
|
4657
4744
|
}).run();
|
|
4658
4745
|
if (notifier !== null && notifier.client === client) notifier.endpoint = registration.subscription.endpoint;
|
|
4659
|
-
|
|
4746
|
+
log$8.info("a browser subscribed to push notifications");
|
|
4660
4747
|
}
|
|
4661
4748
|
function unsubscribe(endpoint) {
|
|
4662
4749
|
db.delete(pushSubscriptions).where(eq(pushSubscriptions.endpoint, endpoint)).run();
|
|
4663
4750
|
if (notifier !== null && notifier.endpoint === endpoint) notifier.endpoint = null;
|
|
4664
|
-
|
|
4751
|
+
log$8.info("a browser unsubscribed from push notifications");
|
|
4665
4752
|
}
|
|
4666
4753
|
async function sendPush(message) {
|
|
4667
4754
|
const subscriptions = targets();
|
|
@@ -4672,7 +4759,7 @@ async function sendPush(message) {
|
|
|
4672
4759
|
body: [...message.body].slice(0, BODY_LIMIT).join("")
|
|
4673
4760
|
};
|
|
4674
4761
|
const gone = [];
|
|
4675
|
-
let
|
|
4762
|
+
let delivered = 0;
|
|
4676
4763
|
await Promise.all(subscriptions.map(async (subscription) => {
|
|
4677
4764
|
try {
|
|
4678
4765
|
await webPush.sendNotification({
|
|
@@ -4694,7 +4781,7 @@ async function sendPush(message) {
|
|
|
4694
4781
|
urgency: "high",
|
|
4695
4782
|
timeout: SEND_TIMEOUT
|
|
4696
4783
|
});
|
|
4697
|
-
|
|
4784
|
+
delivered += 1;
|
|
4698
4785
|
} catch (error) {
|
|
4699
4786
|
const status = error.statusCode;
|
|
4700
4787
|
if (status !== void 0 && [
|
|
@@ -4703,22 +4790,24 @@ async function sendPush(message) {
|
|
|
4703
4790
|
404,
|
|
4704
4791
|
410
|
|
4705
4792
|
].includes(status)) {
|
|
4706
|
-
|
|
4793
|
+
log$8.info(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
|
|
4707
4794
|
gone.push(subscription.endpoint);
|
|
4708
4795
|
return;
|
|
4709
4796
|
}
|
|
4710
|
-
|
|
4797
|
+
log$8.warn(`could not send a push to ${subscription.endpoint}:`, error);
|
|
4711
4798
|
}
|
|
4712
4799
|
}));
|
|
4713
|
-
|
|
4800
|
+
log$8.debug(`sent a push to ${delivered} of ${subscriptions.length} browsers`);
|
|
4801
|
+
if (gone.length === 0) return delivered > 0;
|
|
4714
4802
|
db.delete(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), inArray(pushSubscriptions.endpoint, gone))).run();
|
|
4715
|
-
return
|
|
4803
|
+
return delivered > 0;
|
|
4716
4804
|
}
|
|
4717
4805
|
function targets() {
|
|
4718
4806
|
if (notifier === null || notifier.endpoint === null) return [];
|
|
4719
4807
|
return db.select().from(pushSubscriptions).where(and(eq(pushSubscriptions.server, server$1), eq(pushSubscriptions.endpoint, notifier.endpoint))).all();
|
|
4720
4808
|
}
|
|
4721
4809
|
// src/lib/claude/claude.ts
|
|
4810
|
+
const log$7 = logger.withTag("claude");
|
|
4722
4811
|
const efforts = [
|
|
4723
4812
|
"low",
|
|
4724
4813
|
"medium",
|
|
@@ -4954,7 +5043,7 @@ function claudeDisplayName(configDir) {
|
|
|
4954
5043
|
try {
|
|
4955
5044
|
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
4956
5045
|
} catch (error) {
|
|
4957
|
-
if (error.code !== "ENOENT")
|
|
5046
|
+
if (error.code !== "ENOENT") log$7.warn(`could not read the Claude account name:`, error);
|
|
4958
5047
|
return null;
|
|
4959
5048
|
}
|
|
4960
5049
|
if (!isRecord(raw) || !isRecord(raw.oauthAccount) || typeof raw.oauthAccount.displayName !== "string") return null;
|
|
@@ -5735,7 +5824,10 @@ var Run = class {
|
|
|
5735
5824
|
try {
|
|
5736
5825
|
for await (const message of this.query) this.receive(message);
|
|
5737
5826
|
} catch (error) {
|
|
5738
|
-
if (!this.stopping)
|
|
5827
|
+
if (!this.stopping) {
|
|
5828
|
+
logger.withTag("claude").warn(`${this.sessionId} failed:`, error);
|
|
5829
|
+
this.add(notice(error instanceof Error ? error.message : String(error)));
|
|
5830
|
+
}
|
|
5739
5831
|
}
|
|
5740
5832
|
clearTimeout(this.idleTimer);
|
|
5741
5833
|
for (const id of this.pending.keys()) this.settle(id, {
|
|
@@ -6353,6 +6445,7 @@ function fenced(language, text) {
|
|
|
6353
6445
|
return `${fence}${language}\n${text}\n${fence}`;
|
|
6354
6446
|
}
|
|
6355
6447
|
// src/services/message-search.ts
|
|
6448
|
+
const log$6 = logger.withTag("search");
|
|
6356
6449
|
const READ_LIMIT = 30;
|
|
6357
6450
|
const CODEX_READ_LIMIT = 10;
|
|
6358
6451
|
const RG_TIME_LIMIT = 5e3;
|
|
@@ -6377,7 +6470,7 @@ async function searchMessages(query) {
|
|
|
6377
6470
|
failed: false
|
|
6378
6471
|
};
|
|
6379
6472
|
} catch (error) {
|
|
6380
|
-
|
|
6473
|
+
log$6.warn(`could not search ${providerName(provider)} messages:`, error);
|
|
6381
6474
|
return {
|
|
6382
6475
|
provider,
|
|
6383
6476
|
matches: [],
|
|
@@ -6427,7 +6520,7 @@ async function claudeCandidates(query) {
|
|
|
6427
6520
|
const lists = await Promise.all(words.map((word) => sessionsWith(word)));
|
|
6428
6521
|
return new Set(lists[0].filter((id) => lists.every((list) => list.includes(id))));
|
|
6429
6522
|
} catch (error) {
|
|
6430
|
-
|
|
6523
|
+
log$6.warn(`could not narrow Claude sessions with rg:`, error);
|
|
6431
6524
|
return null;
|
|
6432
6525
|
}
|
|
6433
6526
|
}
|
|
@@ -6989,6 +7082,7 @@ function shellIntegrationIssue(command, platform, defaultShell) {
|
|
|
6989
7082
|
return "Available for bash, zsh and fish";
|
|
6990
7083
|
}
|
|
6991
7084
|
// src/services/terminal/shell-integration.ts
|
|
7085
|
+
const log$5 = logger.withTag("terminal");
|
|
6992
7086
|
const scripts = path.join(serverDir, "src/services/terminal/shell-integration");
|
|
6993
7087
|
const folder = path.join(home, "shell-integration");
|
|
6994
7088
|
function prepare(env, command, shell) {
|
|
@@ -7031,7 +7125,7 @@ function zshGlobalsSupported() {
|
|
|
7031
7125
|
contents = readFileSync(global, "utf8");
|
|
7032
7126
|
} catch (error) {
|
|
7033
7127
|
if (error.code === "ENOENT") continue;
|
|
7034
|
-
|
|
7128
|
+
log$5.warn(`could not read ${global}, zsh starts without shell integration:`, error);
|
|
7035
7129
|
return false;
|
|
7036
7130
|
}
|
|
7037
7131
|
for (const rawLine of contents.split("\n")) {
|
|
@@ -7076,11 +7170,12 @@ function writeScript(target, source) {
|
|
|
7076
7170
|
renameSync(`${target}.tmp`, target);
|
|
7077
7171
|
return true;
|
|
7078
7172
|
} catch (error) {
|
|
7079
|
-
|
|
7173
|
+
log$5.warn(`could not write ${target}, the shell starts without shell integration:`, error);
|
|
7080
7174
|
return false;
|
|
7081
7175
|
}
|
|
7082
7176
|
}
|
|
7083
7177
|
// src/services/terminal/process.ts
|
|
7178
|
+
const log$4 = logger.withTag("terminal");
|
|
7084
7179
|
const run = promisify(execFile);
|
|
7085
7180
|
const iterm2Version = "3.6.8";
|
|
7086
7181
|
const ghosttyVersion = "1.3.1";
|
|
@@ -7176,7 +7271,7 @@ async function busyProgram(pid) {
|
|
|
7176
7271
|
try {
|
|
7177
7272
|
return await programName(group);
|
|
7178
7273
|
} catch (error) {
|
|
7179
|
-
|
|
7274
|
+
log$4.warn(`could not read the name of process ${group}:`, error);
|
|
7180
7275
|
return `process ${group}`;
|
|
7181
7276
|
}
|
|
7182
7277
|
}
|
|
@@ -7418,6 +7513,7 @@ var Alerts = class {
|
|
|
7418
7513
|
}
|
|
7419
7514
|
};
|
|
7420
7515
|
// src/services/terminal/terminal.ts
|
|
7516
|
+
const log$3 = logger.withTag("terminal");
|
|
7421
7517
|
const HISTORY_LIMIT = 2097152;
|
|
7422
7518
|
const OUTPUT_LIMIT = 1048576;
|
|
7423
7519
|
const STALL_LIMIT = 1e4;
|
|
@@ -7452,7 +7548,7 @@ async function terminalRequest(client, message) {
|
|
|
7452
7548
|
function terminalInput(client, frame) {
|
|
7453
7549
|
const connection = connections.get(client);
|
|
7454
7550
|
if (connection === void 0) {
|
|
7455
|
-
|
|
7551
|
+
log$3.warn("terminal input from a browser that is not attached");
|
|
7456
7552
|
return;
|
|
7457
7553
|
}
|
|
7458
7554
|
input(connection, frame);
|
|
@@ -7469,7 +7565,7 @@ async function busyTerminal(id) {
|
|
|
7469
7565
|
try {
|
|
7470
7566
|
return { program: await busyProgram(terminal.pty.pid) };
|
|
7471
7567
|
} catch (error) {
|
|
7472
|
-
|
|
7568
|
+
log$3.warn(`could not tell if terminal ${id} is busy:`, error);
|
|
7473
7569
|
return { error: String(error) };
|
|
7474
7570
|
}
|
|
7475
7571
|
}
|
|
@@ -7538,12 +7634,12 @@ function input(connection, bytes) {
|
|
|
7538
7634
|
try {
|
|
7539
7635
|
frame = decodeFrame(bytes);
|
|
7540
7636
|
} catch (error) {
|
|
7541
|
-
|
|
7637
|
+
log$3.warn(`ignoring terminal input:`, error);
|
|
7542
7638
|
return;
|
|
7543
7639
|
}
|
|
7544
7640
|
const terminal = terminals.get(frame.id);
|
|
7545
7641
|
if (terminal === void 0) {
|
|
7546
|
-
|
|
7642
|
+
log$3.warn(`input for terminal ${frame.id}, which does not exist`);
|
|
7547
7643
|
return;
|
|
7548
7644
|
}
|
|
7549
7645
|
if (frame.kind === 1) {
|
|
@@ -7555,7 +7651,7 @@ function input(connection, bytes) {
|
|
|
7555
7651
|
terminal.pty.resize(size.cols, size.rows);
|
|
7556
7652
|
}
|
|
7557
7653
|
} else {
|
|
7558
|
-
|
|
7654
|
+
log$3.warn(`ignoring terminal input of unknown kind ${frame.kind}`);
|
|
7559
7655
|
return;
|
|
7560
7656
|
}
|
|
7561
7657
|
terminal.pty.write(Buffer.from(frame.data));
|
|
@@ -7563,7 +7659,7 @@ function input(connection, bytes) {
|
|
|
7563
7659
|
async function createTab(projectId) {
|
|
7564
7660
|
const cwd = projectId === null ? workingDirectory() : projectPath(projectId);
|
|
7565
7661
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
7566
|
-
|
|
7662
|
+
log$3.error(`${cwd} is not a folder, terminal not created`);
|
|
7567
7663
|
return;
|
|
7568
7664
|
}
|
|
7569
7665
|
const activeId = active.get(projectId ?? "global");
|
|
@@ -7572,7 +7668,7 @@ async function createTab(projectId) {
|
|
|
7572
7668
|
if (current !== void 0) try {
|
|
7573
7669
|
directory = await currentDirectory(current.pty.pid, current.scanner.directory);
|
|
7574
7670
|
} catch (error) {
|
|
7575
|
-
|
|
7671
|
+
log$3.warn(`starting in the project folder, could not read the folder of the active terminal:`, error);
|
|
7576
7672
|
}
|
|
7577
7673
|
nextTab += 1;
|
|
7578
7674
|
const id = nextTab;
|
|
@@ -7580,7 +7676,7 @@ async function createTab(projectId) {
|
|
|
7580
7676
|
try {
|
|
7581
7677
|
pty = startShell(directory);
|
|
7582
7678
|
} catch (error) {
|
|
7583
|
-
|
|
7679
|
+
log$3.error(`could not start terminal ${id}:`, error);
|
|
7584
7680
|
return;
|
|
7585
7681
|
}
|
|
7586
7682
|
const terminal = {
|
|
@@ -7591,6 +7687,7 @@ async function createTab(projectId) {
|
|
|
7591
7687
|
alerts: new Alerts(),
|
|
7592
7688
|
scanner: new Scanner()
|
|
7593
7689
|
};
|
|
7690
|
+
log$3.info(`started terminal ${id} with pid ${pty.pid} in ${directory}`);
|
|
7594
7691
|
terminals.set(id, terminal);
|
|
7595
7692
|
tabs.push({
|
|
7596
7693
|
id,
|
|
@@ -7603,7 +7700,8 @@ async function createTab(projectId) {
|
|
|
7603
7700
|
pty.pause();
|
|
7604
7701
|
held.add(pty);
|
|
7605
7702
|
}
|
|
7606
|
-
pty.onExit(() => {
|
|
7703
|
+
pty.onExit((event) => {
|
|
7704
|
+
log$3.info(`terminal ${id} exited with code ${event.exitCode}`);
|
|
7607
7705
|
terminals.delete(id);
|
|
7608
7706
|
paused.delete(pty);
|
|
7609
7707
|
held.delete(pty);
|
|
@@ -7613,6 +7711,7 @@ async function createTab(projectId) {
|
|
|
7613
7711
|
function closeTab(id) {
|
|
7614
7712
|
const tab = findTab(id);
|
|
7615
7713
|
if (tab === void 0) return false;
|
|
7714
|
+
log$3.debug(`closing terminal ${id}`);
|
|
7616
7715
|
release(tab);
|
|
7617
7716
|
tabs.splice(tabs.indexOf(tab), 1);
|
|
7618
7717
|
const terminal = terminals.get(id);
|
|
@@ -7654,7 +7753,7 @@ function drain() {
|
|
|
7654
7753
|
last.queued = queued;
|
|
7655
7754
|
progress.set(connection, last);
|
|
7656
7755
|
if (now - last.at > STALL_LIMIT) {
|
|
7657
|
-
|
|
7756
|
+
log$3.warn("a browser is not reading fast enough, disconnecting it");
|
|
7658
7757
|
connection.client.socket.terminate();
|
|
7659
7758
|
continue;
|
|
7660
7759
|
}
|
|
@@ -7899,6 +7998,7 @@ function saveAppIcon(value) {
|
|
|
7899
7998
|
return result.data.appIcon;
|
|
7900
7999
|
}
|
|
7901
8000
|
// src/api/socket.ts
|
|
8001
|
+
const log$2 = logger.withTag("socket");
|
|
7902
8002
|
const handlers = {
|
|
7903
8003
|
ping: () => null,
|
|
7904
8004
|
sync: () => {
|
|
@@ -8128,6 +8228,7 @@ function connectClient(socket, device, address) {
|
|
|
8128
8228
|
send: (message) => socket.send(JSON.stringify(message))
|
|
8129
8229
|
};
|
|
8130
8230
|
clients.add(client);
|
|
8231
|
+
log$2.info(`a browser connected from ${address}`);
|
|
8131
8232
|
socket.on("message", (data, isBinary) => {
|
|
8132
8233
|
client.heardAt = Date.now();
|
|
8133
8234
|
if (isBinary) {
|
|
@@ -8136,8 +8237,9 @@ function connectClient(socket, device, address) {
|
|
|
8136
8237
|
}
|
|
8137
8238
|
answer(client, String(data));
|
|
8138
8239
|
});
|
|
8139
|
-
socket.on("error", (error) =>
|
|
8140
|
-
socket.on("close", () => {
|
|
8240
|
+
socket.on("error", (error) => log$2.warn(`a browser connection from ${address} failed:`, error));
|
|
8241
|
+
socket.on("close", (code) => {
|
|
8242
|
+
log$2.info(`a browser from ${address} disconnected (code ${code})`);
|
|
8141
8243
|
clients.delete(client);
|
|
8142
8244
|
detachTerminal(client);
|
|
8143
8245
|
});
|
|
@@ -8147,9 +8249,10 @@ async function answer(client, text) {
|
|
|
8147
8249
|
try {
|
|
8148
8250
|
call = JSON.parse(text);
|
|
8149
8251
|
} catch (error) {
|
|
8150
|
-
|
|
8252
|
+
log$2.warn(`ignoring a message from ${client.address} that is not JSON:`, error);
|
|
8151
8253
|
return;
|
|
8152
8254
|
}
|
|
8255
|
+
const started = performance.now();
|
|
8153
8256
|
try {
|
|
8154
8257
|
if (!Object.hasOwn(handlers, call.method)) throw new Error(`unknown method ${call.method}`);
|
|
8155
8258
|
const handler = handlers[call.method];
|
|
@@ -8160,11 +8263,15 @@ async function answer(client, text) {
|
|
|
8160
8263
|
result
|
|
8161
8264
|
});
|
|
8162
8265
|
} catch (error) {
|
|
8266
|
+
log$2.warn(`${call.method} from ${client.address} failed:`, error);
|
|
8163
8267
|
client.send({
|
|
8164
8268
|
type: "reply",
|
|
8165
8269
|
id: call.id,
|
|
8166
8270
|
error: error instanceof Error ? error.message : String(error)
|
|
8167
8271
|
});
|
|
8272
|
+
} finally {
|
|
8273
|
+
const took = Math.round(performance.now() - started);
|
|
8274
|
+
if (call.method !== "ping") log$2.debug(`${call.method} from ${client.address} took ${took}ms`);
|
|
8168
8275
|
}
|
|
8169
8276
|
}
|
|
8170
8277
|
// src/lib/proxy/css.ts
|
|
@@ -9337,6 +9444,7 @@ function fail(response, status, message) {
|
|
|
9337
9444
|
response.end(message);
|
|
9338
9445
|
}
|
|
9339
9446
|
// src/services/app-icon.ts
|
|
9447
|
+
const log$1 = logger.withTag("app-icon");
|
|
9340
9448
|
const pngs = /* @__PURE__ */ new Map([
|
|
9341
9449
|
["icon-192.png", {
|
|
9342
9450
|
size: 192,
|
|
@@ -9369,7 +9477,7 @@ async function serveAppIcon(publicDir, pathname, response) {
|
|
|
9369
9477
|
response.setHeader("cache-control", "no-cache");
|
|
9370
9478
|
response.end(icon.body);
|
|
9371
9479
|
} catch (error) {
|
|
9372
|
-
|
|
9480
|
+
log$1.warn(`could not serve the app icon:`, error);
|
|
9373
9481
|
response.statusCode = 500;
|
|
9374
9482
|
response.end("could not render the app icon");
|
|
9375
9483
|
}
|
|
@@ -9401,10 +9509,53 @@ function tile(icon, maskable) {
|
|
|
9401
9509
|
if (icon.tile.shape === "circle") return `<circle cx="256" cy="256" r="256" fill="${icon.tile.color}"/>`;
|
|
9402
9510
|
throw new Error(`unknown tile shape for ${icon.id}`);
|
|
9403
9511
|
}
|
|
9512
|
+
// src/services/browser-logs.ts
|
|
9513
|
+
const logsBody = z.array(z.object({
|
|
9514
|
+
time: z.number(),
|
|
9515
|
+
type: z.enum([
|
|
9516
|
+
"fatal",
|
|
9517
|
+
"error",
|
|
9518
|
+
"warn",
|
|
9519
|
+
"log",
|
|
9520
|
+
"info",
|
|
9521
|
+
"success",
|
|
9522
|
+
"fail",
|
|
9523
|
+
"ready",
|
|
9524
|
+
"start",
|
|
9525
|
+
"box",
|
|
9526
|
+
"debug",
|
|
9527
|
+
"trace",
|
|
9528
|
+
"verbose"
|
|
9529
|
+
]),
|
|
9530
|
+
tag: z.string(),
|
|
9531
|
+
message: z.string()
|
|
9532
|
+
}));
|
|
9533
|
+
async function saveBrowserLogs(device, request, response) {
|
|
9534
|
+
let body = "";
|
|
9535
|
+
for await (const chunk of request) body += chunk;
|
|
9536
|
+
let entries;
|
|
9537
|
+
try {
|
|
9538
|
+
entries = logsBody.parse(JSON.parse(body));
|
|
9539
|
+
} catch {
|
|
9540
|
+
response.statusCode = 400;
|
|
9541
|
+
response.end("expected [{ time, type, tag, message }]");
|
|
9542
|
+
return;
|
|
9543
|
+
}
|
|
9544
|
+
const source = device === null ? request.socket.remoteAddress ?? "unknown address" : readDevices().find((item) => item.hash === device)?.name ?? "Unnamed";
|
|
9545
|
+
for (const entry of entries) browserLogger.log({
|
|
9546
|
+
date: new Date(entry.time),
|
|
9547
|
+
type: entry.type,
|
|
9548
|
+
tag: entry.tag,
|
|
9549
|
+
args: [`${source}: ${entry.message}`]
|
|
9550
|
+
});
|
|
9551
|
+
response.statusCode = 204;
|
|
9552
|
+
response.end();
|
|
9553
|
+
}
|
|
9404
9554
|
// src/main.ts
|
|
9405
9555
|
const UNPAIR_CHECK = 5e3;
|
|
9406
9556
|
const TICKET_LIFETIME = 6e4;
|
|
9407
9557
|
const openBody = z.object({ path: z.string() });
|
|
9558
|
+
const log = logger.withTag("server");
|
|
9408
9559
|
const contentTypes = {
|
|
9409
9560
|
".html": "text/html; charset=utf-8",
|
|
9410
9561
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -9440,6 +9591,7 @@ const browseTypes = new Map([
|
|
|
9440
9591
|
const port = Number(listenPort());
|
|
9441
9592
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
9442
9593
|
console.error(`invalid port ${args.port}`);
|
|
9594
|
+
log.error(`invalid port ${args.port}`);
|
|
9443
9595
|
process.exit(1);
|
|
9444
9596
|
}
|
|
9445
9597
|
const address = `http://${args.host}:${port}`;
|
|
@@ -9454,12 +9606,14 @@ if (args.command === "unpair") {
|
|
|
9454
9606
|
process.exit(0);
|
|
9455
9607
|
}
|
|
9456
9608
|
lockHome();
|
|
9609
|
+
log.info(`starting offsite ${args.command} with pid ${process.pid} on ${address}, dir ${args.dir}, auth ${args.auth ? "on" : "off"}`);
|
|
9457
9610
|
process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
|
|
9458
9611
|
installOpen(args.host, port);
|
|
9459
9612
|
watchSessions();
|
|
9460
9613
|
refreshAll();
|
|
9461
9614
|
if (findClaude() === null) {
|
|
9462
9615
|
console.error("could not find claude on PATH; install Claude Code first");
|
|
9616
|
+
log.error("could not find claude on PATH; install Claude Code first");
|
|
9463
9617
|
process.exit(1);
|
|
9464
9618
|
}
|
|
9465
9619
|
const publicDir = path.join(import.meta.dirname, "..", "..", "client", "dist");
|
|
@@ -9469,7 +9623,7 @@ const server = createServer((request, response) => {
|
|
|
9469
9623
|
try {
|
|
9470
9624
|
answerRequest(request, response);
|
|
9471
9625
|
} catch (error) {
|
|
9472
|
-
|
|
9626
|
+
log.warn("a request failed:", error);
|
|
9473
9627
|
if (response.headersSent) {
|
|
9474
9628
|
response.destroy();
|
|
9475
9629
|
return;
|
|
@@ -9559,11 +9713,16 @@ server.listen(port, args.host, () => {
|
|
|
9559
9713
|
console.log(`offsite is running at ${address}`);
|
|
9560
9714
|
if (args.auth && args.pair) console.log("Run offsite pair to pair another browser");
|
|
9561
9715
|
}
|
|
9562
|
-
|
|
9716
|
+
log.info(`listening on ${address}`);
|
|
9717
|
+
if (!args.auth) {
|
|
9718
|
+
console.warn("auth is off; anyone who can reach this port can control this machine");
|
|
9719
|
+
log.warn("auth is off; anyone who can reach this port can control this machine");
|
|
9720
|
+
}
|
|
9563
9721
|
});
|
|
9564
9722
|
process.on("SIGINT", stop);
|
|
9565
9723
|
process.on("SIGTERM", stop);
|
|
9566
|
-
function stop() {
|
|
9724
|
+
function stop(signal) {
|
|
9725
|
+
log.info(`stopping on ${signal}`);
|
|
9567
9726
|
stopAll();
|
|
9568
9727
|
process.exit(0);
|
|
9569
9728
|
}
|
|
@@ -9592,6 +9751,16 @@ function answerApi(pathname, request, response) {
|
|
|
9592
9751
|
answerJson(response, { token });
|
|
9593
9752
|
return;
|
|
9594
9753
|
}
|
|
9754
|
+
if (pathname === "/api/logs") {
|
|
9755
|
+
const device = given === null ? null : findDevice(given);
|
|
9756
|
+
if (args.auth && device === null) {
|
|
9757
|
+
response.statusCode = 401;
|
|
9758
|
+
response.end("This browser needs pairing");
|
|
9759
|
+
return;
|
|
9760
|
+
}
|
|
9761
|
+
saveBrowserLogs(device, request, response);
|
|
9762
|
+
return;
|
|
9763
|
+
}
|
|
9595
9764
|
if (pathname === "/api/ticket") {
|
|
9596
9765
|
if (!args.auth) {
|
|
9597
9766
|
answerJson(response, { ticket: null });
|