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.
@@ -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)) return current;
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) return pending;
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: await providerLimits(account),
214
+ limits,
140
215
  fetchedAt: Date.now(),
141
216
  updatedAt: Date.now(),
142
217
  error: null
143
218
  };
144
219
  } catch (error) {
145
- console.warn(`could not read usage limits for ${account.name}: ${error}`);
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
- consola.error(`offsite is already running for ${home} (pid ${pid})`);
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
- console.warn(`invalid ${file}, using the defaults: ${error}`);
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
- console.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
450
+ log$17.warn(`invalid ${file}, using the defaults:\n${z.prettifyError(result.error)}`);
408
451
  return structuredClone(defaults);
409
452
  }
410
- console.warn(`${file}: discarded ${discarded.map(String).join(".")} (${issue.message} at ${target.map(String).join(".")})`);
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
- return await this.call(method, params, REQUEST_TIMEOUT);
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
- console.warn(`ignoring a Codex line that is not JSON: ${line}`);
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") console.warn(`could not read the Codex account name: ${error}`);
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
- console.warn(`could not auto-resolve ${request.id}: ${error}`);
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) => console.warn(`could not unload Codex thread: ${error}`)).finally(() => this.end());
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) => console.warn(`could not load OpenCode messages: ${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
- console.warn(`could not search ${providerName(provider)}: ${error}`);
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
- console.warn(`could not list ${providerName(provider)} sessions: ${message}`);
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) => console.warn(`stopped watching ${dir}: ${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
- console.warn(`could not check other chats in ${project.path}: ${error}`);
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
- console.warn("the OpenCode event stream ended");
4451
+ log$10.warn("the OpenCode event stream ended");
4369
4452
  } catch (error) {
4370
4453
  if (listener !== controller) return;
4371
- console.warn(controller.signal.aborted ? `the OpenCode event stream was silent for ${SILENCE_LIMIT$2 / 1e3} seconds` : `lost the OpenCode event stream: ${error}`);
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) => console.warn(`could not catch up with OpenCode: ${error}`));
4476
+ await catchUp(client).catch((error) => log$10.warn(`could not catch up with OpenCode:`, error));
4392
4477
  return client;
4393
4478
  }
4394
- console.warn(`could not reconnect to OpenCode: ${failure}`);
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
- console.warn(`could not list accounts: ${error}`);
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
- console.log("a browser subscribed to push notifications");
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
- console.log("a browser unsubscribed from push notifications");
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 sent = false;
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
- sent = true;
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
- console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
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
- console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
4797
+ log$8.warn(`could not send a push to ${subscription.endpoint}:`, error);
4711
4798
  }
4712
4799
  }));
4713
- if (gone.length === 0) return sent;
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 sent;
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") console.warn(`could not read the Claude account name: ${error}`);
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) this.add(notice(error instanceof Error ? error.message : String(error)));
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
- console.warn(`could not search ${providerName(provider)} messages: ${error}`);
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
- console.warn(`could not narrow Claude sessions with rg: ${error}`);
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
- console.warn(`could not read ${global}, zsh starts without shell integration: ${error}`);
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
- console.warn(`could not write ${target}, the shell starts without shell integration: ${error}`);
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
- console.warn(`could not read the name of process ${group}: ${error}`);
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
- console.warn("terminal input from a browser that is not attached");
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
- console.warn(`could not tell if terminal ${id} is busy: ${error}`);
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
- console.warn(`ignoring terminal input: ${error}`);
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
- console.warn(`input for terminal ${frame.id}, which does not exist`);
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
- console.warn(`ignoring terminal input of unknown kind ${frame.kind}`);
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
- console.error(`${cwd} is not a folder, terminal not created`);
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
- console.warn(`starting in the project folder, could not read the folder of the active terminal: ${error}`);
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
- console.error(`could not start terminal ${id}: ${error}`);
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
- console.warn("a browser is not reading fast enough, disconnecting it");
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) => console.warn(`a browser connection failed: ${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
- console.warn(`ignoring a message that is not JSON: ${error}`);
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
- console.warn(`could not serve the app icon: ${error}`);
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
- console.warn(`a request failed: ${error}`);
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
- if (!args.auth) console.warn("auth is off; anyone who can reach this port can control this machine");
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 });