@threadbase-sh/streamer 1.39.1 → 1.40.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.
package/dist/index.js CHANGED
@@ -3017,7 +3017,7 @@ import { randomUUID as randomUUID5 } from "crypto";
3017
3017
  import { EventEmitter } from "events";
3018
3018
  import {
3019
3019
  createReadStream,
3020
- existsSync as existsSync10,
3020
+ existsSync as existsSync11,
3021
3021
  watch as fsWatch,
3022
3022
  readdirSync as readdirSync6,
3023
3023
  readFileSync as readFileSync8,
@@ -3025,7 +3025,7 @@ import {
3025
3025
  } from "fs";
3026
3026
  import { realpath as realpath2 } from "fs/promises";
3027
3027
  import { createServer } from "http";
3028
- import { homedir as homedir9, hostname as hostname2 } from "os";
3028
+ import { homedir as homedir9, hostname as hostname3 } from "os";
3029
3029
  import { basename as basename5, dirname as dirname9, join as join18 } from "path";
3030
3030
  import { createInterface } from "readline";
3031
3031
 
@@ -3285,7 +3285,7 @@ async function handleStartAgentSession(body, deps) {
3285
3285
  }
3286
3286
 
3287
3287
  // src/api/app.ts
3288
- import { Hono as Hono16 } from "hono";
3288
+ import { Hono as Hono18 } from "hono";
3289
3289
 
3290
3290
  // src/db/repositories/devices.repository.ts
3291
3291
  import { createHash, randomBytes as randomBytes2, randomUUID as randomUUID3, timingSafeEqual as timingSafeEqual2 } from "crypto";
@@ -3605,12 +3605,222 @@ var errorMiddleware = (err, c) => {
3605
3605
  return c.json({ error: message }, 500);
3606
3606
  };
3607
3607
 
3608
- // src/api/routes/browse.routes.ts
3608
+ // src/api/routes/backup.routes.ts
3609
3609
  import { Hono as Hono2 } from "hono";
3610
+ import { hostname } from "os";
3611
+
3612
+ // src/services/backup/backup.ts
3613
+ var BACKUP_FORMAT_VERSION = 1;
3614
+ var BackupError = class extends Error {
3615
+ constructor(message, code) {
3616
+ super(message);
3617
+ this.code = code;
3618
+ }
3619
+ code;
3620
+ };
3621
+ function validateArchive(input) {
3622
+ if (!input || typeof input !== "object") {
3623
+ throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
3624
+ }
3625
+ const archive = input;
3626
+ const manifest = archive.manifest;
3627
+ if (!manifest || typeof manifest !== "object") {
3628
+ throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
3629
+ }
3630
+ if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
3631
+ throw new BackupError(
3632
+ `Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
3633
+ "UNSUPPORTED_VERSION"
3634
+ );
3635
+ }
3636
+ if (!Array.isArray(archive.projects)) {
3637
+ throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
3638
+ }
3639
+ for (const [i, p] of archive.projects.entries()) {
3640
+ if (!p || typeof p !== "object") {
3641
+ throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
3642
+ }
3643
+ if (typeof p.id !== "string" || p.id.length === 0) {
3644
+ throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
3645
+ }
3646
+ if (typeof p.path !== "string" || p.path.length === 0) {
3647
+ throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
3648
+ }
3649
+ }
3650
+ const ids = new Set(archive.projects.map((p) => p.id));
3651
+ if (ids.size !== archive.projects.length) {
3652
+ throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
3653
+ }
3654
+ return archive;
3655
+ }
3656
+ function remapPaths(projects, rules) {
3657
+ const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
3658
+ return projects.map((p) => {
3659
+ const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
3660
+ if (!rule) return p;
3661
+ return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
3662
+ });
3663
+ }
3664
+ function planRestore(incoming, existing) {
3665
+ const byId = new Map(existing.map((e) => [e.id, e]));
3666
+ const byPath = new Map(existing.map((e) => [e.path, e]));
3667
+ const plan = { create: [], update: [], conflict: [] };
3668
+ for (const p of incoming) {
3669
+ const sameId = byId.get(p.id);
3670
+ if (sameId) {
3671
+ if (sameId.path !== p.path) plan.update.push(p);
3672
+ continue;
3673
+ }
3674
+ const samePath = byPath.get(p.path);
3675
+ if (samePath) {
3676
+ plan.conflict.push({ incoming: p, existingId: samePath.id });
3677
+ continue;
3678
+ }
3679
+ plan.create.push(p);
3680
+ }
3681
+ return plan;
3682
+ }
3683
+
3684
+ // src/version.ts
3685
+ import { readFileSync as readFileSync4, realpathSync } from "fs";
3686
+ import { dirname as dirname5, join as join7 } from "path";
3687
+ var cached;
3688
+ function getVersion() {
3689
+ if (cached !== void 0) return cached;
3690
+ cached = resolveVersion();
3691
+ return cached;
3692
+ }
3693
+ function resolveVersion() {
3694
+ const scriptPath = process.argv[1] ?? "";
3695
+ const here = scriptPath ? dirname5(scriptPath) : process.cwd();
3696
+ let realHere = here;
3697
+ try {
3698
+ realHere = dirname5(realpathSync(scriptPath));
3699
+ } catch {
3700
+ }
3701
+ const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
3702
+ for (const dir of searchDirs) {
3703
+ try {
3704
+ const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
3705
+ if (v) return v;
3706
+ } catch {
3707
+ }
3708
+ }
3709
+ try {
3710
+ const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
3711
+ if (pkg.version) return `${pkg.version}+source`;
3712
+ } catch {
3713
+ }
3714
+ return "0.0.0+unknown";
3715
+ }
3716
+
3717
+ // src/api/routes/backup.routes.ts
3718
+ function readBody(c) {
3719
+ return new Promise((resolve2, reject) => {
3720
+ const chunks = [];
3721
+ c.env.incoming.on("data", (chunk) => chunks.push(chunk));
3722
+ c.env.incoming.on("end", () => {
3723
+ try {
3724
+ const raw = Buffer.concat(chunks).toString("utf-8");
3725
+ resolve2(raw ? JSON.parse(raw) : {});
3726
+ } catch {
3727
+ reject(new Error("Invalid JSON body"));
3728
+ }
3729
+ });
3730
+ c.env.incoming.on("error", reject);
3731
+ });
3732
+ }
3733
+ var createBackupRoutes = (deps) => {
3734
+ const app = new Hono2();
3735
+ app.get("/export", (c) => {
3736
+ const repo = deps.projectsRepo();
3737
+ if (!repo) {
3738
+ return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
3739
+ }
3740
+ const projects = repo.listProjects().map((p) => ({
3741
+ id: p.id,
3742
+ path: p.path,
3743
+ name: p.name ?? null,
3744
+ createdAt: p.createdAt,
3745
+ updatedAt: p.updatedAt
3746
+ }));
3747
+ return c.json({
3748
+ manifest: {
3749
+ formatVersion: BACKUP_FORMAT_VERSION,
3750
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3751
+ streamerVersion: getVersion(),
3752
+ sourceHost: hostname(),
3753
+ // No endpoint here exports the API key. The flag is recorded so an
3754
+ // archive is self-describing about its own sensitivity rather than
3755
+ // requiring a reader to infer it.
3756
+ includesSecrets: false,
3757
+ counts: { projects: projects.length }
3758
+ },
3759
+ projects
3760
+ });
3761
+ });
3762
+ app.post("/restore", async (c) => {
3763
+ const repo = deps.projectsRepo();
3764
+ if (!repo) {
3765
+ return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
3766
+ }
3767
+ let body;
3768
+ try {
3769
+ body = await readBody(c);
3770
+ } catch {
3771
+ return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
3772
+ }
3773
+ let archive;
3774
+ try {
3775
+ archive = validateArchive(body.archive);
3776
+ } catch (err) {
3777
+ if (err instanceof BackupError) {
3778
+ return c.json({ error: err.message, code: err.code }, 400);
3779
+ }
3780
+ throw err;
3781
+ }
3782
+ const rules = Array.isArray(body.pathMap) ? body.pathMap.filter((r) => typeof r?.from === "string" && typeof r?.to === "string").map((r) => ({ from: r.from, to: r.to })) : [];
3783
+ const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
3784
+ const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
3785
+ const plan = planRestore(incoming, existing);
3786
+ const summary = {
3787
+ create: plan.create.length,
3788
+ update: plan.update.length,
3789
+ conflict: plan.conflict.length
3790
+ };
3791
+ if (body.apply !== true) {
3792
+ return c.json({ applied: false, summary, plan });
3793
+ }
3794
+ if (plan.conflict.length > 0) {
3795
+ return c.json(
3796
+ {
3797
+ error: "Restore has unresolved conflicts",
3798
+ code: "RESTORE_CONFLICT",
3799
+ summary,
3800
+ plan
3801
+ },
3802
+ 409
3803
+ );
3804
+ }
3805
+ let applied = 0;
3806
+ for (const p of [...plan.create, ...plan.update]) {
3807
+ try {
3808
+ repo.upsertProjectByPath(p.path, { name: p.name });
3809
+ applied++;
3810
+ } catch {
3811
+ }
3812
+ }
3813
+ return c.json({ applied: true, summary, appliedCount: applied });
3814
+ });
3815
+ return app;
3816
+ };
3817
+
3818
+ // src/api/routes/browse.routes.ts
3819
+ import { Hono as Hono3 } from "hono";
3610
3820
  var ALREADY_HANDLED = 597;
3611
3821
  var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
3612
3822
  var createBrowseRoutes = (deps) => {
3613
- const app = new Hono2();
3823
+ const app = new Hono3();
3614
3824
  app.get("/browse", async (c) => {
3615
3825
  const url = new URL(c.req.url);
3616
3826
  await deps.handleBrowse(url, c.env.outgoing);
@@ -3624,7 +3834,7 @@ var createBrowseRoutes = (deps) => {
3624
3834
  };
3625
3835
 
3626
3836
  // src/api/routes/cacheAlert.routes.ts
3627
- import { Hono as Hono3 } from "hono";
3837
+ import { Hono as Hono4 } from "hono";
3628
3838
 
3629
3839
  // src/schemas/cacheAlert.schema.ts
3630
3840
  import { z } from "zod";
@@ -3647,7 +3857,7 @@ function readRawBody2(req) {
3647
3857
  });
3648
3858
  }
3649
3859
  var createCacheAlertRoutes = (deps) => {
3650
- const app = new Hono3();
3860
+ const app = new Hono4();
3651
3861
  app.get("/", (c) => {
3652
3862
  const monitor = deps.cacheMonitor();
3653
3863
  return c.json({ pending: monitor?.pending ?? null });
@@ -3684,7 +3894,7 @@ var createCacheAlertRoutes = (deps) => {
3684
3894
  };
3685
3895
 
3686
3896
  // src/api/routes/config.routes.ts
3687
- import { Hono as Hono4 } from "hono";
3897
+ import { Hono as Hono5 } from "hono";
3688
3898
 
3689
3899
  // src/schemas/claudeFlags.schema.ts
3690
3900
  import { z as z2 } from "zod";
@@ -3705,7 +3915,7 @@ function readRawBody3(req) {
3705
3915
  });
3706
3916
  }
3707
3917
  var createConfigRoutes = (deps) => {
3708
- const app = new Hono4();
3918
+ const app = new Hono5();
3709
3919
  app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
3710
3920
  app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
3711
3921
  app.put("/claude-flags", async (c) => {
@@ -3740,11 +3950,11 @@ var createConfigRoutes = (deps) => {
3740
3950
  };
3741
3951
 
3742
3952
  // src/api/routes/conversations.routes.ts
3743
- import { Hono as Hono5 } from "hono";
3953
+ import { Hono as Hono6 } from "hono";
3744
3954
  var ALREADY_HANDLED2 = 597;
3745
3955
  var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
3746
3956
  var createConversationRoutes = (deps) => {
3747
- const app = new Hono5();
3957
+ const app = new Hono6();
3748
3958
  app.get("/count", async (c) => {
3749
3959
  const url = new URL(c.req.url);
3750
3960
  await deps.handleConversationsCount(url, c.env.outgoing);
@@ -3771,9 +3981,9 @@ var createConversationRoutes = (deps) => {
3771
3981
  };
3772
3982
 
3773
3983
  // src/api/routes/devices.routes.ts
3774
- import { Hono as Hono6 } from "hono";
3984
+ import { Hono as Hono7 } from "hono";
3775
3985
  var createDeviceRoutes = (deps) => {
3776
- const app = new Hono6();
3986
+ const app = new Hono7();
3777
3987
  app.get("/", (c) => {
3778
3988
  const repo = deps.devicesRepo();
3779
3989
  if (!repo) return c.json({ devices: [], available: false });
@@ -3796,45 +4006,134 @@ var createDeviceRoutes = (deps) => {
3796
4006
  return app;
3797
4007
  };
3798
4008
 
3799
- // src/api/routes/health.routes.ts
3800
- import { Hono as Hono7 } from "hono";
4009
+ // src/api/routes/diagnostics.routes.ts
4010
+ import { existsSync as existsSync5 } from "fs";
4011
+ import { Hono as Hono8 } from "hono";
3801
4012
 
3802
- // src/version.ts
3803
- import { readFileSync as readFileSync4, realpathSync } from "fs";
3804
- import { dirname as dirname5, join as join7 } from "path";
3805
- var cached;
3806
- function getVersion() {
3807
- if (cached !== void 0) return cached;
3808
- cached = resolveVersion();
3809
- return cached;
4013
+ // src/services/diagnostics/diagnostics.ts
4014
+ var DIAGNOSTICS_CONTRACT_VERSION = 1;
4015
+ function redactPath(path) {
4016
+ if (!path) return null;
4017
+ const parts = path.split(/[/\\]/).filter(Boolean);
4018
+ if (parts.length <= 2) return parts.join("/");
4019
+ return `\u2026/${parts.slice(-2).join("/")}`;
4020
+ }
4021
+ function worstStatus(checks) {
4022
+ const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
4023
+ return checks.reduce(
4024
+ (worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
4025
+ "ok"
4026
+ );
3810
4027
  }
3811
- function resolveVersion() {
3812
- const scriptPath = process.argv[1] ?? "";
3813
- const here = scriptPath ? dirname5(scriptPath) : process.cwd();
3814
- let realHere = here;
3815
- try {
3816
- realHere = dirname5(realpathSync(scriptPath));
3817
- } catch {
4028
+ function buildReport(checks, now = /* @__PURE__ */ new Date()) {
4029
+ return {
4030
+ contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
4031
+ generatedAt: now.toISOString(),
4032
+ overall: worstStatus(checks),
4033
+ checks
4034
+ };
4035
+ }
4036
+ var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
4037
+ function redactValue(value) {
4038
+ if (Array.isArray(value)) {
4039
+ return value.map((v) => redactValue(v));
3818
4040
  }
3819
- const searchDirs = realHere === here ? [here, join7(here, "..")] : [here, join7(here, ".."), realHere, join7(realHere, "..")];
3820
- for (const dir of searchDirs) {
3821
- try {
3822
- const v = readFileSync4(join7(dir, "version.txt"), "utf8").trim();
3823
- if (v) return v;
3824
- } catch {
4041
+ if (value && typeof value === "object") {
4042
+ const out = {};
4043
+ for (const [k, v] of Object.entries(value)) {
4044
+ out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
3825
4045
  }
4046
+ return out;
3826
4047
  }
4048
+ return value;
4049
+ }
4050
+
4051
+ // src/api/routes/diagnostics.routes.ts
4052
+ function providerCheck(name, resolve2) {
3827
4053
  try {
3828
- const pkg = JSON.parse(readFileSync4(join7(here, "..", "package.json"), "utf8"));
3829
- if (pkg.version) return `${pkg.version}+source`;
4054
+ const exe = resolve2();
4055
+ return {
4056
+ id: `provider:${name}`,
4057
+ status: "ok",
4058
+ summary: `${name} CLI is installed.`,
4059
+ remediation: "NONE",
4060
+ detail: { location: redactPath(exe) }
4061
+ };
3830
4062
  } catch {
4063
+ return {
4064
+ id: `provider:${name}`,
4065
+ status: "failed",
4066
+ summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
4067
+ remediation: "PROVIDER_NOT_INSTALLED"
4068
+ };
3831
4069
  }
3832
- return "0.0.0+unknown";
3833
4070
  }
4071
+ var createDiagnosticsRoutes = (deps) => {
4072
+ const app = new Hono8();
4073
+ app.get("/", (c) => {
4074
+ const checks = [];
4075
+ checks.push({
4076
+ id: "streamer",
4077
+ status: "ok",
4078
+ summary: "Streamer is running.",
4079
+ remediation: "NONE",
4080
+ detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
4081
+ });
4082
+ checks.push(providerCheck("claude-code", resolveClaudeExe));
4083
+ checks.push(providerCheck("codex-cli", resolveCodexExe));
4084
+ const cacheAlert = deps.cacheMonitor()?.healthzField();
4085
+ checks.push(
4086
+ cacheAlert ? {
4087
+ id: "cache",
4088
+ status: "degraded",
4089
+ summary: "Conversation cache reported an integrity alert.",
4090
+ remediation: "CACHE_DEGRADED"
4091
+ } : {
4092
+ id: "cache",
4093
+ status: "ok",
4094
+ summary: "Conversation cache is healthy.",
4095
+ remediation: "NONE"
4096
+ }
4097
+ );
4098
+ let ptyOk = true;
4099
+ try {
4100
+ __require.resolve("node-pty");
4101
+ } catch {
4102
+ ptyOk = false;
4103
+ }
4104
+ checks.push(
4105
+ ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
4106
+ id: "pty",
4107
+ status: "failed",
4108
+ summary: "node-pty failed to load, so no managed session can start.",
4109
+ remediation: "PTY_UNAVAILABLE"
4110
+ }
4111
+ );
4112
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4113
+ const claudeProjects = home ? `${home}/.claude/projects` : "";
4114
+ checks.push(
4115
+ claudeProjects && existsSync5(claudeProjects) ? {
4116
+ id: "filesystem",
4117
+ status: "ok",
4118
+ summary: "Provider history directory is present.",
4119
+ remediation: "NONE",
4120
+ detail: { location: redactPath(claudeProjects) }
4121
+ } : {
4122
+ id: "filesystem",
4123
+ status: "degraded",
4124
+ summary: "Provider history directory was not found; history may be unavailable.",
4125
+ remediation: "FS_SCOPE_MISSING"
4126
+ }
4127
+ );
4128
+ return c.json(redactValue(buildReport(checks)));
4129
+ });
4130
+ return app;
4131
+ };
3834
4132
 
3835
4133
  // src/api/routes/health.routes.ts
4134
+ import { Hono as Hono9 } from "hono";
3836
4135
  var createHealthRoutes = (deps) => {
3837
- const app = new Hono7();
4136
+ const app = new Hono9();
3838
4137
  app.get("/", (c) => {
3839
4138
  const cacheAlert = deps.cacheMonitor()?.healthzField();
3840
4139
  return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
@@ -3843,9 +4142,9 @@ var createHealthRoutes = (deps) => {
3843
4142
  };
3844
4143
 
3845
4144
  // src/api/routes/logs.routes.ts
3846
- import { closeSync, existsSync as existsSync5, fstatSync, openSync, readSync, statSync } from "fs";
4145
+ import { closeSync, existsSync as existsSync6, fstatSync, openSync, readSync, statSync } from "fs";
3847
4146
  import { join as join9 } from "path";
3848
- import { Hono as Hono8 } from "hono";
4147
+ import { Hono as Hono10 } from "hono";
3849
4148
 
3850
4149
  // src/lifecycle/constants.ts
3851
4150
  import { homedir as homedir4 } from "os";
@@ -3863,12 +4162,12 @@ function resolveLogPath(source) {
3863
4162
  function pickDefaultSource() {
3864
4163
  for (const source of ["stdout", "stderr", "dev"]) {
3865
4164
  const p = resolveLogPath(source);
3866
- if (existsSync5(p) && statSync(p).size > 0) return source;
4165
+ if (existsSync6(p) && statSync(p).size > 0) return source;
3867
4166
  }
3868
4167
  return "stdout";
3869
4168
  }
3870
4169
  function readLogLines(filePath, sinceOffset, limit) {
3871
- if (!existsSync5(filePath)) {
4170
+ if (!existsSync6(filePath)) {
3872
4171
  return { lines: [], offset: 0, total: 0 };
3873
4172
  }
3874
4173
  const fd = openSync(filePath, "r");
@@ -3903,7 +4202,7 @@ function readLogLines(filePath, sinceOffset, limit) {
3903
4202
  }
3904
4203
  }
3905
4204
  function createLogsRoutes() {
3906
- const app = new Hono8();
4205
+ const app = new Hono10();
3907
4206
  app.get("/", (c) => {
3908
4207
  try {
3909
4208
  const sourceParam = (c.req.query("source") || "").toLowerCase();
@@ -3911,7 +4210,7 @@ function createLogsRoutes() {
3911
4210
  const logPath = resolveLogPath(source);
3912
4211
  const sinceOffset = parseInt(c.req.query("since") || "0", 10);
3913
4212
  const limit = Math.min(parseInt(c.req.query("limit") || "100", 10) || 100, 1e3);
3914
- if (!existsSync5(logPath)) {
4213
+ if (!existsSync6(logPath)) {
3915
4214
  return c.json({
3916
4215
  logs: [],
3917
4216
  message: `No log file found for source=${source}`,
@@ -3948,7 +4247,7 @@ function createLogsRoutes() {
3948
4247
  try {
3949
4248
  const sources = ["stdout", "stderr", "dev"].map((source) => {
3950
4249
  const logPath = resolveLogPath(source);
3951
- if (!existsSync5(logPath)) {
4250
+ if (!existsSync6(logPath)) {
3952
4251
  return { source, exists: false, total: 0, fileSize: 0 };
3953
4252
  }
3954
4253
  const stats = statSync(logPath);
@@ -3974,8 +4273,8 @@ function createLogsRoutes() {
3974
4273
  // src/api/routes/misc.routes.ts
3975
4274
  import { spawn } from "child_process";
3976
4275
  import { createHmac, timingSafeEqual as timingSafeEqual3 } from "crypto";
3977
- import { Hono as Hono9 } from "hono";
3978
- import { hostname } from "os";
4276
+ import { Hono as Hono11 } from "hono";
4277
+ import { hostname as hostname2 } from "os";
3979
4278
 
3980
4279
  // src/config/update-config.ts
3981
4280
  import { readFileSync as readFileSync5 } from "fs";
@@ -4305,12 +4604,12 @@ function verifyWebhookSignature(body, header, secret) {
4305
4604
  }
4306
4605
  var clientLog = getLogger("client");
4307
4606
  var createMiscRoutes = (deps) => {
4308
- const app = new Hono9();
4607
+ const app = new Hono11();
4309
4608
  app.get("/api/info", (c) => {
4310
4609
  const ptyIds = deps.ptyAttachedIds();
4311
4610
  return c.json({
4312
4611
  version: getVersion(),
4313
- machineName: hostname(),
4612
+ machineName: hostname2(),
4314
4613
  platform: process.platform,
4315
4614
  activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
4316
4615
  publicUrl: deps.publicUrl,
@@ -4436,11 +4735,11 @@ var createMiscRoutes = (deps) => {
4436
4735
  };
4437
4736
 
4438
4737
  // src/api/routes/pair.routes.ts
4439
- import { Hono as Hono10 } from "hono";
4738
+ import { Hono as Hono12 } from "hono";
4440
4739
  var ALREADY_HANDLED3 = 597;
4441
4740
  var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
4442
4741
  var createPairRoutes = (deps) => {
4443
- const app = new Hono10();
4742
+ const app = new Hono12();
4444
4743
  app.post("/start", (c) => {
4445
4744
  deps.handlePairStart(c.env.outgoing);
4446
4745
  return alreadyHandled3();
@@ -4453,11 +4752,11 @@ var createPairRoutes = (deps) => {
4453
4752
  };
4454
4753
 
4455
4754
  // src/api/routes/projects.routes.ts
4456
- import { Hono as Hono11 } from "hono";
4755
+ import { Hono as Hono13 } from "hono";
4457
4756
  var ALREADY_HANDLED4 = 597;
4458
4757
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
4459
4758
  var createProjectRoutes = (deps) => {
4460
- const app = new Hono11();
4759
+ const app = new Hono13();
4461
4760
  app.get("/", (c) => {
4462
4761
  const url = new URL(c.req.url);
4463
4762
  deps.handleListProjects(url, c.env.outgoing);
@@ -4472,7 +4771,7 @@ var createProjectRoutes = (deps) => {
4472
4771
  };
4473
4772
 
4474
4773
  // src/api/routes/providers.routes.ts
4475
- import { Hono as Hono12 } from "hono";
4774
+ import { Hono as Hono14 } from "hono";
4476
4775
 
4477
4776
  // src/services/providers/providerHealth.ts
4478
4777
  import { execFile as execFile2 } from "child_process";
@@ -4595,7 +4894,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
4595
4894
 
4596
4895
  // src/api/routes/providers.routes.ts
4597
4896
  var createProviderRoutes = () => {
4598
- const app = new Hono12();
4897
+ const app = new Hono14();
4599
4898
  app.get("/", async (c) => {
4600
4899
  const providers = await Promise.all([
4601
4900
  providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
@@ -4607,11 +4906,11 @@ var createProviderRoutes = () => {
4607
4906
  };
4608
4907
 
4609
4908
  // src/api/routes/scanner.routes.ts
4610
- import { Hono as Hono13 } from "hono";
4909
+ import { Hono as Hono15 } from "hono";
4611
4910
  var ALREADY_HANDLED5 = 597;
4612
4911
  var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
4613
4912
  var createScannerRoutes = (deps) => {
4614
- const app = new Hono13();
4913
+ const app = new Hono15();
4615
4914
  app.get("/api/search", async (c) => {
4616
4915
  const url = new URL(c.req.url);
4617
4916
  await deps.handleSearch(url, c.env.outgoing);
@@ -4621,11 +4920,11 @@ var createScannerRoutes = (deps) => {
4621
4920
  };
4622
4921
 
4623
4922
  // src/api/routes/sessions.routes.ts
4624
- import { Hono as Hono14 } from "hono";
4923
+ import { Hono as Hono16 } from "hono";
4625
4924
  var ALREADY_HANDLED6 = 597;
4626
4925
  var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
4627
4926
  var createSessionRoutes = (deps) => {
4628
- const app = new Hono14();
4927
+ const app = new Hono16();
4629
4928
  app.get("/count", (c) => {
4630
4929
  deps.handleSessionsCount(c.env.outgoing);
4631
4930
  return alreadyHandled6();
@@ -4700,9 +4999,9 @@ var createSessionRoutes = (deps) => {
4700
4999
  };
4701
5000
 
4702
5001
  // src/api/routes/ws.routes.ts
4703
- import { Hono as Hono15 } from "hono";
5002
+ import { Hono as Hono17 } from "hono";
4704
5003
  var createWsRoutes = (deps, upgradeWebSocket) => {
4705
- const app = new Hono15();
5004
+ const app = new Hono17();
4706
5005
  app.get(
4707
5006
  "/ws",
4708
5007
  upgradeWebSocket(() => {
@@ -4728,7 +5027,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
4728
5027
 
4729
5028
  // src/api/app.ts
4730
5029
  var createHonoApp = (deps, upgradeWebSocket) => {
4731
- const app = new Hono16();
5030
+ const app = new Hono18();
4732
5031
  const httpLog = getLogger("http");
4733
5032
  app.use("*", async (c, next) => {
4734
5033
  const start = Date.now();
@@ -4749,6 +5048,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
4749
5048
  app.use("*", authMiddleware(deps));
4750
5049
  app.onError(errorMiddleware);
4751
5050
  app.route("/healthz", createHealthRoutes(deps));
5051
+ app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
4752
5052
  app.route("/", createMiscRoutes(deps));
4753
5053
  app.route("/api/sessions", createSessionRoutes(deps));
4754
5054
  app.route("/api/conversations", createConversationRoutes(deps));
@@ -4757,6 +5057,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
4757
5057
  app.route("/api/projects", createProjectRoutes(deps));
4758
5058
  app.route("/api/providers", createProviderRoutes());
4759
5059
  app.route("/api/devices", createDeviceRoutes(deps));
5060
+ app.route("/api/backup", createBackupRoutes(deps));
4760
5061
  app.route("/api/pair", createPairRoutes(deps));
4761
5062
  app.route("/api", createBrowseRoutes(deps));
4762
5063
  app.route("/", createScannerRoutes(deps));
@@ -4825,7 +5126,7 @@ import {
4825
5126
  parseJsonlLine
4826
5127
  } from "@threadbase-sh/scanner";
4827
5128
  import Database from "better-sqlite3";
4828
- import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
5129
+ import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, openSync as openSync3, readSync as readSync3, statSync as statSync3 } from "fs";
4829
5130
  import { open as openAsync } from "fs/promises";
4830
5131
  import { dirname as dirname7 } from "path";
4831
5132
  import { setImmediate as yieldToEventLoop } from "timers/promises";
@@ -6057,7 +6358,7 @@ var ConversationCache = class _ConversationCache {
6057
6358
  * `handleGetConversation` can still serve the cached tail even when the
6058
6359
  * JSONL has been deleted.
6059
6360
  */
6060
- pruneGhostFiles(exists = existsSync6) {
6361
+ pruneGhostFiles(exists = existsSync7) {
6061
6362
  const rows = this.stmts.allFilePaths.all();
6062
6363
  const ghosts = [];
6063
6364
  const prune = this.db.transaction((ids) => {
@@ -6112,7 +6413,7 @@ var ConversationCache = class _ConversationCache {
6112
6413
  * Returns the removed IDs.
6113
6414
  */
6114
6415
  reconcileDeletions(livePaths, opts) {
6115
- const exists = opts?.exists ?? existsSync6;
6416
+ const exists = opts?.exists ?? existsSync7;
6116
6417
  const rows = this.stmts.allFilePaths.all();
6117
6418
  const removed = [];
6118
6419
  const drop = this.db.transaction((ids) => {
@@ -6147,7 +6448,7 @@ var ConversationCache = class _ConversationCache {
6147
6448
  * reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
6148
6449
  * rows that still have cached history (which pruneGhostFiles would keep).
6149
6450
  */
6150
- listMissingFiles(exists = existsSync6) {
6451
+ listMissingFiles(exists = existsSync7) {
6151
6452
  const rows = this.stmts.allFilePathsWithTitle.all();
6152
6453
  const missing = [];
6153
6454
  for (const row of rows) {
@@ -6648,7 +6949,7 @@ function setCacheMetadata(repo, key, value) {
6648
6949
 
6649
6950
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
6650
6951
  import { createHash as createHash3 } from "crypto";
6651
- import { existsSync as existsSync8 } from "fs";
6952
+ import { existsSync as existsSync9 } from "fs";
6652
6953
 
6653
6954
  // src/services/cache-integrity/alertStore.ts
6654
6955
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
@@ -6674,7 +6975,7 @@ function saveAlertState(state) {
6674
6975
  }
6675
6976
 
6676
6977
  // src/services/cache-integrity/backup.ts
6677
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync5, unlinkSync } from "fs";
6978
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync5, unlinkSync } from "fs";
6678
6979
  import { join as join15 } from "path";
6679
6980
  var DEFAULT_RETAIN = 3;
6680
6981
  function retainCount() {
@@ -6696,7 +6997,7 @@ async function backupCacheDb(db, cacheDir) {
6696
6997
  return { full, mtime: statSync5(full).mtimeMs };
6697
6998
  }).sort((a, b) => b.mtime - a.mtime);
6698
6999
  for (const stale of backups.slice(retain)) {
6699
- if (existsSync7(stale.full)) unlinkSync(stale.full);
7000
+ if (existsSync8(stale.full)) unlinkSync(stale.full);
6700
7001
  }
6701
7002
  return destPath;
6702
7003
  }
@@ -6793,7 +7094,7 @@ var CacheIntegrityMonitor = class {
6793
7094
  * the pending record, back up on high severity, and broadcast the alert.
6794
7095
  */
6795
7096
  async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
6796
- const all = this.cache.listMissingFiles(existsSync8);
7097
+ const all = this.cache.listMissingFiles(existsSync9);
6797
7098
  const missing = all.filter((m) => !this.ignoredIds.has(m.id));
6798
7099
  if (missing.length === 0) {
6799
7100
  if (this._pending) {
@@ -6897,7 +7198,7 @@ var CacheIntegrityMonitor = class {
6897
7198
  case "prune_all": {
6898
7199
  await this.ensureBackup(pending);
6899
7200
  const backupPath = pending.backupPath;
6900
- const stillMissing = pending.missing.filter((m) => !existsSync8(m.filePath)).map((m) => m.id);
7201
+ const stillMissing = pending.missing.filter((m) => !existsSync9(m.filePath)).map((m) => m.id);
6901
7202
  const pruned = this.cache.dropRowsById(stillMissing);
6902
7203
  this.applyDeferredUnlinks();
6903
7204
  this.clearPending();
@@ -7167,14 +7468,14 @@ function findSearchTarget(messages, query) {
7167
7468
  }
7168
7469
 
7169
7470
  // src/services/conversations/pruneAgentConversations.ts
7170
- import { existsSync as existsSync9 } from "fs";
7471
+ import { existsSync as existsSync10 } from "fs";
7171
7472
  function pruneAgentConversations(cache) {
7172
7473
  const db = cache.getDatabase();
7173
7474
  const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
7174
7475
  let pruned = 0;
7175
7476
  let missing = 0;
7176
7477
  for (const row of rows) {
7177
- if (!existsSync9(row.file_path)) {
7478
+ if (!existsSync10(row.file_path)) {
7178
7479
  missing += 1;
7179
7480
  continue;
7180
7481
  }
@@ -8051,6 +8352,88 @@ function resolveAnswer(pending, body) {
8051
8352
  }
8052
8353
  }
8053
8354
 
8355
+ // src/services/search/searchQuery.ts
8356
+ var DEFAULT_SEARCH_LIMIT = 50;
8357
+ var MAX_SEARCH_LIMIT = 200;
8358
+ var MAX_QUERY_LENGTH = 256;
8359
+ var SearchQueryError = class extends Error {
8360
+ constructor(message, code) {
8361
+ super(message);
8362
+ this.code = code;
8363
+ }
8364
+ code;
8365
+ };
8366
+ function intOr(raw, fallback) {
8367
+ if (raw === null) return fallback;
8368
+ const n = Number.parseInt(raw, 10);
8369
+ return Number.isFinite(n) ? n : fallback;
8370
+ }
8371
+ function parseSearchQuery(params) {
8372
+ const q = (params.get("q") ?? "").trim();
8373
+ if (!q) {
8374
+ throw new SearchQueryError("Missing query parameter: q", "invalid_query");
8375
+ }
8376
+ if (q.length > MAX_QUERY_LENGTH) {
8377
+ throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
8378
+ }
8379
+ const limit = Math.min(
8380
+ Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
8381
+ MAX_SEARCH_LIMIT
8382
+ );
8383
+ const offset = Math.max(intOr(params.get("offset"), 0), 0);
8384
+ const filters = {};
8385
+ const provider = params.get("provider");
8386
+ if (provider !== null) {
8387
+ if (!isProviderName(provider)) {
8388
+ throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
8389
+ }
8390
+ filters.provider = provider;
8391
+ }
8392
+ const projectPath = params.get("projectPath");
8393
+ if (projectPath) filters.projectPath = projectPath;
8394
+ const branch = params.get("branch");
8395
+ if (branch) filters.branch = branch;
8396
+ for (const [key, field] of [
8397
+ ["since", "since"],
8398
+ ["until", "until"]
8399
+ ]) {
8400
+ const raw = params.get(key);
8401
+ if (raw === null) continue;
8402
+ const ms = Date.parse(raw);
8403
+ if (Number.isNaN(ms)) {
8404
+ throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
8405
+ }
8406
+ filters[field] = ms;
8407
+ }
8408
+ if (filters.since != null && filters.until != null && filters.since > filters.until) {
8409
+ throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
8410
+ }
8411
+ return { q, limit, offset, filters };
8412
+ }
8413
+ function applyFilters(results, filters) {
8414
+ return results.filter((r) => {
8415
+ if (filters.provider && r.provider !== filters.provider) return false;
8416
+ if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
8417
+ if (filters.branch && r.branch !== filters.branch) return false;
8418
+ if (filters.since != null || filters.until != null) {
8419
+ const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
8420
+ if (Number.isNaN(ts)) return false;
8421
+ if (filters.since != null && ts < filters.since) return false;
8422
+ if (filters.until != null && ts > filters.until) return false;
8423
+ }
8424
+ return true;
8425
+ });
8426
+ }
8427
+ function paginate(results, offset, limit) {
8428
+ const items = results.slice(offset, offset + limit);
8429
+ return {
8430
+ items,
8431
+ total: results.length,
8432
+ offset,
8433
+ hasMore: offset + items.length < results.length
8434
+ };
8435
+ }
8436
+
8054
8437
  // src/services/sessions/conversationBusy.ts
8055
8438
  import { statSync as statSync8 } from "fs";
8056
8439
  var RESUME_BUSY_WINDOW_MS = 12e4;
@@ -8753,6 +9136,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
8753
9136
  var GRACE_MAX_DEFERS = 4;
8754
9137
  var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
8755
9138
  var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
9139
+ var SEARCH_OVERFETCH = 4;
9140
+ var SEARCH_MAX_SCAN = 1e3;
8756
9141
  var RESUME_DISCOVERY_TIMEOUT_MS = 750;
8757
9142
  var DISCOVERY_TTL_MS = 15e3;
8758
9143
  var ADOPT_KILL_TIMEOUT_MS = 5e3;
@@ -9521,14 +9906,14 @@ var StreamerServer = class {
9521
9906
  }
9522
9907
  this.apnsClient = new ApnsClient(creds);
9523
9908
  const sender = new LiveActivitySender(this.apnsClient, pushRepo);
9524
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname2();
9525
- this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname2());
9909
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? hostname3();
9910
+ this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, hostname3());
9526
9911
  this.liveActivityRenewal = new LiveActivityRenewalScheduler({
9527
9912
  repo: pushRepo,
9528
9913
  sender,
9529
9914
  sessionStore: this.sessionStore,
9530
9915
  serverId,
9531
- serverLabel: hostname2()
9916
+ serverLabel: hostname3()
9532
9917
  });
9533
9918
  this.liveActivityRenewal.start();
9534
9919
  this.log.info("Live Activity push enabled", {
@@ -9867,7 +10252,7 @@ var StreamerServer = class {
9867
10252
  this.fileWatcher.watchDirectory(dir);
9868
10253
  }
9869
10254
  for (const dir of this.codexRoots) {
9870
- if (!existsSync10(dir)) continue;
10255
+ if (!existsSync11(dir)) continue;
9871
10256
  this.fileWatcher.watchDirectory(dir);
9872
10257
  }
9873
10258
  } catch (err) {
@@ -10140,7 +10525,7 @@ var StreamerServer = class {
10140
10525
  }
10141
10526
  let body;
10142
10527
  try {
10143
- body = await readBody(req);
10528
+ body = await readBody2(req);
10144
10529
  } catch (err) {
10145
10530
  const message = err instanceof Error ? err.message : "Invalid body";
10146
10531
  json(res, 400, { error: message });
@@ -10186,7 +10571,7 @@ var StreamerServer = class {
10186
10571
  nonce: sealed.nonce,
10187
10572
  ephemeralPublicKey: sealed.ephemeralPublicKey,
10188
10573
  publicUrl: this.publicUrl,
10189
- machineName: hostname2(),
10574
+ machineName: hostname3(),
10190
10575
  ...device && {
10191
10576
  deviceId: device.deviceId,
10192
10577
  deviceToken: device.deviceToken,
@@ -10721,15 +11106,15 @@ var StreamerServer = class {
10721
11106
  findJsonlPath(uuid) {
10722
11107
  const filename = `${uuid}.jsonl`;
10723
11108
  for (const projectsDir of this.projectsDirs()) {
10724
- if (!existsSync10(projectsDir)) continue;
11109
+ if (!existsSync11(projectsDir)) continue;
10725
11110
  for (const dir of readdirSync6(projectsDir)) {
10726
11111
  const fp = join18(projectsDir, dir, filename);
10727
- if (existsSync10(fp)) return fp;
11112
+ if (existsSync11(fp)) return fp;
10728
11113
  const projectDir = join18(projectsDir, dir);
10729
11114
  try {
10730
11115
  for (const sub of readdirSync6(projectDir)) {
10731
11116
  const subagentPath = join18(projectDir, sub, "subagents", filename);
10732
- if (existsSync10(subagentPath)) return subagentPath;
11117
+ if (existsSync11(subagentPath)) return subagentPath;
10733
11118
  }
10734
11119
  } catch {
10735
11120
  }
@@ -11266,7 +11651,7 @@ var StreamerServer = class {
11266
11651
  }
11267
11652
  let body;
11268
11653
  try {
11269
- body = await readBody(req);
11654
+ body = await readBody2(req);
11270
11655
  } catch {
11271
11656
  res.setHeader("Accept-Query", "application/json");
11272
11657
  json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
@@ -11304,17 +11689,27 @@ var StreamerServer = class {
11304
11689
  });
11305
11690
  }
11306
11691
  async handleSearch(url, res) {
11307
- const q = url.searchParams.get("q") ?? "";
11308
- if (!q) {
11309
- json(res, 400, { error: "Missing query parameter: q" });
11310
- return;
11692
+ let parsed;
11693
+ try {
11694
+ parsed = parseSearchQuery(url.searchParams);
11695
+ } catch (err) {
11696
+ if (err instanceof SearchQueryError) {
11697
+ json(res, 400, { error: err.message, code: err.code });
11698
+ return;
11699
+ }
11700
+ throw err;
11311
11701
  }
11312
- const limit = intParam(url, "limit", 50);
11702
+ const { q, limit, offset, filters } = parsed;
11703
+ const startedAt = Date.now();
11313
11704
  const scanner = await this.getScanner();
11314
11705
  const results = await search(
11315
11706
  q,
11316
11707
  {
11317
- limit,
11708
+ // Fetch beyond the requested page: filters below are applied AFTER the
11709
+ // scanner returns, so slicing at `limit` here would drop results that a
11710
+ // later page should contain. Bounded so a broad query cannot pull an
11711
+ // unbounded set into memory.
11712
+ limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
11318
11713
  include: "conversations",
11319
11714
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
11320
11715
  ...this.codexScanOpts()
@@ -11338,13 +11733,24 @@ var StreamerServer = class {
11338
11733
  lastActivity: r.meta.timestamp,
11339
11734
  firstMessage: r.meta.firstMessage ?? void 0,
11340
11735
  lastMessage: r.meta.lastMessage ?? void 0,
11341
- provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
11736
+ provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
11737
+ // The scanner already computes relevance and match snippets; the previous
11738
+ // adapter discarded both, so results arrived in an unexplained order with
11739
+ // no indication of WHY anything matched.
11740
+ score: r.score,
11741
+ matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
11742
+ field: m.field,
11743
+ snippet: m.snippet
11744
+ })) : []
11342
11745
  }));
11746
+ const page = paginate(applyFilters(adapted, filters), offset, limit);
11343
11747
  json(res, 200, {
11344
- conversations: adapted,
11345
- hasMore: false,
11346
- offset: 0,
11347
- total: adapted.length
11748
+ conversations: page.items,
11749
+ hasMore: page.hasMore,
11750
+ offset: page.offset,
11751
+ total: page.total,
11752
+ // Query timing, so a slow search is diagnosable rather than merely felt.
11753
+ tookMs: Date.now() - startedAt
11348
11754
  });
11349
11755
  }
11350
11756
  async handleListSessions(url, res) {
@@ -11390,7 +11796,7 @@ var StreamerServer = class {
11390
11796
  if (this.rejectIfWarmingUp(res)) return;
11391
11797
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
11392
11798
  if (session) {
11393
- if (!existsSync10(session.projectPath)) {
11799
+ if (!existsSync11(session.projectPath)) {
11394
11800
  session.failureReason = `Project directory not found: ${session.projectPath}`;
11395
11801
  }
11396
11802
  const reconciled = this.withReconciledLifecycle([session])[0];
@@ -11417,7 +11823,7 @@ var StreamerServer = class {
11417
11823
  json(res, 404, { error: "Session not found" });
11418
11824
  }
11419
11825
  async handleResume(req, res) {
11420
- const body = await readBody(req);
11826
+ const body = await readBody2(req);
11421
11827
  const sessionId = body.sessionId ?? body.conversationId;
11422
11828
  if (!sessionId) {
11423
11829
  json(res, 400, { error: "Missing sessionId" });
@@ -11549,7 +11955,7 @@ var StreamerServer = class {
11549
11955
  return;
11550
11956
  }
11551
11957
  if (this.agentConfig.enabled) {
11552
- const body2 = await readBody(req);
11958
+ const body2 = await readBody2(req);
11553
11959
  const cache = this.cache;
11554
11960
  if (!cache) {
11555
11961
  json(res, 503, {
@@ -11568,7 +11974,7 @@ var StreamerServer = class {
11568
11974
  json(res, result.status, result.body);
11569
11975
  return;
11570
11976
  }
11571
- const body = await readBody(req);
11977
+ const body = await readBody2(req);
11572
11978
  const { input, keys } = body;
11573
11979
  let idempotencyKey;
11574
11980
  try {
@@ -11732,7 +12138,7 @@ var StreamerServer = class {
11732
12138
  });
11733
12139
  }
11734
12140
  async handleSendAnswer(sessionId, req, res) {
11735
- const body = await readBody(req);
12141
+ const body = await readBody2(req);
11736
12142
  const pending = this.pendingQuestions.get(sessionId);
11737
12143
  const resolution = resolveAnswer(pending, body);
11738
12144
  if (!resolution.ok) {
@@ -11761,7 +12167,7 @@ var StreamerServer = class {
11761
12167
  json(res, 400, { error: "Session has no project path" });
11762
12168
  return;
11763
12169
  }
11764
- const body = await readBody(req);
12170
+ const body = await readBody2(req);
11765
12171
  const { filename, mimeType, dataBase64 } = body ?? {};
11766
12172
  if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
11767
12173
  json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
@@ -11963,7 +12369,7 @@ var StreamerServer = class {
11963
12369
  return;
11964
12370
  }
11965
12371
  if (this.agentConfig.enabled) {
11966
- const body2 = await readBody(req);
12372
+ const body2 = await readBody2(req);
11967
12373
  const result = await handleStartAgentSession(body2, {
11968
12374
  sessionStore: this.sessionStore,
11969
12375
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
@@ -11977,7 +12383,7 @@ var StreamerServer = class {
11977
12383
  }
11978
12384
  return;
11979
12385
  }
11980
- const body = await readBody(req);
12386
+ const body = await readBody2(req);
11981
12387
  const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
11982
12388
  if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
11983
12389
  json(res, 400, { error: "Invalid provider" });
@@ -12149,8 +12555,8 @@ var StreamerServer = class {
12149
12555
  cleanup();
12150
12556
  return;
12151
12557
  }
12152
- let resolvedFilePath = existsSync10(filePath) ? filePath : null;
12153
- if (!resolvedFilePath && existsSync10(projectsDir)) {
12558
+ let resolvedFilePath = existsSync11(filePath) ? filePath : null;
12559
+ if (!resolvedFilePath && existsSync11(projectsDir)) {
12154
12560
  try {
12155
12561
  const now = Date.now();
12156
12562
  const match = readdirSync6(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: statSync9(join18(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
@@ -12246,7 +12652,7 @@ var StreamerServer = class {
12246
12652
  );
12247
12653
  for (const root of this.codexRoots) {
12248
12654
  const sessionsDir = join18(root, dateDir);
12249
- if (!existsSync10(sessionsDir)) continue;
12655
+ if (!existsSync11(sessionsDir)) continue;
12250
12656
  let candidateFiles;
12251
12657
  try {
12252
12658
  candidateFiles = readdirSync6(sessionsDir).filter((f) => f.endsWith(".jsonl"));
@@ -12335,7 +12741,7 @@ var StreamerServer = class {
12335
12741
  });
12336
12742
  return;
12337
12743
  }
12338
- const body = await readBody(req);
12744
+ const body = await readBody2(req);
12339
12745
  const { path: relativePath, name } = body;
12340
12746
  if (!name || typeof name !== "string") {
12341
12747
  json(res, 400, { error: "Missing name field" });
@@ -12365,7 +12771,7 @@ var StreamerServer = class {
12365
12771
  }
12366
12772
  let parsed;
12367
12773
  try {
12368
- parsed = await readBody(req);
12774
+ parsed = await readBody2(req);
12369
12775
  } catch {
12370
12776
  json(res, 400, { error: "Invalid JSON" });
12371
12777
  return;
@@ -12422,7 +12828,7 @@ var StreamerServer = class {
12422
12828
  }
12423
12829
  let parsed;
12424
12830
  try {
12425
- parsed = await readBody(req);
12831
+ parsed = await readBody2(req);
12426
12832
  } catch {
12427
12833
  json(res, 400, { error: "Invalid JSON" });
12428
12834
  return;
@@ -12481,7 +12887,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
12481
12887
  }
12482
12888
  function classifyResumability(cwd) {
12483
12889
  if (!cwd) return { resumable: true };
12484
- if (existsSync10(cwd)) return { resumable: true };
12890
+ if (existsSync11(cwd)) return { resumable: true };
12485
12891
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
12486
12892
  return {
12487
12893
  resumable: false,
@@ -12599,7 +13005,7 @@ function parseSessionListQuery(url) {
12599
13005
  const cursor = url.searchParams.get("cursor") ?? void 0;
12600
13006
  return { query: { limit, sortBy, order, status, cursor } };
12601
13007
  }
12602
- function readBody(req) {
13008
+ function readBody2(req) {
12603
13009
  return new Promise((resolve2, reject) => {
12604
13010
  const chunks = [];
12605
13011
  req.on("data", (chunk) => chunks.push(chunk));