kaizenai 0.4.0 → 0.6.0

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,14 +1,14 @@
1
1
  // @bun
2
2
  // src/server/cli.ts
3
3
  import { existsSync as existsSync10 } from "fs";
4
- import process9 from "process";
4
+ import process10 from "process";
5
+ import { fileURLToPath } from "url";
5
6
  // package.json
6
7
  var package_default = {
7
8
  name: "kaizenai",
8
9
  type: "module",
9
- version: "0.4.0",
10
+ version: "0.6.0",
10
11
  description: "Local web UI for coding agents",
11
- license: "MIT",
12
12
  keywords: [
13
13
  "claude",
14
14
  "claude-code",
@@ -44,15 +44,15 @@ var package_default = {
44
44
  bun: ">=1.3.5"
45
45
  },
46
46
  scripts: {
47
- build: "vite build && bun run ./scripts/build-server.ts",
47
+ build: "vite build && bun ./scripts/build-server.ts",
48
48
  check: "tsc --noEmit && bun run build",
49
- dev: "bun run ./scripts/dev.ts",
49
+ dev: "bun ./scripts/dev.ts",
50
50
  "dev:client": "vite --host 0.0.0.0 --port 5174",
51
- "dev:server": "bun run ./scripts/dev-server.ts --no-open --port 5175",
51
+ "dev:server": "bun ./scripts/dev-server.ts --no-open --port 5175",
52
52
  fmt: "prettier package.json README.md --check",
53
53
  "install:dev": "bun install && bun run build && bun install -g .",
54
54
  lint: "tsc --noEmit",
55
- start: "bun run ./dist/server/cli.js",
55
+ start: "bun ./dist/server/cli.js",
56
56
  test: "bun test",
57
57
  typecheck: "tsc --noEmit",
58
58
  prepublishOnly: "bun run build"
@@ -68,6 +68,7 @@ var package_default = {
68
68
  "@xterm/xterm": "^6.0.0",
69
69
  "default-shell": "^2.2.0",
70
70
  "puppeteer-core": "^24.40.0",
71
+ qrcode: "^1.5.4",
71
72
  "react-resizable-panels": "^4.7.3",
72
73
  sonner: "^2.0.7"
73
74
  },
@@ -159,8 +160,8 @@ function getProviderSettingsFilePath(homeDir, env = getRuntimeEnv()) {
159
160
  }
160
161
 
161
162
  // src/server/cli-runtime.ts
162
- import process2 from "process";
163
- import { spawnSync as spawnSync2 } from "child_process";
163
+ import process3 from "process";
164
+ import { spawnSync as spawnSync3 } from "child_process";
164
165
 
165
166
  // src/server/process-utils.ts
166
167
  import { spawn, spawnSync } from "child_process";
@@ -185,6 +186,277 @@ function canOpenMacApp(appName) {
185
186
 
186
187
  // src/shared/ports.ts
187
188
  var PROD_SERVER_PORT = 3210;
189
+ var DEV_CLIENT_PORT = 5174;
190
+
191
+ // src/server/remote-access.ts
192
+ import { spawnSync as spawnSync2 } from "child_process";
193
+ import { isIP } from "net";
194
+ import QRCode from "qrcode";
195
+ var TAILSCALE_COMMAND = "tailscale";
196
+ var HTTPS_PORT = 443;
197
+ var COMMAND_TIMEOUT_MS = 5000;
198
+ function normalizeTailscaleHostname(hostname) {
199
+ if (!hostname)
200
+ return null;
201
+ const normalized = hostname.trim().replace(/\.+$/, "");
202
+ return normalized || null;
203
+ }
204
+ function selectPreferredTailscaleHost(status) {
205
+ const dnsName = normalizeTailscaleHostname(status.Self?.DNSName);
206
+ if (dnsName)
207
+ return dnsName;
208
+ const ip = status.Self?.TailscaleIPs?.find((candidate) => candidate.trim().length > 0)?.trim();
209
+ return ip || null;
210
+ }
211
+ function buildRemoteBaseUrl(host, port, protocol = "http") {
212
+ const url = new URL(`${protocol}://${host}`);
213
+ if (protocol === "https" && port !== HTTPS_PORT || protocol === "http" && port !== 80) {
214
+ url.port = String(port);
215
+ }
216
+ return url.toString().replace(/\/$/, "");
217
+ }
218
+ function buildHandoffUrl(remoteBaseUrl) {
219
+ return new URL("/open", `${remoteBaseUrl}/`).toString();
220
+ }
221
+ function buildRemoteSetupUrl(baseUrl, localPort) {
222
+ return buildRemoteSetupUrlWithOptions(baseUrl, localPort, {});
223
+ }
224
+ function buildRemoteSetupUrlWithOptions(baseUrl, localPort, options) {
225
+ const url = new URL("/remote-setup", `${baseUrl}/`);
226
+ url.searchParams.set("localPort", String(localPort));
227
+ if (options.setupReason) {
228
+ url.searchParams.set("reason", options.setupReason);
229
+ }
230
+ if (options.setupCommand) {
231
+ url.searchParams.set("setupCommand", options.setupCommand);
232
+ }
233
+ return url.toString();
234
+ }
235
+ function createRemoteAccessWarning(reason) {
236
+ if (reason === "missing") {
237
+ return "remote mode is available, but Tailscale is not installed. Install Tailscale to print a stable handoff QR code.";
238
+ }
239
+ if (reason === "disconnected") {
240
+ return "remote mode bound all interfaces, but Tailscale is not connected. Run `tailscale up` to enable QR handoff.";
241
+ }
242
+ if (reason === "https_requires_dns") {
243
+ return "Tailscale HTTPS handoff requires this machine\u2019s MagicDNS name. Falling back to HTTP because only a Tailscale IP was available.";
244
+ }
245
+ if (reason === "serve_unavailable") {
246
+ return "Tailscale HTTPS Serve was unavailable, so Kaizen fell back to an HTTP handoff URL.";
247
+ }
248
+ if (reason === "serve_not_enabled") {
249
+ return "Tailscale HTTPS Serve is not enabled on this tailnet, so Kaizen fell back to an HTTP handoff URL.";
250
+ }
251
+ if (reason === "serve_conflict") {
252
+ return "Tailscale HTTPS Serve is already configured for another local service on this machine, so Kaizen fell back to an HTTP handoff URL.";
253
+ }
254
+ if (reason === "serve_permission_denied") {
255
+ return "Kaizen could not enable Tailscale HTTPS Serve because this user is not allowed to change Tailscale Serve config yet.";
256
+ }
257
+ if (reason === "serve_failed") {
258
+ return "Kaizen could not enable Tailscale HTTPS Serve and fell back to an HTTP handoff URL.";
259
+ }
260
+ return "remote mode bound all interfaces, but Kaizen could not resolve this machine\u2019s Tailscale address.";
261
+ }
262
+ function readTailscaleStatus() {
263
+ const result = spawnSync2(TAILSCALE_COMMAND, ["status", "--json"], {
264
+ stdio: ["ignore", "pipe", "pipe"],
265
+ encoding: "utf8",
266
+ timeout: COMMAND_TIMEOUT_MS
267
+ });
268
+ if (result.status !== 0) {
269
+ return null;
270
+ }
271
+ try {
272
+ return JSON.parse(result.stdout);
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ function readTailscaleServeStatus() {
278
+ const result = spawnSync2(TAILSCALE_COMMAND, ["serve", "status", "--json"], {
279
+ stdio: ["ignore", "pipe", "pipe"],
280
+ encoding: "utf8",
281
+ timeout: COMMAND_TIMEOUT_MS
282
+ });
283
+ if (result.status !== 0) {
284
+ return null;
285
+ }
286
+ try {
287
+ return JSON.parse(result.stdout);
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+ function enableTailscaleServeHttps(targetUrl) {
293
+ const result = spawnSync2(TAILSCALE_COMMAND, ["serve", "--bg", "--yes", "--https", String(HTTPS_PORT), targetUrl], {
294
+ stdio: ["ignore", "pipe", "pipe"],
295
+ encoding: "utf8",
296
+ timeout: COMMAND_TIMEOUT_MS
297
+ });
298
+ return {
299
+ ok: result.status === 0,
300
+ output: `${result.stdout ?? ""}
301
+ ${result.stderr ?? ""}`.trim()
302
+ };
303
+ }
304
+ function isRecord(value) {
305
+ return typeof value === "object" && value !== null;
306
+ }
307
+ function isEmptyTailscaleServeStatus(status) {
308
+ const topLevelValues = Object.values(status);
309
+ if (topLevelValues.length === 0)
310
+ return true;
311
+ return topLevelValues.every((value) => isRecord(value) && Object.keys(value).length === 0);
312
+ }
313
+ function buildServeTargetCandidates(localUrl, port) {
314
+ return [
315
+ localUrl,
316
+ `http://127.0.0.1:${String(port)}`,
317
+ `http://localhost:${String(port)}`,
318
+ `127.0.0.1:${String(port)}`,
319
+ `localhost:${String(port)}`,
320
+ String(port)
321
+ ];
322
+ }
323
+ function isKaizenServeConfig(status, localUrl, port, runtimeProfile) {
324
+ const serialized = JSON.stringify(status);
325
+ const currentTargets = buildServeTargetCandidates(localUrl, port);
326
+ if (currentTargets.some((candidate) => serialized.includes(candidate))) {
327
+ return true;
328
+ }
329
+ if (runtimeProfile !== "dev") {
330
+ const defaultDevTargets = [
331
+ `http://127.0.0.1:${String(DEV_CLIENT_PORT)}`,
332
+ `http://localhost:${String(DEV_CLIENT_PORT)}`,
333
+ `127.0.0.1:${String(DEV_CLIENT_PORT)}`,
334
+ `localhost:${String(DEV_CLIENT_PORT)}`
335
+ ];
336
+ return defaultDevTargets.some((candidate) => serialized.includes(candidate));
337
+ }
338
+ return false;
339
+ }
340
+ function canUseTailscaleHttps(host) {
341
+ return isIP(host) === 0 && host.includes(".");
342
+ }
343
+ function createServeOperatorSetupCommand() {
344
+ return "sudo tailscale set --operator=$USER";
345
+ }
346
+ function createDefaultRemoteAccessDeps() {
347
+ return {
348
+ hasCommand,
349
+ readTailscaleStatus,
350
+ readTailscaleServeStatus,
351
+ enableTailscaleServeHttps
352
+ };
353
+ }
354
+ async function resolveRemoteAccessInfo(options, deps = createDefaultRemoteAccessDeps()) {
355
+ const fallback = {
356
+ localUrl: options.localUrl,
357
+ remoteBaseUrl: null,
358
+ handoffUrl: null,
359
+ warning: null,
360
+ setupCommand: null,
361
+ setupReason: null
362
+ };
363
+ if (!deps.hasCommand(TAILSCALE_COMMAND)) {
364
+ return {
365
+ ...fallback,
366
+ warning: createRemoteAccessWarning("missing")
367
+ };
368
+ }
369
+ const status = deps.readTailscaleStatus();
370
+ if (!status) {
371
+ return {
372
+ ...fallback,
373
+ warning: createRemoteAccessWarning("unavailable")
374
+ };
375
+ }
376
+ if (status.BackendState && status.BackendState !== "Running") {
377
+ return {
378
+ ...fallback,
379
+ warning: createRemoteAccessWarning("disconnected")
380
+ };
381
+ }
382
+ const host = selectPreferredTailscaleHost(status);
383
+ if (!host) {
384
+ return {
385
+ ...fallback,
386
+ warning: createRemoteAccessWarning("unavailable")
387
+ };
388
+ }
389
+ let warning = null;
390
+ let remoteBaseUrl;
391
+ let setupCommand = null;
392
+ let setupReason = null;
393
+ const runtimeProfile = options.runtimeProfile ?? getRuntimeProfile();
394
+ if (canUseTailscaleHttps(host)) {
395
+ const serveStatus = deps.readTailscaleServeStatus();
396
+ if (!serveStatus) {
397
+ warning = createRemoteAccessWarning("serve_unavailable");
398
+ remoteBaseUrl = buildRemoteBaseUrl(host, options.port);
399
+ } else if (!isEmptyTailscaleServeStatus(serveStatus) && !isKaizenServeConfig(serveStatus, options.localUrl, options.port, runtimeProfile)) {
400
+ warning = createRemoteAccessWarning("serve_conflict");
401
+ remoteBaseUrl = buildRemoteBaseUrl(host, options.port);
402
+ } else {
403
+ const serveResult = deps.enableTailscaleServeHttps(`http://127.0.0.1:${String(options.port)}`);
404
+ if (serveResult.ok) {
405
+ remoteBaseUrl = buildRemoteBaseUrl(host, HTTPS_PORT, "https");
406
+ } else {
407
+ const output = serveResult.output.toLowerCase();
408
+ if (output.includes("serve config denied") || output.includes("to not require root") || output.includes("set --operator")) {
409
+ warning = createRemoteAccessWarning("serve_permission_denied");
410
+ setupCommand = createServeOperatorSetupCommand();
411
+ setupReason = "serve_permission_denied";
412
+ } else {
413
+ warning = createRemoteAccessWarning(output.includes("serve is not enabled on your tailnet") ? "serve_not_enabled" : "serve_failed");
414
+ }
415
+ remoteBaseUrl = buildRemoteBaseUrl(host, options.port);
416
+ }
417
+ }
418
+ } else {
419
+ warning = createRemoteAccessWarning("https_requires_dns");
420
+ remoteBaseUrl = buildRemoteBaseUrl(host, options.port);
421
+ }
422
+ return {
423
+ ...fallback,
424
+ remoteBaseUrl,
425
+ handoffUrl: buildHandoffUrl(remoteBaseUrl),
426
+ warning,
427
+ setupCommand,
428
+ setupReason
429
+ };
430
+ }
431
+ async function renderTerminalQrCode(url) {
432
+ return QRCode.toString(url, {
433
+ type: "terminal",
434
+ small: true,
435
+ margin: 1
436
+ });
437
+ }
438
+
439
+ // src/server/terminal-output.ts
440
+ import process2 from "process";
441
+ var ANSI_RESET = "\x1B[0m";
442
+ var ANSI_BOLD = "\x1B[1m";
443
+ var ANSI_CYAN = "\x1B[36m";
444
+ var ANSI_YELLOW = "\x1B[33m";
445
+ function shouldUseAnsiColor() {
446
+ return process2.stdout.isTTY === true || process2.stderr.isTTY === true;
447
+ }
448
+ function formatTerminalHighlight(label, value) {
449
+ if (!shouldUseAnsiColor()) {
450
+ return `${label}${value}`;
451
+ }
452
+ return `${ANSI_BOLD}${ANSI_CYAN}${label}${ANSI_RESET}${value}`;
453
+ }
454
+ function formatTerminalWarning(message) {
455
+ if (!shouldUseAnsiColor()) {
456
+ return message;
457
+ }
458
+ return `${ANSI_BOLD}${ANSI_YELLOW}${message}${ANSI_RESET}`;
459
+ }
188
460
 
189
461
  // src/server/restart.ts
190
462
  var CLI_CHILD_MODE_ENV_VAR = "KAIZEN_CLI_MODE";
@@ -241,6 +513,7 @@ function parseChildArgsEnv(value) {
241
513
 
242
514
  // src/server/cli-runtime.ts
243
515
  var MINIMUM_BUN_VERSION = "1.3.5";
516
+ var SUPPRESS_REMOTE_STARTUP_OUTPUT_ENV_VAR = "KAIZEN_SUPPRESS_REMOTE_STARTUP_OUTPUT";
244
517
  function printHelp() {
245
518
  console.log(`${APP_NAME} \u2014 local-only project chat UI
246
519
 
@@ -250,7 +523,7 @@ Usage:
250
523
  Options:
251
524
  --port <number> Port to listen on (default: ${PROD_SERVER_PORT})
252
525
  --host <host> Bind to a specific host or IP
253
- --remote Shortcut for --host 0.0.0.0
526
+ --remote Share to your Tailscale devices and print a QR handoff
254
527
  --strict-port Fail instead of trying another port
255
528
  --no-open Don't open browser automatically
256
529
  --version Print version and exit
@@ -261,6 +534,7 @@ function parseArgs(argv) {
261
534
  let host = "127.0.0.1";
262
535
  let openBrowser = true;
263
536
  let strictPort = false;
537
+ let remoteMode = false;
264
538
  for (let index = 0;index < argv.length; index += 1) {
265
539
  const arg = argv[index];
266
540
  if (arg === "--version" || arg === "-v") {
@@ -287,6 +561,7 @@ function parseArgs(argv) {
287
561
  }
288
562
  if (arg === "--remote") {
289
563
  host = "0.0.0.0";
564
+ remoteMode = true;
290
565
  continue;
291
566
  }
292
567
  if (arg === "--no-open") {
@@ -306,7 +581,8 @@ function parseArgs(argv) {
306
581
  port,
307
582
  host,
308
583
  openBrowser,
309
- strictPort
584
+ strictPort,
585
+ remoteMode
310
586
  }
311
587
  };
312
588
  }
@@ -323,14 +599,25 @@ function compareVersions(currentVersion, latestVersion) {
323
599
  }
324
600
  return 0;
325
601
  }
602
+ function isNpxRuntimePath(value) {
603
+ return /(^|[\\/])_npx([\\/]|$)/.test(value);
604
+ }
605
+ function createUnsupportedNpxInstallResult() {
606
+ return {
607
+ ok: false,
608
+ errorCode: "install_failed",
609
+ userTitle: "Update unavailable in npx",
610
+ userMessage: "Kaizen cannot self-update while running from npx. Rerun `npx kaizenai` to get the latest version, or install it globally with `npm install -g kaizenai`."
611
+ };
612
+ }
326
613
  function normalizeVersion(version) {
327
614
  return version.trim().replace(/^v/i, "").split("-")[0].split(".").map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
328
615
  }
329
616
  async function maybeSelfUpdate(_argv, deps) {
330
- if (process2.env[DISABLE_SELF_UPDATE_ENV_VAR] === "1") {
617
+ if (process3.env[DISABLE_SELF_UPDATE_ENV_VAR] === "1") {
331
618
  return null;
332
619
  }
333
- if (shouldSkipStartupUpdateOnce(process2.env[CLI_SKIP_STARTUP_UPDATE_ONCE_ENV_VAR])) {
620
+ if (shouldSkipStartupUpdateOnce(process3.env[CLI_SKIP_STARTUP_UPDATE_ONCE_ENV_VAR])) {
334
621
  deps.warn(`${LOG_PREFIX} skipping startup update after restart to avoid an update loop`);
335
622
  return null;
336
623
  }
@@ -395,8 +682,34 @@ async function runCli(argv, deps) {
395
682
  const displayHost = bindHost === "127.0.0.1" || bindHost === "0.0.0.0" ? "localhost" : bindHost;
396
683
  const launchUrl = `http://${displayHost}:${port}`;
397
684
  deps.log(`${LOG_PREFIX} listening on http://${bindHost}:${port}`);
685
+ deps.log(`${LOG_PREFIX} local URL: ${launchUrl}`);
398
686
  deps.log(`${LOG_PREFIX} data dir: ${getDataDirDisplay()}`);
399
- const suppressOpenBrowser = process2.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] === "1";
687
+ if (parsedArgs.options.remoteMode && process3.env[SUPPRESS_REMOTE_STARTUP_OUTPUT_ENV_VAR] !== "1") {
688
+ const remoteAccess = await deps.resolveRemoteAccessInfo({
689
+ localUrl: launchUrl,
690
+ port,
691
+ runtimeProfile: getRuntimeProfile()
692
+ });
693
+ deps.log(formatTerminalHighlight(`${LOG_PREFIX} remote setup help: `, buildRemoteSetupUrlWithOptions(launchUrl, port, {
694
+ setupReason: remoteAccess.setupReason,
695
+ setupCommand: remoteAccess.setupCommand
696
+ })));
697
+ if (remoteAccess.warning) {
698
+ deps.warn(formatTerminalWarning(`${LOG_PREFIX} ${remoteAccess.warning}`));
699
+ if (remoteAccess.setupCommand) {
700
+ deps.warn(formatTerminalHighlight(`${LOG_PREFIX} one-time Tailscale fix: `, remoteAccess.setupCommand));
701
+ }
702
+ }
703
+ if (remoteAccess.remoteBaseUrl && remoteAccess.handoffUrl) {
704
+ deps.log(`${LOG_PREFIX} Tailscale URL: ${remoteAccess.remoteBaseUrl}`);
705
+ deps.log(`${LOG_PREFIX} handoff URL: ${remoteAccess.handoffUrl}`);
706
+ deps.log(`${LOG_PREFIX} scan to open on another Tailscale device:`);
707
+ deps.log((await deps.renderTerminalQrCode(remoteAccess.handoffUrl)).trimEnd());
708
+ }
709
+ } else {
710
+ deps.log(formatTerminalHighlight(`${LOG_PREFIX} remote setup help: `, buildRemoteSetupUrl(launchUrl, port)));
711
+ }
712
+ const suppressOpenBrowser = process3.env[CLI_SUPPRESS_OPEN_ONCE_ENV_VAR] === "1";
400
713
  if (parsedArgs.options.openBrowser && !suppressOpenBrowser) {
401
714
  deps.openUrl(launchUrl);
402
715
  }
@@ -406,7 +719,7 @@ async function runCli(argv, deps) {
406
719
  };
407
720
  }
408
721
  function openUrl(url) {
409
- const platform = process2.platform;
722
+ const platform = process3.platform;
410
723
  if (platform === "darwin") {
411
724
  spawnDetached("open", [url]);
412
725
  } else if (platform === "win32") {
@@ -453,16 +766,16 @@ function installPackageVersion(packageName, version) {
453
766
  userMessage: "Kaizen could not find Bun to install the update."
454
767
  };
455
768
  }
456
- const result = spawnSync2("bun", ["install", "-g", `${packageName}@${version}`], {
769
+ const result = spawnSync3("bun", ["install", "-g", `${packageName}@${version}`], {
457
770
  stdio: ["ignore", "pipe", "pipe"],
458
771
  encoding: "utf8"
459
772
  });
460
773
  const stdout = result.stdout ?? "";
461
774
  const stderr = result.stderr ?? "";
462
775
  if (stdout)
463
- process2.stdout.write(stdout);
776
+ process3.stdout.write(stdout);
464
777
  if (stderr)
465
- process2.stderr.write(stderr);
778
+ process3.stderr.write(stderr);
466
779
  if (result.status === 0) {
467
780
  return {
468
781
  ok: true,
@@ -500,6 +813,11 @@ var CODEX_REASONING_OPTIONS = [
500
813
  { id: "high", label: "High" },
501
814
  { id: "xhigh", label: "XHigh" }
502
815
  ];
816
+ var CLAUDE_CONTEXT_WINDOW_FALLBACKS = {
817
+ sonnet: 1e6,
818
+ opus: 1e6,
819
+ haiku: 200000
820
+ };
503
821
  var GEMINI_THINKING_OPTIONS = [
504
822
  { id: "off", label: "Off" },
505
823
  { id: "standard", label: "Standard" },
@@ -1085,6 +1403,7 @@ class EventStore {
1085
1403
  await this.ensureFile(this.turnsLogPath);
1086
1404
  await this.loadSnapshot();
1087
1405
  await this.replayLogs();
1406
+ await this.repairDuplicateSessionTokens();
1088
1407
  this.hydrateProjectWorktrees();
1089
1408
  await this.reconcileAllProjectFeatureState();
1090
1409
  if (!await this.hasLegacyTranscriptData() && await this.shouldCompact()) {
@@ -1552,6 +1871,29 @@ class EventStore {
1552
1871
  }
1553
1872
  return entries;
1554
1873
  }
1874
+ async repairDuplicateSessionTokens() {
1875
+ const chatsBySessionKey = new Map;
1876
+ for (const chat of this.state.chatsById.values()) {
1877
+ if (chat.deletedAt)
1878
+ continue;
1879
+ const sessionKey = this.getChatPersistenceKey(chat);
1880
+ if (!sessionKey)
1881
+ continue;
1882
+ const existing = chatsBySessionKey.get(sessionKey) ?? [];
1883
+ existing.push(chat);
1884
+ chatsBySessionKey.set(sessionKey, existing);
1885
+ }
1886
+ for (const chats of chatsBySessionKey.values()) {
1887
+ if (chats.length < 2)
1888
+ continue;
1889
+ const [keeper, ...duplicates] = [...chats].sort((left, right) => right.updatedAt - left.updatedAt || (right.lastMessageAt ?? 0) - (left.lastMessageAt ?? 0) || right.createdAt - left.createdAt);
1890
+ for (const duplicate of duplicates) {
1891
+ if (!duplicate.sessionToken)
1892
+ continue;
1893
+ await this.setSessionToken(duplicate.id, null);
1894
+ }
1895
+ }
1896
+ }
1555
1897
  async openProject(localPath, title) {
1556
1898
  const identity = resolveProjectRepositoryIdentity(localPath);
1557
1899
  const worktreePaths = identity.isGitRepo ? resolveProjectWorktreePaths(identity.worktreePath) : [identity.worktreePath];
@@ -2019,15 +2361,33 @@ class EventStore {
2019
2361
  const chat = this.requireChat(chatId);
2020
2362
  if (chat.sessionToken === sessionToken)
2021
2363
  return;
2022
- const event = {
2364
+ const affectedProjectIds = new Set([chat.projectId]);
2365
+ if (sessionToken && chat.provider) {
2366
+ for (const candidate of this.state.chatsById.values()) {
2367
+ if (candidate.id === chatId || candidate.deletedAt)
2368
+ continue;
2369
+ if (candidate.provider !== chat.provider || candidate.sessionToken !== sessionToken)
2370
+ continue;
2371
+ affectedProjectIds.add(candidate.projectId);
2372
+ await this.append(this.turnsLogPath, {
2373
+ v: STORE_VERSION,
2374
+ type: "session_token_set",
2375
+ timestamp: Date.now(),
2376
+ chatId: candidate.id,
2377
+ sessionToken: null
2378
+ });
2379
+ }
2380
+ }
2381
+ await this.append(this.turnsLogPath, {
2023
2382
  v: STORE_VERSION,
2024
2383
  type: "session_token_set",
2025
2384
  timestamp: Date.now(),
2026
2385
  chatId,
2027
2386
  sessionToken
2028
- };
2029
- await this.append(this.turnsLogPath, event);
2030
- await this.syncProjectFeatureState(chat.projectId);
2387
+ });
2388
+ for (const projectId of affectedProjectIds) {
2389
+ await this.syncProjectFeatureState(projectId);
2390
+ }
2031
2391
  }
2032
2392
  async reconcileProjectFeatureState(projectId) {
2033
2393
  const project = this.getProject(projectId);
@@ -2391,7 +2751,9 @@ class EventStore {
2391
2751
  }
2392
2752
 
2393
2753
  // src/server/agent.ts
2394
- import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
2754
+ import {
2755
+ query as query2
2756
+ } from "@anthropic-ai/claude-agent-sdk";
2395
2757
 
2396
2758
  // src/shared/tools.ts
2397
2759
  function normalizeToolCall(args) {
@@ -3169,7 +3531,7 @@ class CodexAppServerManager {
3169
3531
  }
3170
3532
  async startSession(args) {
3171
3533
  const existing = this.sessions.get(args.chatId);
3172
- if (existing && !existing.closed && existing.cwd === args.cwd) {
3534
+ if (existing && !existing.closed && existing.cwd === args.cwd && existing.sessionToken === args.sessionToken) {
3173
3535
  return;
3174
3536
  }
3175
3537
  if (existing) {
@@ -5576,7 +5938,7 @@ function codexServiceTierFromModelOptions(modelOptions) {
5576
5938
  import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, writeFileSync as writeFileSync2 } from "fs";
5577
5939
  import { homedir as homedir4 } from "os";
5578
5940
  import path7 from "path";
5579
- import process3 from "process";
5941
+ import process4 from "process";
5580
5942
 
5581
5943
  // src/server/usage/base-provider-usage.ts
5582
5944
  import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
@@ -5672,6 +6034,69 @@ function usageWarnings(args) {
5672
6034
  }
5673
6035
  return warnings;
5674
6036
  }
6037
+ function relevantMessagesForCurrentContext(messages) {
6038
+ let lastContextClearedIndex = -1;
6039
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
6040
+ if (messages[index]?.kind === "context_cleared") {
6041
+ lastContextClearedIndex = index;
6042
+ break;
6043
+ }
6044
+ }
6045
+ return lastContextClearedIndex >= 0 ? messages.slice(lastContextClearedIndex + 1) : messages;
6046
+ }
6047
+ function estimateTokenCountFromText(text) {
6048
+ if (!text)
6049
+ return 0;
6050
+ const trimmed = text.trim();
6051
+ if (!trimmed)
6052
+ return 0;
6053
+ return Math.ceil(trimmed.length / 4);
6054
+ }
6055
+ function estimateTokenCountFromUnknown(value) {
6056
+ if (typeof value === "string") {
6057
+ return estimateTokenCountFromText(value);
6058
+ }
6059
+ try {
6060
+ return estimateTokenCountFromText(JSON.stringify(value));
6061
+ } catch {
6062
+ return 0;
6063
+ }
6064
+ }
6065
+ function estimateCurrentThreadTokens(messages) {
6066
+ let total = 0;
6067
+ for (const entry of relevantMessagesForCurrentContext(messages)) {
6068
+ switch (entry.kind) {
6069
+ case "user_prompt":
6070
+ total += estimateTokenCountFromText(entry.content);
6071
+ if (entry.attachments?.length) {
6072
+ total += entry.attachments.length * 256;
6073
+ }
6074
+ break;
6075
+ case "assistant_text":
6076
+ total += estimateTokenCountFromText(entry.text);
6077
+ break;
6078
+ case "compact_summary":
6079
+ total += estimateTokenCountFromText(entry.summary);
6080
+ break;
6081
+ case "result":
6082
+ total += estimateTokenCountFromText(entry.result);
6083
+ break;
6084
+ case "tool_call":
6085
+ total += estimateTokenCountFromText(entry.tool.toolName);
6086
+ total += estimateTokenCountFromUnknown(entry.tool.input);
6087
+ break;
6088
+ case "tool_result":
6089
+ total += estimateTokenCountFromUnknown(entry.content);
6090
+ break;
6091
+ case "status":
6092
+ total += estimateTokenCountFromText(entry.status);
6093
+ break;
6094
+ default:
6095
+ break;
6096
+ }
6097
+ }
6098
+ return total;
6099
+ }
5675
6100
  function buildSnapshot(args) {
5676
6101
  const contextUsedPercent = args.threadTokens !== null && args.contextWindowTokens && args.contextWindowTokens > 0 ? toPercent(args.threadTokens / args.contextWindowTokens * 100) : null;
5677
6102
  const snapshot = {
@@ -5706,6 +6131,65 @@ function buildSnapshot(args) {
5706
6131
  ].some((value) => value !== null);
5707
6132
  return hasAnyData ? snapshot : null;
5708
6133
  }
6134
+ function usageTotals(usage) {
6135
+ const inputTokens = toNumber(usage.input_tokens) ?? 0;
6136
+ const outputTokens = toNumber(usage.output_tokens) ?? 0;
6137
+ const cachedInputTokens = (toNumber(usage.cache_read_input_tokens) ?? 0) + (toNumber(usage.cache_creation_input_tokens) ?? 0);
6138
+ const reasoningOutputTokens = toNumber(usage.reasoning_output_tokens) ?? 0;
6139
+ return {
6140
+ inputTokens,
6141
+ outputTokens,
6142
+ cachedInputTokens,
6143
+ reasoningOutputTokens,
6144
+ totalTokens: inputTokens + outputTokens + cachedInputTokens + reasoningOutputTokens
6145
+ };
6146
+ }
6147
+ function mergeUsageSnapshots(reconstructed, live) {
6148
+ if (!reconstructed)
6149
+ return live;
6150
+ if (!live)
6151
+ return reconstructed;
6152
+ const merged = {
6153
+ ...reconstructed,
6154
+ ...live,
6155
+ provider: live.provider,
6156
+ threadTokens: live.threadTokens ?? reconstructed.threadTokens,
6157
+ contextWindowTokens: live.contextWindowTokens ?? reconstructed.contextWindowTokens,
6158
+ contextUsedPercent: live.contextUsedPercent ?? reconstructed.contextUsedPercent,
6159
+ lastTurnTokens: live.lastTurnTokens ?? reconstructed.lastTurnTokens,
6160
+ inputTokens: live.inputTokens ?? reconstructed.inputTokens,
6161
+ outputTokens: live.outputTokens ?? reconstructed.outputTokens,
6162
+ cachedInputTokens: live.cachedInputTokens ?? reconstructed.cachedInputTokens,
6163
+ reasoningOutputTokens: live.reasoningOutputTokens ?? reconstructed.reasoningOutputTokens,
6164
+ sessionLimitUsedPercent: live.sessionLimitUsedPercent ?? reconstructed.sessionLimitUsedPercent,
6165
+ rateLimitResetAt: live.rateLimitResetAt ?? reconstructed.rateLimitResetAt,
6166
+ source: live.source === "live" ? "live" : reconstructed.source,
6167
+ updatedAt: live.updatedAt ?? reconstructed.updatedAt,
6168
+ warnings: []
6169
+ };
6170
+ merged.warnings = usageWarnings({
6171
+ contextUsedPercent: merged.contextUsedPercent,
6172
+ sessionLimitUsedPercent: merged.sessionLimitUsedPercent,
6173
+ updatedAt: merged.updatedAt
6174
+ });
6175
+ return merged;
6176
+ }
6177
+ function applyThreadEstimate(snapshot, messages) {
6178
+ const estimatedThreadTokens = estimateCurrentThreadTokens(messages);
6179
+ if (!snapshot)
6180
+ return null;
6181
+ const contextUsedPercent = snapshot.contextWindowTokens && snapshot.contextWindowTokens > 0 ? toPercent(estimatedThreadTokens / snapshot.contextWindowTokens * 100) : null;
6182
+ return {
6183
+ ...snapshot,
6184
+ threadTokens: estimatedThreadTokens,
6185
+ contextUsedPercent,
6186
+ warnings: usageWarnings({
6187
+ contextUsedPercent,
6188
+ sessionLimitUsedPercent: snapshot.sessionLimitUsedPercent,
6189
+ updatedAt: snapshot.updatedAt
6190
+ })
6191
+ };
6192
+ }
5709
6193
  function deriveAvailability(updatedAt) {
5710
6194
  if (updatedAt === null)
5711
6195
  return "available";
@@ -5773,6 +6257,77 @@ function unavailableEntry(provider) {
5773
6257
  // src/server/usage/claude-usage.ts
5774
6258
  var PROVIDER_CACHE_TTL_MS = 30000;
5775
6259
  var PROVIDER_USAGE_REQUEST_MIN_INTERVAL_MS = 30 * 60 * 1000;
6260
+ function extractClaudeUsageRecord(entry) {
6261
+ if (!entry.debugRaw)
6262
+ return null;
6263
+ const raw = parseJsonLine3(entry.debugRaw);
6264
+ if (!raw)
6265
+ return null;
6266
+ const type = raw.type;
6267
+ if (type !== "assistant" && type !== "result")
6268
+ return null;
6269
+ if (type === "assistant") {
6270
+ const message = asRecord3(raw.message);
6271
+ const usage2 = asRecord3(message?.usage);
6272
+ if (!usage2)
6273
+ return null;
6274
+ const usageTotalsRecord2 = usageTotals(usage2);
6275
+ return {
6276
+ key: typeof raw.uuid === "string" ? raw.uuid : entry.messageId ?? entry._id,
6277
+ updatedAt: entry.createdAt,
6278
+ totals: usageTotalsRecord2,
6279
+ contextWindowTokens: null
6280
+ };
6281
+ }
6282
+ const usage = asRecord3(raw.usage);
6283
+ const modelUsage = asRecord3(raw.modelUsage);
6284
+ const firstModel = modelUsage ? Object.values(modelUsage).map((value) => asRecord3(value)).find(Boolean) ?? null : null;
6285
+ if (!usage && !firstModel)
6286
+ return null;
6287
+ const usageTotalsRecord = usage ? usageTotals(usage) : {
6288
+ inputTokens: toNumber(firstModel?.inputTokens) ?? 0,
6289
+ outputTokens: toNumber(firstModel?.outputTokens) ?? 0,
6290
+ cachedInputTokens: (toNumber(firstModel?.cacheReadInputTokens) ?? 0) + (toNumber(firstModel?.cacheCreationInputTokens) ?? 0),
6291
+ reasoningOutputTokens: 0,
6292
+ totalTokens: (toNumber(firstModel?.inputTokens) ?? 0) + (toNumber(firstModel?.outputTokens) ?? 0) + (toNumber(firstModel?.cacheReadInputTokens) ?? 0) + (toNumber(firstModel?.cacheCreationInputTokens) ?? 0)
6293
+ };
6294
+ return {
6295
+ key: typeof raw.uuid === "string" ? raw.uuid : entry.messageId ?? entry._id,
6296
+ updatedAt: entry.createdAt,
6297
+ totals: usageTotalsRecord,
6298
+ contextWindowTokens: toNumber(firstModel?.contextWindow)
6299
+ };
6300
+ }
6301
+ function reconstructClaudeUsage(messages, liveRateLimit) {
6302
+ const relevantMessages = relevantMessagesForCurrentContext(messages);
6303
+ const deduped = new Map;
6304
+ for (const entry of relevantMessages) {
6305
+ const usageRecord = extractClaudeUsageRecord(entry);
6306
+ if (!usageRecord)
6307
+ continue;
6308
+ deduped.set(usageRecord.key, usageRecord);
6309
+ }
6310
+ const latest = [...deduped.values()].filter((value) => Boolean(value)).sort((a, b) => b.updatedAt - a.updatedAt)[0];
6311
+ if (!latest && liveRateLimit?.percent == null) {
6312
+ return null;
6313
+ }
6314
+ const latestModel = [...relevantMessages].reverse().find((entry) => entry.kind === "system_init" && entry.provider === "claude");
6315
+ const fallbackContextWindow = latestModel ? CLAUDE_CONTEXT_WINDOW_FALLBACKS[latestModel.model.toLowerCase()] ?? null : null;
6316
+ return buildSnapshot({
6317
+ provider: "claude",
6318
+ threadTokens: estimateCurrentThreadTokens(messages),
6319
+ contextWindowTokens: latest?.contextWindowTokens ?? fallbackContextWindow,
6320
+ lastTurnTokens: latest?.totals.totalTokens ?? null,
6321
+ inputTokens: latest?.totals.inputTokens ?? null,
6322
+ outputTokens: latest?.totals.outputTokens ?? null,
6323
+ cachedInputTokens: latest?.totals.cachedInputTokens ?? null,
6324
+ reasoningOutputTokens: latest?.totals.reasoningOutputTokens ?? null,
6325
+ sessionLimitUsedPercent: liveRateLimit?.percent ?? null,
6326
+ rateLimitResetAt: liveRateLimit?.resetsAt ?? null,
6327
+ source: latest ? "reconstructed" : "live",
6328
+ updatedAt: latest?.updatedAt ?? null
6329
+ });
6330
+ }
5776
6331
  function createClaudeRateLimitSnapshot(percent, resetsAt) {
5777
6332
  return buildSnapshot({
5778
6333
  provider: "claude",
@@ -5915,7 +6470,7 @@ function collectClaudeUsageScreen(args) {
5915
6470
  stderr: "pipe",
5916
6471
  cwd: args?.cwd,
5917
6472
  env: {
5918
- ...process3.env,
6473
+ ...process4.env,
5919
6474
  ...args?.cwd ? { CLAUDE_USAGE_CWD: args.cwd } : {},
5920
6475
  CLAUDE_USAGE_CONTINUE: args?.continueSession ? "1" : "0"
5921
6476
  }
@@ -5924,7 +6479,7 @@ function collectClaudeUsageScreen(args) {
5924
6479
  }
5925
6480
  function isRunningPid(pid) {
5926
6481
  try {
5927
- process3.kill(pid, 0);
6482
+ process4.kill(pid, 0);
5928
6483
  return true;
5929
6484
  } catch {
5930
6485
  return false;
@@ -6272,7 +6827,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync
6272
6827
  import { mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
6273
6828
  import { tmpdir as tmpdir3 } from "os";
6274
6829
  import path10 from "path";
6275
- import process5 from "process";
6830
+ import process6 from "process";
6276
6831
  import puppeteer from "puppeteer-core";
6277
6832
 
6278
6833
  // src/server/usage/cursor-cookies.ts
@@ -6281,7 +6836,7 @@ import { copyFileSync, existsSync as existsSync5, mkdtempSync, readdirSync as re
6281
6836
  import { createDecipheriv, pbkdf2Sync } from "crypto";
6282
6837
  import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
6283
6838
  import path9 from "path";
6284
- import process4 from "process";
6839
+ import process5 from "process";
6285
6840
  var CHROME_EPOCH_OFFSET_MS = Date.UTC(1601, 0, 1);
6286
6841
  var CURSOR_SESSION_COOKIE_NAME = "WorkosCursorSessionToken";
6287
6842
  function normalizeCookieDomain(domain) {
@@ -6578,7 +7133,7 @@ function decryptChromiumCookieValue(encryptedValue, key, platform) {
6578
7133
  return null;
6579
7134
  }
6580
7135
  }
6581
- function bootstrapCursorSessionFromBrowser(platform = process4.platform) {
7136
+ function bootstrapCursorSessionFromBrowser(platform = process5.platform) {
6582
7137
  if (platform !== "linux" && platform !== "darwin")
6583
7138
  return null;
6584
7139
  for (const source of discoverChromiumCookieSources(platform)) {
@@ -6912,7 +7467,7 @@ class CursorUsage extends BaseProviderUsage {
6912
7467
  return null;
6913
7468
  }
6914
7469
  }
6915
- async refresh(platform = process5.platform, force = false) {
7470
+ async refresh(platform = process6.platform, force = false) {
6916
7471
  const now = Date.now();
6917
7472
  const persistedEntry = this.loadPersistedEntry();
6918
7473
  const lastRequestedAt = persistedEntry?.lastRequestedAt ?? this.readLastRequestedAt(this.provider);
@@ -6977,7 +7532,7 @@ class CursorUsage extends BaseProviderUsage {
6977
7532
  this.persistUsageEntry(entry);
6978
7533
  return entry;
6979
7534
  }
6980
- async importFromCurl(curlCommand, platform = process5.platform) {
7535
+ async importFromCurl(curlCommand, platform = process6.platform) {
6981
7536
  const imported = importCursorSessionFromCurl(curlCommand);
6982
7537
  if (!imported) {
6983
7538
  return cursorStatusEntry({
@@ -6995,7 +7550,7 @@ class CursorUsage extends BaseProviderUsage {
6995
7550
  this.persistSession(session);
6996
7551
  return this.refresh(platform);
6997
7552
  }
6998
- async signInWithBrowser(platform = process5.platform) {
7553
+ async signInWithBrowser(platform = process6.platform) {
6999
7554
  const executablePath = resolveBrowserExecutable(platform);
7000
7555
  if (!executablePath) {
7001
7556
  return cursorStatusEntry({
@@ -7068,13 +7623,13 @@ function getCursorUsage(dataDir) {
7068
7623
  }
7069
7624
  return _instances3.get(dataDir);
7070
7625
  }
7071
- async function refreshCursorUsage(dataDir, platform = process5.platform, force = false) {
7626
+ async function refreshCursorUsage(dataDir, platform = process6.platform, force = false) {
7072
7627
  return getCursorUsage(dataDir).refresh(platform, force);
7073
7628
  }
7074
- async function importCursorUsageFromCurl(dataDir, curlCommand, platform = process5.platform) {
7629
+ async function importCursorUsageFromCurl(dataDir, curlCommand, platform = process6.platform) {
7075
7630
  return getCursorUsage(dataDir).importFromCurl(curlCommand, platform);
7076
7631
  }
7077
- async function signInToCursorWithBrowser(dataDir, platform = process5.platform) {
7632
+ async function signInToCursorWithBrowser(dataDir, platform = process6.platform) {
7078
7633
  return getCursorUsage(dataDir).signInWithBrowser(platform);
7079
7634
  }
7080
7635
 
@@ -7222,10 +7777,21 @@ function formatRecoveredAskUserQuestionFollowUp(tool, result) {
7222
7777
  ].join(`
7223
7778
  `);
7224
7779
  }
7225
- function planModeFollowUp(result) {
7780
+ function planModeFollowUp(result, plan) {
7226
7781
  let text = "";
7227
7782
  if (result.confirmed) {
7228
- text = result.message ? `Proceed with the approved plan. Additional guidance: ${result.message}` : "Proceed with the approved plan.";
7783
+ const trimmedPlan = plan?.trim();
7784
+ const sections = [
7785
+ trimmedPlan ? "Proceed with the approved plan below." : "Proceed with the approved plan."
7786
+ ];
7787
+ if (trimmedPlan) {
7788
+ sections.push("", "Approved plan:", trimmedPlan);
7789
+ }
7790
+ if (result.message) {
7791
+ sections.push("", `Additional guidance: ${result.message}`);
7792
+ }
7793
+ text = sections.join(`
7794
+ `);
7229
7795
  } else {
7230
7796
  text = result.message ? `Revise the plan using this feedback: ${result.message}` : "Revise the plan using this feedback.";
7231
7797
  }
@@ -7321,7 +7887,14 @@ function normalizeClaudeStreamMessage(message) {
7321
7887
  ];
7322
7888
  }
7323
7889
  if (message.type === "system" && message.subtype === "status" && typeof message.status === "string") {
7324
- return [timestamped3({ kind: "status", messageId, status: message.status, debugRaw })];
7890
+ return [
7891
+ timestamped3({
7892
+ kind: "status",
7893
+ messageId,
7894
+ status: message.status,
7895
+ debugRaw
7896
+ })
7897
+ ];
7325
7898
  }
7326
7899
  if (message.type === "system" && message.subtype === "compact_boundary") {
7327
7900
  return [timestamped3({ kind: "compact_boundary", messageId, debugRaw })];
@@ -7330,7 +7903,14 @@ function normalizeClaudeStreamMessage(message) {
7330
7903
  return [timestamped3({ kind: "context_cleared", messageId, debugRaw })];
7331
7904
  }
7332
7905
  if (message.type === "user" && message.message?.role === "user" && typeof message.message.content === "string" && message.message.content.startsWith("This session is being continued")) {
7333
- return [timestamped3({ kind: "compact_summary", messageId, summary: message.message.content, debugRaw })];
7906
+ return [
7907
+ timestamped3({
7908
+ kind: "compact_summary",
7909
+ messageId,
7910
+ summary: message.message.content,
7911
+ debugRaw
7912
+ })
7913
+ ];
7334
7914
  }
7335
7915
  return [];
7336
7916
  }
@@ -7968,7 +8548,7 @@ class AgentCoordinator {
7968
8548
  await this.store.setSessionToken(command.chatId, null);
7969
8549
  await this.store.appendMessage(command.chatId, timestamped3({ kind: "context_cleared" }));
7970
8550
  }
7971
- const followUp = planModeFollowUp(result);
8551
+ const followUp = planModeFollowUp(result, recoveredPending.input.plan);
7972
8552
  const messageEntry = timestamped3({
7973
8553
  kind: "user_prompt",
7974
8554
  content: followUp.content
@@ -8016,7 +8596,7 @@ class AgentCoordinator {
8016
8596
  await this.store.appendMessage(command.chatId, timestamped3({ kind: "context_cleared" }));
8017
8597
  }
8018
8598
  if (shouldUseSyntheticPlanFollowUp(active.provider, pending.tool)) {
8019
- active.postToolFollowUp = planModeFollowUp(result);
8599
+ active.postToolFollowUp = planModeFollowUp(result, pending.tool.input.plan);
8020
8600
  }
8021
8601
  }
8022
8602
  pending.resolve(command.result);
@@ -9702,17 +10282,17 @@ function formatDisplayPath3(filePath) {
9702
10282
 
9703
10283
  // src/server/machine-name.ts
9704
10284
  import { hostname } from "os";
9705
- import process6 from "process";
9706
- import { spawnSync as spawnSync3 } from "child_process";
10285
+ import process7 from "process";
10286
+ import { spawnSync as spawnSync4 } from "child_process";
9707
10287
  function runAndRead(command, args) {
9708
- const result = spawnSync3(command, args, { encoding: "utf8" });
10288
+ const result = spawnSync4(command, args, { encoding: "utf8" });
9709
10289
  if (result.status !== 0)
9710
10290
  return null;
9711
10291
  const value = result.stdout.trim();
9712
10292
  return value || null;
9713
10293
  }
9714
10294
  function getMachineDisplayName() {
9715
- if (process6.platform === "darwin") {
10295
+ if (process7.platform === "darwin") {
9716
10296
  const computerName = runAndRead("scutil", ["--get", "ComputerName"]);
9717
10297
  if (computerName) {
9718
10298
  return computerName;
@@ -9724,15 +10304,15 @@ function getMachineDisplayName() {
9724
10304
 
9725
10305
  // src/server/terminal-manager.ts
9726
10306
  import path16 from "path";
9727
- import process7 from "process";
10307
+ import process8 from "process";
9728
10308
  import defaultShell, { detectDefaultShell } from "default-shell";
9729
10309
  import { Terminal } from "@xterm/headless";
9730
10310
  import { SerializeAddon } from "@xterm/addon-serialize";
9731
10311
  var DEFAULT_COLS = 80;
9732
10312
  var DEFAULT_ROWS = 24;
9733
- var DEFAULT_SCROLLBACK = 1000;
10313
+ var DEFAULT_SCROLLBACK = 1e4;
9734
10314
  var MIN_SCROLLBACK = 500;
9735
- var MAX_SCROLLBACK = 5000;
10315
+ var MAX_SCROLLBACK = 50000;
9736
10316
  var FOCUS_IN_SEQUENCE = "\x1B[I";
9737
10317
  var FOCUS_OUT_SEQUENCE = "\x1B[O";
9738
10318
  var MODE_SEQUENCE_TAIL_LENGTH = 16;
@@ -9752,14 +10332,14 @@ function resolveShell() {
9752
10332
  } catch {
9753
10333
  if (defaultShell)
9754
10334
  return defaultShell;
9755
- if (process7.platform === "win32") {
9756
- return process7.env.ComSpec || "cmd.exe";
10335
+ if (process8.platform === "win32") {
10336
+ return process8.env.ComSpec || "cmd.exe";
9757
10337
  }
9758
- return process7.env.SHELL || "/bin/sh";
10338
+ return process8.env.SHELL || "/bin/sh";
9759
10339
  }
9760
10340
  }
9761
10341
  function resolveShellArgs(shellPath) {
9762
- if (process7.platform === "win32") {
10342
+ if (process8.platform === "win32") {
9763
10343
  return [];
9764
10344
  }
9765
10345
  const shellName = path16.basename(shellPath);
@@ -9770,7 +10350,7 @@ function resolveShellArgs(shellPath) {
9770
10350
  }
9771
10351
  function createTerminalEnv() {
9772
10352
  return {
9773
- ...process7.env,
10353
+ ...process8.env,
9774
10354
  TERM: "xterm-256color",
9775
10355
  COLORTERM: "truecolor"
9776
10356
  };
@@ -9795,9 +10375,9 @@ function killTerminalProcessTree(subprocess) {
9795
10375
  const pid = subprocess.pid;
9796
10376
  if (typeof pid !== "number")
9797
10377
  return;
9798
- if (process7.platform !== "win32") {
10378
+ if (process8.platform !== "win32") {
9799
10379
  try {
9800
- process7.kill(-pid, "SIGKILL");
10380
+ process8.kill(-pid, "SIGKILL");
9801
10381
  return;
9802
10382
  } catch {}
9803
10383
  }
@@ -9811,9 +10391,9 @@ function signalTerminalProcessGroup(subprocess, signal) {
9811
10391
  const pid = subprocess.pid;
9812
10392
  if (typeof pid !== "number")
9813
10393
  return false;
9814
- if (process7.platform !== "win32") {
10394
+ if (process8.platform !== "win32") {
9815
10395
  try {
9816
- process7.kill(-pid, signal);
10396
+ process8.kill(-pid, signal);
9817
10397
  return true;
9818
10398
  } catch {}
9819
10399
  }
@@ -9835,7 +10415,7 @@ class TerminalManager {
9835
10415
  };
9836
10416
  }
9837
10417
  createTerminal(args) {
9838
- if (process7.platform === "win32") {
10418
+ if (process8.platform === "win32") {
9839
10419
  throw new Error("Embedded terminal is currently supported on macOS/Linux only.");
9840
10420
  }
9841
10421
  if (typeof Bun.Terminal !== "function") {
@@ -9857,7 +10437,12 @@ class TerminalManager {
9857
10437
  const rows = normalizeTerminalDimension(args.rows, DEFAULT_ROWS);
9858
10438
  const scrollback = clampScrollback(args.scrollback);
9859
10439
  const title = path16.basename(shell) || "shell";
9860
- const headless = new Terminal({ cols, rows, scrollback, allowProposedApi: true });
10440
+ const headless = new Terminal({
10441
+ cols,
10442
+ rows,
10443
+ scrollback,
10444
+ allowProposedApi: true
10445
+ });
9861
10446
  const serializeAddon = new SerializeAddon;
9862
10447
  headless.loadAddon(serializeAddon);
9863
10448
  const session = {
@@ -10002,7 +10587,9 @@ class TerminalManager {
10002
10587
  cols: session.cols,
10003
10588
  rows: session.rows,
10004
10589
  scrollback: session.scrollback,
10005
- serializedState: session.serializeAddon.serialize({ scrollback: session.scrollback }),
10590
+ serializedState: session.serializeAddon.serialize({
10591
+ scrollback: session.scrollback
10592
+ }),
10006
10593
  status: session.status,
10007
10594
  exitCode: session.exitCode
10008
10595
  };
@@ -10605,14 +11192,14 @@ function isClientEnvelope(value) {
10605
11192
  // src/server/external-open.ts
10606
11193
  import { stat as stat2 } from "fs/promises";
10607
11194
  import path18 from "path";
10608
- import process8 from "process";
11195
+ import process9 from "process";
10609
11196
  var DEFAULT_EDITOR_SETTINGS = {
10610
11197
  preset: "cursor",
10611
11198
  commandTemplate: "cursor {path}"
10612
11199
  };
10613
11200
  async function openExternal(command) {
10614
11201
  const resolvedPath = resolveLocalPath(command.localPath);
10615
- const platform = process8.platform;
11202
+ const platform = process9.platform;
10616
11203
  const info = command.action === "open_editor" || command.action === "open_finder" ? await stat2(resolvedPath).catch(() => null) : null;
10617
11204
  if (command.action === "open_editor") {
10618
11205
  if (!info) {
@@ -10682,7 +11269,7 @@ async function openExternal(command) {
10682
11269
  }
10683
11270
  }
10684
11271
  async function openUrl2(command) {
10685
- const platform = process8.platform;
11272
+ const platform = process9.platform;
10686
11273
  if (platform === "darwin") {
10687
11274
  spawnDetached("open", [command.url]);
10688
11275
  return;
@@ -11347,6 +11934,25 @@ function deriveChatSnapshot(state, activeStatuses, chatId, getMessages, pendingT
11347
11934
  };
11348
11935
  }
11349
11936
 
11937
+ // src/server/usage/chat-usage.ts
11938
+ function resolveChatUsage({
11939
+ chat,
11940
+ messages,
11941
+ liveUsage,
11942
+ codexSessionsDir
11943
+ }) {
11944
+ if (!chat?.provider)
11945
+ return liveUsage;
11946
+ if (chat.provider === "claude") {
11947
+ return mergeUsageSnapshots(reconstructClaudeUsage(messages), liveUsage);
11948
+ }
11949
+ if (chat.provider === "codex") {
11950
+ const reconstructed = chat.sessionToken ? applyThreadEstimate(reconstructCodexUsageFromFile(chat.sessionToken, codexSessionsDir), messages) : null;
11951
+ return mergeUsageSnapshots(reconstructed, liveUsage);
11952
+ }
11953
+ return liveUsage;
11954
+ }
11955
+
11350
11956
  // src/server/ws-router.ts
11351
11957
  var PROVIDER_USAGE_POLL_INTERVAL_MS = 30 * 60 * 1000;
11352
11958
  var PROVIDER_USAGE_POLL_MAX_INTERVAL_MS = 31 * 60 * 1000;
@@ -11375,7 +11981,8 @@ function createWsRouter({
11375
11981
  await refreshCursorUsage(store.dataDir, undefined, force).then(() => {});
11376
11982
  }
11377
11983
  },
11378
- openUrlCommand = openUrl2
11984
+ openUrlCommand = openUrl2,
11985
+ resolveChatUsageForSnapshot = resolveChatUsage
11379
11986
  }) {
11380
11987
  const sockets = new Set;
11381
11988
  let providerUsageRefreshInFlight = null;
@@ -11490,7 +12097,13 @@ function createWsRouter({
11490
12097
  snapshot: {
11491
12098
  type: "chat",
11492
12099
  data: (() => {
11493
- return deriveChatSnapshot(store.state, agent.getActiveStatuses(), topic.chatId, (chatId) => store.getMessages(chatId), agent.getChatPendingTool(topic.chatId), agent.getLiveUsage(topic.chatId), providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS);
12100
+ const chat = store.getChat(topic.chatId);
12101
+ const messages = store.getMessages(topic.chatId);
12102
+ return deriveChatSnapshot(store.state, agent.getActiveStatuses(), topic.chatId, () => messages, agent.getChatPendingTool(topic.chatId), resolveChatUsageForSnapshot({
12103
+ chat,
12104
+ messages,
12105
+ liveUsage: agent.getLiveUsage(topic.chatId)
12106
+ }), providerSettings?.getSnapshot().settings ?? DEFAULT_PROVIDER_SETTINGS);
11494
12107
  })()
11495
12108
  }
11496
12109
  };
@@ -12319,12 +12932,14 @@ async function serveStatic(distDir, pathname) {
12319
12932
 
12320
12933
  // src/server/cli.ts
12321
12934
  var packageRootUrl = new URL("../../", import.meta.url);
12935
+ var packageRootPath = fileURLToPath(packageRootUrl);
12322
12936
  var pkg = await Bun.file(new URL("package.json", packageRootUrl)).json();
12323
12937
  var VERSION = pkg.version ?? "0.0.0";
12324
- var ALLOW_SELF_UPDATE = !existsSync10(new URL(".git", packageRootUrl));
12325
- var argv = process9.argv.slice(2);
12938
+ var IS_NPX_RUNTIME = isNpxRuntimePath(packageRootPath);
12939
+ var ALLOW_SELF_UPDATE = !existsSync10(new URL(".git", packageRootUrl)) && !IS_NPX_RUNTIME;
12940
+ var argv = process10.argv.slice(2);
12326
12941
  var resolveExitAction = null;
12327
- var WEB_ONLY_MODE = shouldRunWebOnlyMode(process9.env[CLI_WEB_ONLY_MODE_ENV_VAR]);
12942
+ var WEB_ONLY_MODE = shouldRunWebOnlyMode(process10.env[CLI_WEB_ONLY_MODE_ENV_VAR]);
12328
12943
  var result = await runCli(argv, {
12329
12944
  version: VERSION,
12330
12945
  bunVersion: Bun.version,
@@ -12345,27 +12960,29 @@ var result = await runCli(argv, {
12345
12960
  return started;
12346
12961
  },
12347
12962
  fetchLatestVersion: fetchLatestPackageVersion,
12348
- installVersion: installPackageVersion,
12963
+ installVersion: IS_NPX_RUNTIME ? () => createUnsupportedNpxInstallResult() : installPackageVersion,
12349
12964
  openUrl,
12965
+ resolveRemoteAccessInfo,
12966
+ renderTerminalQrCode,
12350
12967
  log: console.log,
12351
12968
  warn: console.warn
12352
12969
  });
12353
12970
  if (result.kind === "exited") {
12354
- process9.exit(result.code);
12971
+ process10.exit(result.code);
12355
12972
  }
12356
12973
  if (result.kind === "restarting") {
12357
- process9.exit(result.reason === "startup_update" ? CLI_STARTUP_UPDATE_RESTART_EXIT_CODE : CLI_UI_UPDATE_RESTART_EXIT_CODE);
12974
+ process10.exit(result.reason === "startup_update" ? CLI_STARTUP_UPDATE_RESTART_EXIT_CODE : CLI_UI_UPDATE_RESTART_EXIT_CODE);
12358
12975
  }
12359
12976
  var exitAction = await new Promise((resolve2) => {
12360
12977
  resolveExitAction = resolve2;
12361
12978
  const shutdown = () => {
12362
12979
  resolve2("exit");
12363
12980
  };
12364
- process9.once("SIGINT", shutdown);
12365
- process9.once("SIGTERM", shutdown);
12981
+ process10.once("SIGINT", shutdown);
12982
+ process10.once("SIGTERM", shutdown);
12366
12983
  });
12367
12984
  await result.stop();
12368
12985
  if (exitAction === "ui_restart") {
12369
12986
  console.log(`${LOG_PREFIX} current process stopped, handing restart back to supervisor`);
12370
12987
  }
12371
- process9.exit(exitAction === "ui_restart" ? CLI_UI_UPDATE_RESTART_EXIT_CODE : 0);
12988
+ process10.exit(exitAction === "ui_restart" ? CLI_UI_UPDATE_RESTART_EXIT_CODE : 0);