@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.cjs CHANGED
@@ -3061,10 +3061,10 @@ var import_client = require("@temporalio/client");
3061
3061
  var import_scanner3 = require("@threadbase-sh/scanner");
3062
3062
  var import_crypto11 = require("crypto");
3063
3063
  var import_events = require("events");
3064
- var import_fs18 = require("fs");
3064
+ var import_fs19 = require("fs");
3065
3065
  var import_promises7 = require("fs/promises");
3066
3066
  var import_http = require("http");
3067
- var import_os9 = require("os");
3067
+ var import_os10 = require("os");
3068
3068
  var import_path18 = require("path");
3069
3069
  var import_readline = require("readline");
3070
3070
 
@@ -3324,7 +3324,7 @@ async function handleStartAgentSession(body, deps) {
3324
3324
  }
3325
3325
 
3326
3326
  // src/api/app.ts
3327
- var import_hono16 = require("hono");
3327
+ var import_hono18 = require("hono");
3328
3328
 
3329
3329
  // src/db/repositories/devices.repository.ts
3330
3330
  var import_crypto5 = require("crypto");
@@ -3644,12 +3644,222 @@ var errorMiddleware = (err, c) => {
3644
3644
  return c.json({ error: message }, 500);
3645
3645
  };
3646
3646
 
3647
- // src/api/routes/browse.routes.ts
3647
+ // src/api/routes/backup.routes.ts
3648
3648
  var import_hono2 = require("hono");
3649
+ var import_os5 = require("os");
3650
+
3651
+ // src/services/backup/backup.ts
3652
+ var BACKUP_FORMAT_VERSION = 1;
3653
+ var BackupError = class extends Error {
3654
+ constructor(message, code) {
3655
+ super(message);
3656
+ this.code = code;
3657
+ }
3658
+ code;
3659
+ };
3660
+ function validateArchive(input) {
3661
+ if (!input || typeof input !== "object") {
3662
+ throw new BackupError("Backup is not an object", "INVALID_ARCHIVE");
3663
+ }
3664
+ const archive = input;
3665
+ const manifest = archive.manifest;
3666
+ if (!manifest || typeof manifest !== "object") {
3667
+ throw new BackupError("Backup is missing its manifest", "INVALID_ARCHIVE");
3668
+ }
3669
+ if (manifest.formatVersion !== BACKUP_FORMAT_VERSION) {
3670
+ throw new BackupError(
3671
+ `Unsupported backup format version ${String(manifest.formatVersion)}; this build reads version ${BACKUP_FORMAT_VERSION}`,
3672
+ "UNSUPPORTED_VERSION"
3673
+ );
3674
+ }
3675
+ if (!Array.isArray(archive.projects)) {
3676
+ throw new BackupError("Backup is missing its projects array", "INVALID_ARCHIVE");
3677
+ }
3678
+ for (const [i, p] of archive.projects.entries()) {
3679
+ if (!p || typeof p !== "object") {
3680
+ throw new BackupError(`Project at index ${i} is not an object`, "INVALID_ARCHIVE");
3681
+ }
3682
+ if (typeof p.id !== "string" || p.id.length === 0) {
3683
+ throw new BackupError(`Project at index ${i} has no id`, "INVALID_ARCHIVE");
3684
+ }
3685
+ if (typeof p.path !== "string" || p.path.length === 0) {
3686
+ throw new BackupError(`Project at index ${i} has no path`, "INVALID_ARCHIVE");
3687
+ }
3688
+ }
3689
+ const ids = new Set(archive.projects.map((p) => p.id));
3690
+ if (ids.size !== archive.projects.length) {
3691
+ throw new BackupError("Backup contains duplicate project ids", "INVALID_ARCHIVE");
3692
+ }
3693
+ return archive;
3694
+ }
3695
+ function remapPaths(projects, rules) {
3696
+ const ordered = [...rules].sort((a, b) => b.from.length - a.from.length);
3697
+ return projects.map((p) => {
3698
+ const rule = ordered.find((r) => p.path === r.from || p.path.startsWith(`${r.from}/`));
3699
+ if (!rule) return p;
3700
+ return { ...p, path: `${rule.to}${p.path.slice(rule.from.length)}` };
3701
+ });
3702
+ }
3703
+ function planRestore(incoming, existing) {
3704
+ const byId = new Map(existing.map((e) => [e.id, e]));
3705
+ const byPath = new Map(existing.map((e) => [e.path, e]));
3706
+ const plan = { create: [], update: [], conflict: [] };
3707
+ for (const p of incoming) {
3708
+ const sameId = byId.get(p.id);
3709
+ if (sameId) {
3710
+ if (sameId.path !== p.path) plan.update.push(p);
3711
+ continue;
3712
+ }
3713
+ const samePath = byPath.get(p.path);
3714
+ if (samePath) {
3715
+ plan.conflict.push({ incoming: p, existingId: samePath.id });
3716
+ continue;
3717
+ }
3718
+ plan.create.push(p);
3719
+ }
3720
+ return plan;
3721
+ }
3722
+
3723
+ // src/version.ts
3724
+ var import_node_fs2 = require("fs");
3725
+ var import_node_path3 = require("path");
3726
+ var cached;
3727
+ function getVersion() {
3728
+ if (cached !== void 0) return cached;
3729
+ cached = resolveVersion();
3730
+ return cached;
3731
+ }
3732
+ function resolveVersion() {
3733
+ const scriptPath = process.argv[1] ?? "";
3734
+ const here = scriptPath ? (0, import_node_path3.dirname)(scriptPath) : process.cwd();
3735
+ let realHere = here;
3736
+ try {
3737
+ realHere = (0, import_node_path3.dirname)((0, import_node_fs2.realpathSync)(scriptPath));
3738
+ } catch {
3739
+ }
3740
+ const searchDirs = realHere === here ? [here, (0, import_node_path3.join)(here, "..")] : [here, (0, import_node_path3.join)(here, ".."), realHere, (0, import_node_path3.join)(realHere, "..")];
3741
+ for (const dir of searchDirs) {
3742
+ try {
3743
+ const v = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(dir, "version.txt"), "utf8").trim();
3744
+ if (v) return v;
3745
+ } catch {
3746
+ }
3747
+ }
3748
+ try {
3749
+ const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(here, "..", "package.json"), "utf8"));
3750
+ if (pkg.version) return `${pkg.version}+source`;
3751
+ } catch {
3752
+ }
3753
+ return "0.0.0+unknown";
3754
+ }
3755
+
3756
+ // src/api/routes/backup.routes.ts
3757
+ function readBody(c) {
3758
+ return new Promise((resolve2, reject) => {
3759
+ const chunks = [];
3760
+ c.env.incoming.on("data", (chunk) => chunks.push(chunk));
3761
+ c.env.incoming.on("end", () => {
3762
+ try {
3763
+ const raw = Buffer.concat(chunks).toString("utf-8");
3764
+ resolve2(raw ? JSON.parse(raw) : {});
3765
+ } catch {
3766
+ reject(new Error("Invalid JSON body"));
3767
+ }
3768
+ });
3769
+ c.env.incoming.on("error", reject);
3770
+ });
3771
+ }
3772
+ var createBackupRoutes = (deps) => {
3773
+ const app = new import_hono2.Hono();
3774
+ app.get("/export", (c) => {
3775
+ const repo = deps.projectsRepo();
3776
+ if (!repo) {
3777
+ return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
3778
+ }
3779
+ const projects = repo.listProjects().map((p) => ({
3780
+ id: p.id,
3781
+ path: p.path,
3782
+ name: p.name ?? null,
3783
+ createdAt: p.createdAt,
3784
+ updatedAt: p.updatedAt
3785
+ }));
3786
+ return c.json({
3787
+ manifest: {
3788
+ formatVersion: BACKUP_FORMAT_VERSION,
3789
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3790
+ streamerVersion: getVersion(),
3791
+ sourceHost: (0, import_os5.hostname)(),
3792
+ // No endpoint here exports the API key. The flag is recorded so an
3793
+ // archive is self-describing about its own sensitivity rather than
3794
+ // requiring a reader to infer it.
3795
+ includesSecrets: false,
3796
+ counts: { projects: projects.length }
3797
+ },
3798
+ projects
3799
+ });
3800
+ });
3801
+ app.post("/restore", async (c) => {
3802
+ const repo = deps.projectsRepo();
3803
+ if (!repo) {
3804
+ return c.json({ error: "Project store is unavailable", code: "STORE_UNAVAILABLE" }, 503);
3805
+ }
3806
+ let body;
3807
+ try {
3808
+ body = await readBody(c);
3809
+ } catch {
3810
+ return c.json({ error: "Invalid JSON body", code: "INVALID_BODY" }, 400);
3811
+ }
3812
+ let archive;
3813
+ try {
3814
+ archive = validateArchive(body.archive);
3815
+ } catch (err) {
3816
+ if (err instanceof BackupError) {
3817
+ return c.json({ error: err.message, code: err.code }, 400);
3818
+ }
3819
+ throw err;
3820
+ }
3821
+ 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 })) : [];
3822
+ const incoming = rules.length > 0 ? remapPaths(archive.projects, rules) : archive.projects;
3823
+ const existing = repo.listProjects().map((p) => ({ id: p.id, path: p.path }));
3824
+ const plan = planRestore(incoming, existing);
3825
+ const summary = {
3826
+ create: plan.create.length,
3827
+ update: plan.update.length,
3828
+ conflict: plan.conflict.length
3829
+ };
3830
+ if (body.apply !== true) {
3831
+ return c.json({ applied: false, summary, plan });
3832
+ }
3833
+ if (plan.conflict.length > 0) {
3834
+ return c.json(
3835
+ {
3836
+ error: "Restore has unresolved conflicts",
3837
+ code: "RESTORE_CONFLICT",
3838
+ summary,
3839
+ plan
3840
+ },
3841
+ 409
3842
+ );
3843
+ }
3844
+ let applied = 0;
3845
+ for (const p of [...plan.create, ...plan.update]) {
3846
+ try {
3847
+ repo.upsertProjectByPath(p.path, { name: p.name });
3848
+ applied++;
3849
+ } catch {
3850
+ }
3851
+ }
3852
+ return c.json({ applied: true, summary, appliedCount: applied });
3853
+ });
3854
+ return app;
3855
+ };
3856
+
3857
+ // src/api/routes/browse.routes.ts
3858
+ var import_hono3 = require("hono");
3649
3859
  var ALREADY_HANDLED = 597;
3650
3860
  var alreadyHandled = () => new Response(null, { status: ALREADY_HANDLED });
3651
3861
  var createBrowseRoutes = (deps) => {
3652
- const app = new import_hono2.Hono();
3862
+ const app = new import_hono3.Hono();
3653
3863
  app.get("/browse", async (c) => {
3654
3864
  const url = new URL(c.req.url);
3655
3865
  await deps.handleBrowse(url, c.env.outgoing);
@@ -3663,7 +3873,7 @@ var createBrowseRoutes = (deps) => {
3663
3873
  };
3664
3874
 
3665
3875
  // src/api/routes/cacheAlert.routes.ts
3666
- var import_hono3 = require("hono");
3876
+ var import_hono4 = require("hono");
3667
3877
 
3668
3878
  // src/schemas/cacheAlert.schema.ts
3669
3879
  var import_zod = require("zod");
@@ -3686,7 +3896,7 @@ function readRawBody2(req) {
3686
3896
  });
3687
3897
  }
3688
3898
  var createCacheAlertRoutes = (deps) => {
3689
- const app = new import_hono3.Hono();
3899
+ const app = new import_hono4.Hono();
3690
3900
  app.get("/", (c) => {
3691
3901
  const monitor = deps.cacheMonitor();
3692
3902
  return c.json({ pending: monitor?.pending ?? null });
@@ -3723,7 +3933,7 @@ var createCacheAlertRoutes = (deps) => {
3723
3933
  };
3724
3934
 
3725
3935
  // src/api/routes/config.routes.ts
3726
- var import_hono4 = require("hono");
3936
+ var import_hono5 = require("hono");
3727
3937
 
3728
3938
  // src/schemas/claudeFlags.schema.ts
3729
3939
  var import_zod2 = require("zod");
@@ -3744,7 +3954,7 @@ function readRawBody3(req) {
3744
3954
  });
3745
3955
  }
3746
3956
  var createConfigRoutes = (deps) => {
3747
- const app = new import_hono4.Hono();
3957
+ const app = new import_hono5.Hono();
3748
3958
  app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
3749
3959
  app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
3750
3960
  app.put("/claude-flags", async (c) => {
@@ -3779,11 +3989,11 @@ var createConfigRoutes = (deps) => {
3779
3989
  };
3780
3990
 
3781
3991
  // src/api/routes/conversations.routes.ts
3782
- var import_hono5 = require("hono");
3992
+ var import_hono6 = require("hono");
3783
3993
  var ALREADY_HANDLED2 = 597;
3784
3994
  var alreadyHandled2 = () => new Response(null, { status: ALREADY_HANDLED2 });
3785
3995
  var createConversationRoutes = (deps) => {
3786
- const app = new import_hono5.Hono();
3996
+ const app = new import_hono6.Hono();
3787
3997
  app.get("/count", async (c) => {
3788
3998
  const url = new URL(c.req.url);
3789
3999
  await deps.handleConversationsCount(url, c.env.outgoing);
@@ -3810,9 +4020,9 @@ var createConversationRoutes = (deps) => {
3810
4020
  };
3811
4021
 
3812
4022
  // src/api/routes/devices.routes.ts
3813
- var import_hono6 = require("hono");
4023
+ var import_hono7 = require("hono");
3814
4024
  var createDeviceRoutes = (deps) => {
3815
- const app = new import_hono6.Hono();
4025
+ const app = new import_hono7.Hono();
3816
4026
  app.get("/", (c) => {
3817
4027
  const repo = deps.devicesRepo();
3818
4028
  if (!repo) return c.json({ devices: [], available: false });
@@ -3835,45 +4045,134 @@ var createDeviceRoutes = (deps) => {
3835
4045
  return app;
3836
4046
  };
3837
4047
 
3838
- // src/api/routes/health.routes.ts
3839
- var import_hono7 = require("hono");
4048
+ // src/api/routes/diagnostics.routes.ts
4049
+ var import_fs7 = require("fs");
4050
+ var import_hono8 = require("hono");
3840
4051
 
3841
- // src/version.ts
3842
- var import_node_fs2 = require("fs");
3843
- var import_node_path3 = require("path");
3844
- var cached;
3845
- function getVersion() {
3846
- if (cached !== void 0) return cached;
3847
- cached = resolveVersion();
3848
- return cached;
4052
+ // src/services/diagnostics/diagnostics.ts
4053
+ var DIAGNOSTICS_CONTRACT_VERSION = 1;
4054
+ function redactPath(path) {
4055
+ if (!path) return null;
4056
+ const parts = path.split(/[/\\]/).filter(Boolean);
4057
+ if (parts.length <= 2) return parts.join("/");
4058
+ return `\u2026/${parts.slice(-2).join("/")}`;
4059
+ }
4060
+ function worstStatus(checks) {
4061
+ const rank = { ok: 0, unknown: 1, degraded: 2, failed: 3 };
4062
+ return checks.reduce(
4063
+ (worst, c) => rank[c.status] > rank[worst] ? c.status : worst,
4064
+ "ok"
4065
+ );
3849
4066
  }
3850
- function resolveVersion() {
3851
- const scriptPath = process.argv[1] ?? "";
3852
- const here = scriptPath ? (0, import_node_path3.dirname)(scriptPath) : process.cwd();
3853
- let realHere = here;
3854
- try {
3855
- realHere = (0, import_node_path3.dirname)((0, import_node_fs2.realpathSync)(scriptPath));
3856
- } catch {
4067
+ function buildReport(checks, now = /* @__PURE__ */ new Date()) {
4068
+ return {
4069
+ contractVersion: DIAGNOSTICS_CONTRACT_VERSION,
4070
+ generatedAt: now.toISOString(),
4071
+ overall: worstStatus(checks),
4072
+ checks
4073
+ };
4074
+ }
4075
+ var SECRET_KEY_RE = /(key|token|secret|password|passwd|credential|authorization|cookie)/i;
4076
+ function redactValue(value) {
4077
+ if (Array.isArray(value)) {
4078
+ return value.map((v) => redactValue(v));
3857
4079
  }
3858
- const searchDirs = realHere === here ? [here, (0, import_node_path3.join)(here, "..")] : [here, (0, import_node_path3.join)(here, ".."), realHere, (0, import_node_path3.join)(realHere, "..")];
3859
- for (const dir of searchDirs) {
3860
- try {
3861
- const v = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(dir, "version.txt"), "utf8").trim();
3862
- if (v) return v;
3863
- } catch {
4080
+ if (value && typeof value === "object") {
4081
+ const out = {};
4082
+ for (const [k, v] of Object.entries(value)) {
4083
+ out[k] = SECRET_KEY_RE.test(k) ? "[redacted]" : redactValue(v);
3864
4084
  }
4085
+ return out;
3865
4086
  }
4087
+ return value;
4088
+ }
4089
+
4090
+ // src/api/routes/diagnostics.routes.ts
4091
+ function providerCheck(name, resolve2) {
3866
4092
  try {
3867
- const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path3.join)(here, "..", "package.json"), "utf8"));
3868
- if (pkg.version) return `${pkg.version}+source`;
4093
+ const exe = resolve2();
4094
+ return {
4095
+ id: `provider:${name}`,
4096
+ status: "ok",
4097
+ summary: `${name} CLI is installed.`,
4098
+ remediation: "NONE",
4099
+ detail: { location: redactPath(exe) }
4100
+ };
3869
4101
  } catch {
4102
+ return {
4103
+ id: `provider:${name}`,
4104
+ status: "failed",
4105
+ summary: `${name} CLI could not be located. Sessions for this provider cannot start.`,
4106
+ remediation: "PROVIDER_NOT_INSTALLED"
4107
+ };
3870
4108
  }
3871
- return "0.0.0+unknown";
3872
4109
  }
4110
+ var createDiagnosticsRoutes = (deps) => {
4111
+ const app = new import_hono8.Hono();
4112
+ app.get("/", (c) => {
4113
+ const checks = [];
4114
+ checks.push({
4115
+ id: "streamer",
4116
+ status: "ok",
4117
+ summary: "Streamer is running.",
4118
+ remediation: "NONE",
4119
+ detail: { version: getVersion(), uptimeSeconds: Math.floor(process.uptime()) }
4120
+ });
4121
+ checks.push(providerCheck("claude-code", resolveClaudeExe));
4122
+ checks.push(providerCheck("codex-cli", resolveCodexExe));
4123
+ const cacheAlert = deps.cacheMonitor()?.healthzField();
4124
+ checks.push(
4125
+ cacheAlert ? {
4126
+ id: "cache",
4127
+ status: "degraded",
4128
+ summary: "Conversation cache reported an integrity alert.",
4129
+ remediation: "CACHE_DEGRADED"
4130
+ } : {
4131
+ id: "cache",
4132
+ status: "ok",
4133
+ summary: "Conversation cache is healthy.",
4134
+ remediation: "NONE"
4135
+ }
4136
+ );
4137
+ let ptyOk = true;
4138
+ try {
4139
+ require.resolve("node-pty");
4140
+ } catch {
4141
+ ptyOk = false;
4142
+ }
4143
+ checks.push(
4144
+ ptyOk ? { id: "pty", status: "ok", summary: "PTY subsystem is available.", remediation: "NONE" } : {
4145
+ id: "pty",
4146
+ status: "failed",
4147
+ summary: "node-pty failed to load, so no managed session can start.",
4148
+ remediation: "PTY_UNAVAILABLE"
4149
+ }
4150
+ );
4151
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4152
+ const claudeProjects = home ? `${home}/.claude/projects` : "";
4153
+ checks.push(
4154
+ claudeProjects && (0, import_fs7.existsSync)(claudeProjects) ? {
4155
+ id: "filesystem",
4156
+ status: "ok",
4157
+ summary: "Provider history directory is present.",
4158
+ remediation: "NONE",
4159
+ detail: { location: redactPath(claudeProjects) }
4160
+ } : {
4161
+ id: "filesystem",
4162
+ status: "degraded",
4163
+ summary: "Provider history directory was not found; history may be unavailable.",
4164
+ remediation: "FS_SCOPE_MISSING"
4165
+ }
4166
+ );
4167
+ return c.json(redactValue(buildReport(checks)));
4168
+ });
4169
+ return app;
4170
+ };
3873
4171
 
3874
4172
  // src/api/routes/health.routes.ts
4173
+ var import_hono9 = require("hono");
3875
4174
  var createHealthRoutes = (deps) => {
3876
- const app = new import_hono7.Hono();
4175
+ const app = new import_hono9.Hono();
3877
4176
  app.get("/", (c) => {
3878
4177
  const cacheAlert = deps.cacheMonitor()?.healthzField();
3879
4178
  return c.json({ ok: true, version: getVersion(), ...cacheAlert ? { cacheAlert } : {} });
@@ -3884,7 +4183,7 @@ var createHealthRoutes = (deps) => {
3884
4183
  // src/api/routes/logs.routes.ts
3885
4184
  var import_node_fs3 = require("fs");
3886
4185
  var import_node_path5 = require("path");
3887
- var import_hono8 = require("hono");
4186
+ var import_hono10 = require("hono");
3888
4187
 
3889
4188
  // src/lifecycle/constants.ts
3890
4189
  var import_node_os = require("os");
@@ -3942,7 +4241,7 @@ function readLogLines(filePath, sinceOffset, limit) {
3942
4241
  }
3943
4242
  }
3944
4243
  function createLogsRoutes() {
3945
- const app = new import_hono8.Hono();
4244
+ const app = new import_hono10.Hono();
3946
4245
  app.get("/", (c) => {
3947
4246
  try {
3948
4247
  const sourceParam = (c.req.query("source") || "").toLowerCase();
@@ -4013,8 +4312,8 @@ function createLogsRoutes() {
4013
4312
  // src/api/routes/misc.routes.ts
4014
4313
  var import_node_child_process = require("child_process");
4015
4314
  var import_node_crypto2 = require("crypto");
4016
- var import_hono9 = require("hono");
4017
- var import_os5 = require("os");
4315
+ var import_hono11 = require("hono");
4316
+ var import_os6 = require("os");
4018
4317
 
4019
4318
  // src/config/update-config.ts
4020
4319
  var import_node_fs4 = require("fs");
@@ -4344,12 +4643,12 @@ function verifyWebhookSignature(body, header, secret) {
4344
4643
  }
4345
4644
  var clientLog = getLogger("client");
4346
4645
  var createMiscRoutes = (deps) => {
4347
- const app = new import_hono9.Hono();
4646
+ const app = new import_hono11.Hono();
4348
4647
  app.get("/api/info", (c) => {
4349
4648
  const ptyIds = deps.ptyAttachedIds();
4350
4649
  return c.json({
4351
4650
  version: getVersion(),
4352
- machineName: (0, import_os5.hostname)(),
4651
+ machineName: (0, import_os6.hostname)(),
4353
4652
  platform: process.platform,
4354
4653
  activeSessions: deps.sessionStore.list(ptyIds).filter((s) => s.status === "running").length,
4355
4654
  publicUrl: deps.publicUrl,
@@ -4475,11 +4774,11 @@ var createMiscRoutes = (deps) => {
4475
4774
  };
4476
4775
 
4477
4776
  // src/api/routes/pair.routes.ts
4478
- var import_hono10 = require("hono");
4777
+ var import_hono12 = require("hono");
4479
4778
  var ALREADY_HANDLED3 = 597;
4480
4779
  var alreadyHandled3 = () => new Response(null, { status: ALREADY_HANDLED3 });
4481
4780
  var createPairRoutes = (deps) => {
4482
- const app = new import_hono10.Hono();
4781
+ const app = new import_hono12.Hono();
4483
4782
  app.post("/start", (c) => {
4484
4783
  deps.handlePairStart(c.env.outgoing);
4485
4784
  return alreadyHandled3();
@@ -4492,11 +4791,11 @@ var createPairRoutes = (deps) => {
4492
4791
  };
4493
4792
 
4494
4793
  // src/api/routes/projects.routes.ts
4495
- var import_hono11 = require("hono");
4794
+ var import_hono13 = require("hono");
4496
4795
  var ALREADY_HANDLED4 = 597;
4497
4796
  var alreadyHandled4 = () => new Response(null, { status: ALREADY_HANDLED4 });
4498
4797
  var createProjectRoutes = (deps) => {
4499
- const app = new import_hono11.Hono();
4798
+ const app = new import_hono13.Hono();
4500
4799
  app.get("/", (c) => {
4501
4800
  const url = new URL(c.req.url);
4502
4801
  deps.handleListProjects(url, c.env.outgoing);
@@ -4511,7 +4810,7 @@ var createProjectRoutes = (deps) => {
4511
4810
  };
4512
4811
 
4513
4812
  // src/api/routes/providers.routes.ts
4514
- var import_hono12 = require("hono");
4813
+ var import_hono14 = require("hono");
4515
4814
 
4516
4815
  // src/services/providers/providerHealth.ts
4517
4816
  var import_child_process3 = require("child_process");
@@ -4634,7 +4933,7 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
4634
4933
 
4635
4934
  // src/api/routes/providers.routes.ts
4636
4935
  var createProviderRoutes = () => {
4637
- const app = new import_hono12.Hono();
4936
+ const app = new import_hono14.Hono();
4638
4937
  app.get("/", async (c) => {
4639
4938
  const providers = await Promise.all([
4640
4939
  providerHealth(CLAUDE_CODE_PROVIDER, resolveClaudeExe),
@@ -4646,11 +4945,11 @@ var createProviderRoutes = () => {
4646
4945
  };
4647
4946
 
4648
4947
  // src/api/routes/scanner.routes.ts
4649
- var import_hono13 = require("hono");
4948
+ var import_hono15 = require("hono");
4650
4949
  var ALREADY_HANDLED5 = 597;
4651
4950
  var alreadyHandled5 = () => new Response(null, { status: ALREADY_HANDLED5 });
4652
4951
  var createScannerRoutes = (deps) => {
4653
- const app = new import_hono13.Hono();
4952
+ const app = new import_hono15.Hono();
4654
4953
  app.get("/api/search", async (c) => {
4655
4954
  const url = new URL(c.req.url);
4656
4955
  await deps.handleSearch(url, c.env.outgoing);
@@ -4660,11 +4959,11 @@ var createScannerRoutes = (deps) => {
4660
4959
  };
4661
4960
 
4662
4961
  // src/api/routes/sessions.routes.ts
4663
- var import_hono14 = require("hono");
4962
+ var import_hono16 = require("hono");
4664
4963
  var ALREADY_HANDLED6 = 597;
4665
4964
  var alreadyHandled6 = () => new Response(null, { status: ALREADY_HANDLED6 });
4666
4965
  var createSessionRoutes = (deps) => {
4667
- const app = new import_hono14.Hono();
4966
+ const app = new import_hono16.Hono();
4668
4967
  app.get("/count", (c) => {
4669
4968
  deps.handleSessionsCount(c.env.outgoing);
4670
4969
  return alreadyHandled6();
@@ -4739,9 +5038,9 @@ var createSessionRoutes = (deps) => {
4739
5038
  };
4740
5039
 
4741
5040
  // src/api/routes/ws.routes.ts
4742
- var import_hono15 = require("hono");
5041
+ var import_hono17 = require("hono");
4743
5042
  var createWsRoutes = (deps, upgradeWebSocket) => {
4744
- const app = new import_hono15.Hono();
5043
+ const app = new import_hono17.Hono();
4745
5044
  app.get(
4746
5045
  "/ws",
4747
5046
  upgradeWebSocket(() => {
@@ -4767,7 +5066,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
4767
5066
 
4768
5067
  // src/api/app.ts
4769
5068
  var createHonoApp = (deps, upgradeWebSocket) => {
4770
- const app = new import_hono16.Hono();
5069
+ const app = new import_hono18.Hono();
4771
5070
  const httpLog = getLogger("http");
4772
5071
  app.use("*", async (c, next) => {
4773
5072
  const start = Date.now();
@@ -4788,6 +5087,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
4788
5087
  app.use("*", authMiddleware(deps));
4789
5088
  app.onError(errorMiddleware);
4790
5089
  app.route("/healthz", createHealthRoutes(deps));
5090
+ app.route("/api/diagnostics", createDiagnosticsRoutes(deps));
4791
5091
  app.route("/", createMiscRoutes(deps));
4792
5092
  app.route("/api/sessions", createSessionRoutes(deps));
4793
5093
  app.route("/api/conversations", createConversationRoutes(deps));
@@ -4796,6 +5096,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
4796
5096
  app.route("/api/projects", createProjectRoutes(deps));
4797
5097
  app.route("/api/providers", createProviderRoutes());
4798
5098
  app.route("/api/devices", createDeviceRoutes(deps));
5099
+ app.route("/api/backup", createBackupRoutes(deps));
4799
5100
  app.route("/api/pair", createPairRoutes(deps));
4800
5101
  app.route("/api", createBrowseRoutes(deps));
4801
5102
  app.route("/", createScannerRoutes(deps));
@@ -4861,13 +5162,13 @@ async function createDirectory(parentAbsolutePath, name) {
4861
5162
  // src/conversation-cache.ts
4862
5163
  var import_scanner2 = require("@threadbase-sh/scanner");
4863
5164
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
4864
- var import_fs9 = require("fs");
5165
+ var import_fs10 = require("fs");
4865
5166
  var import_promises3 = require("fs/promises");
4866
5167
  var import_path12 = require("path");
4867
5168
  var import_promises4 = require("timers/promises");
4868
5169
 
4869
5170
  // src/db/sqlite-migrate.ts
4870
- var import_fs7 = require("fs");
5171
+ var import_fs8 = require("fs");
4871
5172
  var import_path10 = require("path");
4872
5173
  var import_url2 = require("url");
4873
5174
  var import_meta2 = {};
@@ -4886,7 +5187,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
4886
5187
  function runSqliteMigrations(db, migrationsDir) {
4887
5188
  db.exec(SCHEMA_MIGRATIONS_SQL);
4888
5189
  const dir = migrationsDir ?? (0, import_path10.join)(getMigrationsDir2(), "migrations");
4889
- const files = (0, import_fs7.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
5190
+ const files = (0, import_fs8.readdirSync)(dir).filter((f) => f.endsWith(".sql")).sort();
4890
5191
  const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
4891
5192
  const appliedSet = new Set(appliedRows.map((r) => r.id));
4892
5193
  const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
@@ -4897,7 +5198,7 @@ function runSqliteMigrations(db, migrationsDir) {
4897
5198
  skipped.push(file);
4898
5199
  continue;
4899
5200
  }
4900
- const sql = (0, import_fs7.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
5201
+ const sql = (0, import_fs8.readFileSync)((0, import_path10.join)(dir, file), "utf-8");
4901
5202
  const tx = db.transaction(() => {
4902
5203
  db.exec(sql);
4903
5204
  recordApplied.run(file, (/* @__PURE__ */ new Date()).toISOString());
@@ -4909,7 +5210,7 @@ function runSqliteMigrations(db, migrationsDir) {
4909
5210
  }
4910
5211
 
4911
5212
  // src/services/conversations/isAgentConversation.ts
4912
- var import_fs8 = require("fs");
5213
+ var import_fs9 = require("fs");
4913
5214
  var DEFAULT_AGENT_ENTRYPOINTS = /* @__PURE__ */ new Set(["sdk-cli", "claude-vscode"]);
4914
5215
  var CHUNK_BYTES = 64 * 1024;
4915
5216
  var ENTRYPOINT_PROBE = `"entrypoint":`;
@@ -4931,12 +5232,12 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
4931
5232
  if (cached2 !== void 0) return cached2;
4932
5233
  let fd;
4933
5234
  try {
4934
- fd = (0, import_fs8.openSync)(filePath, "r");
5235
+ fd = (0, import_fs9.openSync)(filePath, "r");
4935
5236
  } catch {
4936
5237
  return false;
4937
5238
  }
4938
5239
  try {
4939
- const fileSize = (0, import_fs8.statSync)(filePath).size;
5240
+ const fileSize = (0, import_fs9.statSync)(filePath).size;
4940
5241
  if (fileSize === 0) {
4941
5242
  fileDecisionCache.set(key, false);
4942
5243
  return false;
@@ -4947,7 +5248,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
4947
5248
  let carry = "";
4948
5249
  while (offset < fileSize) {
4949
5250
  const toRead = Math.min(CHUNK_BYTES, fileSize - offset);
4950
- const got = (0, import_fs8.readSync)(fd, buf, 0, toRead, offset);
5251
+ const got = (0, import_fs9.readSync)(fd, buf, 0, toRead, offset);
4951
5252
  if (got <= 0) break;
4952
5253
  const chunk = carry + buf.toString("utf8", 0, got);
4953
5254
  for (const marker of markers) {
@@ -4968,7 +5269,7 @@ function isAgentFile(filePath, entrypoints = DEFAULT_AGENT_ENTRYPOINTS) {
4968
5269
  } catch {
4969
5270
  return false;
4970
5271
  } finally {
4971
- (0, import_fs8.closeSync)(fd);
5272
+ (0, import_fs9.closeSync)(fd);
4972
5273
  }
4973
5274
  }
4974
5275
  function parseAgentEntrypointsEnv(raw) {
@@ -5543,7 +5844,7 @@ var ConversationCache = class _ConversationCache {
5543
5844
  if (!fileState) return null;
5544
5845
  let stat3;
5545
5846
  try {
5546
- stat3 = (0, import_fs9.statSync)(filePath);
5847
+ stat3 = (0, import_fs10.statSync)(filePath);
5547
5848
  } catch {
5548
5849
  return null;
5549
5850
  }
@@ -5564,17 +5865,17 @@ var ConversationCache = class _ConversationCache {
5564
5865
  );
5565
5866
  if (rows.length === 0) return { messages: [], total, fromIndex: from };
5566
5867
  const messages = [];
5567
- const fd = (0, import_fs9.openSync)(filePath, "r");
5868
+ const fd = (0, import_fs10.openSync)(filePath, "r");
5568
5869
  try {
5569
5870
  const state = (0, import_scanner2.createJsonlParseState)();
5570
5871
  for (const row of rows) {
5571
5872
  const buf = Buffer.alloc(row.byte_length);
5572
- (0, import_fs9.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
5873
+ (0, import_fs10.readSync)(fd, buf, 0, row.byte_length, row.byte_offset);
5573
5874
  const msg = (0, import_scanner2.parseJsonlLine)(buf.toString("utf-8"), state);
5574
5875
  if (msg) messages.push(msg);
5575
5876
  }
5576
5877
  } finally {
5577
- (0, import_fs9.closeSync)(fd);
5878
+ (0, import_fs10.closeSync)(fd);
5578
5879
  }
5579
5880
  return { messages, total, fromIndex: from };
5580
5881
  }
@@ -5602,14 +5903,14 @@ var ConversationCache = class _ConversationCache {
5602
5903
  isAgentFileCached(filePath) {
5603
5904
  let s;
5604
5905
  try {
5605
- s = (0, import_fs9.statSync)(filePath);
5906
+ s = (0, import_fs10.statSync)(filePath);
5606
5907
  } catch {
5607
5908
  return false;
5608
5909
  }
5609
5910
  return this.classifyAgentFile(filePath, s.mtimeMs, s.size);
5610
5911
  }
5611
5912
  static open(dbPath, tailSize = 10, migrationsDir, options) {
5612
- (0, import_fs9.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
5913
+ (0, import_fs10.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
5613
5914
  const db = new import_better_sqlite3.default(dbPath);
5614
5915
  db.pragma("journal_mode = WAL");
5615
5916
  db.pragma("foreign_keys = ON");
@@ -5796,7 +6097,7 @@ var ConversationCache = class _ConversationCache {
5796
6097
  let mtimeMs = null;
5797
6098
  let fileSize = null;
5798
6099
  try {
5799
- const s = (0, import_fs9.statSync)(m.filePath);
6100
+ const s = (0, import_fs10.statSync)(m.filePath);
5800
6101
  mtimeMs = s.mtimeMs;
5801
6102
  fileSize = s.size;
5802
6103
  } catch {
@@ -5856,8 +6157,8 @@ var ConversationCache = class _ConversationCache {
5856
6157
  let fileSize;
5857
6158
  let fd;
5858
6159
  try {
5859
- fileSize = (0, import_fs9.statSync)(filePath).size;
5860
- fd = (0, import_fs9.openSync)(filePath, "r");
6160
+ fileSize = (0, import_fs10.statSync)(filePath).size;
6161
+ fd = (0, import_fs10.openSync)(filePath, "r");
5861
6162
  } catch {
5862
6163
  return false;
5863
6164
  }
@@ -5870,7 +6171,7 @@ var ConversationCache = class _ConversationCache {
5870
6171
  while (pos > 0 && lines.length < this.tailSize * 4) {
5871
6172
  const toRead = Math.min(CHUNK, pos);
5872
6173
  pos -= toRead;
5873
- (0, import_fs9.readSync)(fd, buf, 0, toRead, pos);
6174
+ (0, import_fs10.readSync)(fd, buf, 0, toRead, pos);
5874
6175
  const chunk = buf.subarray(0, toRead).toString("utf8");
5875
6176
  const combined = chunk + partial;
5876
6177
  const parts = combined.split("\n");
@@ -5881,7 +6182,7 @@ var ConversationCache = class _ConversationCache {
5881
6182
  }
5882
6183
  if (partial) lines.push(partial);
5883
6184
  } finally {
5884
- (0, import_fs9.closeSync)(fd);
6185
+ (0, import_fs10.closeSync)(fd);
5885
6186
  }
5886
6187
  const msgs = [];
5887
6188
  for (let i = 0; i < lines.length && msgs.length < this.tailSize; i++) {
@@ -6094,7 +6395,7 @@ var ConversationCache = class _ConversationCache {
6094
6395
  * `handleGetConversation` can still serve the cached tail even when the
6095
6396
  * JSONL has been deleted.
6096
6397
  */
6097
- pruneGhostFiles(exists = import_fs9.existsSync) {
6398
+ pruneGhostFiles(exists = import_fs10.existsSync) {
6098
6399
  const rows = this.stmts.allFilePaths.all();
6099
6400
  const ghosts = [];
6100
6401
  const prune = this.db.transaction((ids) => {
@@ -6149,7 +6450,7 @@ var ConversationCache = class _ConversationCache {
6149
6450
  * Returns the removed IDs.
6150
6451
  */
6151
6452
  reconcileDeletions(livePaths, opts) {
6152
- const exists = opts?.exists ?? import_fs9.existsSync;
6453
+ const exists = opts?.exists ?? import_fs10.existsSync;
6153
6454
  const rows = this.stmts.allFilePaths.all();
6154
6455
  const removed = [];
6155
6456
  const drop = this.db.transaction((ids) => {
@@ -6184,7 +6485,7 @@ var ConversationCache = class _ConversationCache {
6184
6485
  * reports drift for the CacheIntegrityMonitor to classify. `tailed` flags
6185
6486
  * rows that still have cached history (which pruneGhostFiles would keep).
6186
6487
  */
6187
- listMissingFiles(exists = import_fs9.existsSync) {
6488
+ listMissingFiles(exists = import_fs10.existsSync) {
6188
6489
  const rows = this.stmts.allFilePathsWithTitle.all();
6189
6490
  const missing = [];
6190
6491
  for (const row of rows) {
@@ -6554,8 +6855,8 @@ async function recordUpload(pool2, instanceId, row) {
6554
6855
  }
6555
6856
 
6556
6857
  // src/handlers/handleListProjects.ts
6557
- var import_fs10 = require("fs");
6558
- var import_os6 = require("os");
6858
+ var import_fs11 = require("fs");
6859
+ var import_os7 = require("os");
6559
6860
  var import_path13 = require("path");
6560
6861
  function decodeProjectPath(dirName) {
6561
6862
  return dirName.replace(/-/g, "/");
@@ -6563,14 +6864,14 @@ function decodeProjectPath(dirName) {
6563
6864
  function handleListProjects(url, res) {
6564
6865
  const limit = Math.max(1, parseInt(url.searchParams.get("limit") ?? "50", 10) || 50);
6565
6866
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") ?? "0", 10) || 0);
6566
- const projectsDir = (0, import_path13.join)((0, import_os6.homedir)(), ".claude", "projects");
6867
+ const projectsDir = (0, import_path13.join)((0, import_os7.homedir)(), ".claude", "projects");
6567
6868
  let entries;
6568
6869
  try {
6569
- entries = (0, import_fs10.readdirSync)(projectsDir).map((dirName) => {
6870
+ entries = (0, import_fs11.readdirSync)(projectsDir).map((dirName) => {
6570
6871
  const fullPath = (0, import_path13.join)(projectsDir, dirName);
6571
6872
  let mtime = 0;
6572
6873
  try {
6573
- mtime = (0, import_fs10.statSync)(fullPath).mtimeMs;
6874
+ mtime = (0, import_fs11.statSync)(fullPath).mtimeMs;
6574
6875
  } catch {
6575
6876
  }
6576
6877
  const path = decodeProjectPath(dirName);
@@ -6685,19 +6986,19 @@ function setCacheMetadata(repo, key, value) {
6685
6986
 
6686
6987
  // src/services/cache-integrity/cacheIntegrityMonitor.ts
6687
6988
  var import_crypto9 = require("crypto");
6688
- var import_fs13 = require("fs");
6989
+ var import_fs14 = require("fs");
6689
6990
 
6690
6991
  // src/services/cache-integrity/alertStore.ts
6691
- var import_fs11 = require("fs");
6692
- var import_os7 = require("os");
6992
+ var import_fs12 = require("fs");
6993
+ var import_os8 = require("os");
6693
6994
  var import_path14 = require("path");
6694
6995
  function alertStatePath() {
6695
- const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0, import_os7.homedir)(), ".threadbase");
6996
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path14.join)((0, import_os8.homedir)(), ".threadbase");
6696
6997
  return (0, import_path14.join)(dir, "cache-alert.json");
6697
6998
  }
6698
6999
  function loadAlertState() {
6699
7000
  try {
6700
- const parsed = JSON.parse((0, import_fs11.readFileSync)(alertStatePath(), "utf-8"));
7001
+ const parsed = JSON.parse((0, import_fs12.readFileSync)(alertStatePath(), "utf-8"));
6701
7002
  return parsed && typeof parsed === "object" ? parsed : {};
6702
7003
  } catch {
6703
7004
  return {};
@@ -6705,13 +7006,13 @@ function loadAlertState() {
6705
7006
  }
6706
7007
  function saveAlertState(state) {
6707
7008
  const path = alertStatePath();
6708
- (0, import_fs11.mkdirSync)((0, import_path14.dirname)(path), { recursive: true });
6709
- (0, import_fs11.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
7009
+ (0, import_fs12.mkdirSync)((0, import_path14.dirname)(path), { recursive: true });
7010
+ (0, import_fs12.writeFileSync)(path, `${JSON.stringify(state, null, 2)}
6710
7011
  `);
6711
7012
  }
6712
7013
 
6713
7014
  // src/services/cache-integrity/backup.ts
6714
- var import_fs12 = require("fs");
7015
+ var import_fs13 = require("fs");
6715
7016
  var import_path15 = require("path");
6716
7017
  var DEFAULT_RETAIN = 3;
6717
7018
  function retainCount() {
@@ -6724,16 +7025,16 @@ function timestamp(d) {
6724
7025
  }
6725
7026
  async function backupCacheDb(db, cacheDir) {
6726
7027
  const backupsDir = (0, import_path15.join)(cacheDir, "backups");
6727
- (0, import_fs12.mkdirSync)(backupsDir, { recursive: true });
7028
+ (0, import_fs13.mkdirSync)(backupsDir, { recursive: true });
6728
7029
  const destPath = (0, import_path15.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
6729
7030
  await db.backup(destPath);
6730
7031
  const retain = retainCount();
6731
- const backups = (0, import_fs12.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
7032
+ const backups = (0, import_fs13.readdirSync)(backupsDir).filter((f) => f.startsWith("cache-") && f.endsWith(".db")).map((f) => {
6732
7033
  const full = (0, import_path15.join)(backupsDir, f);
6733
- return { full, mtime: (0, import_fs12.statSync)(full).mtimeMs };
7034
+ return { full, mtime: (0, import_fs13.statSync)(full).mtimeMs };
6734
7035
  }).sort((a, b) => b.mtime - a.mtime);
6735
7036
  for (const stale of backups.slice(retain)) {
6736
- if ((0, import_fs12.existsSync)(stale.full)) (0, import_fs12.unlinkSync)(stale.full);
7037
+ if ((0, import_fs13.existsSync)(stale.full)) (0, import_fs13.unlinkSync)(stale.full);
6737
7038
  }
6738
7039
  return destPath;
6739
7040
  }
@@ -6830,7 +7131,7 @@ var CacheIntegrityMonitor = class {
6830
7131
  * the pending record, back up on high severity, and broadcast the alert.
6831
7132
  */
6832
7133
  async runDetection(detectedAt = (/* @__PURE__ */ new Date()).toISOString()) {
6833
- const all = this.cache.listMissingFiles(import_fs13.existsSync);
7134
+ const all = this.cache.listMissingFiles(import_fs14.existsSync);
6834
7135
  const missing = all.filter((m) => !this.ignoredIds.has(m.id));
6835
7136
  if (missing.length === 0) {
6836
7137
  if (this._pending) {
@@ -6934,7 +7235,7 @@ var CacheIntegrityMonitor = class {
6934
7235
  case "prune_all": {
6935
7236
  await this.ensureBackup(pending);
6936
7237
  const backupPath = pending.backupPath;
6937
- const stillMissing = pending.missing.filter((m) => !(0, import_fs13.existsSync)(m.filePath)).map((m) => m.id);
7238
+ const stillMissing = pending.missing.filter((m) => !(0, import_fs14.existsSync)(m.filePath)).map((m) => m.id);
6938
7239
  const pruned = this.cache.dropRowsById(stillMissing);
6939
7240
  this.applyDeferredUnlinks();
6940
7241
  this.clearPending();
@@ -6996,7 +7297,7 @@ var CacheIntegrityMonitor = class {
6996
7297
 
6997
7298
  // src/services/conversations/conversationWatcher.ts
6998
7299
  var import_chokidar = __toESM(require("chokidar"), 1);
6999
- var import_fs14 = require("fs");
7300
+ var import_fs15 = require("fs");
7000
7301
  var import_promises5 = require("fs/promises");
7001
7302
  var ConversationWatcher = class {
7002
7303
  files = /* @__PURE__ */ new Map();
@@ -7022,7 +7323,7 @@ var ConversationWatcher = class {
7022
7323
  if (this.files.has(key)) return;
7023
7324
  let offset;
7024
7325
  try {
7025
- offset = (0, import_fs14.statSync)(filePath).size;
7326
+ offset = (0, import_fs15.statSync)(filePath).size;
7026
7327
  } catch {
7027
7328
  offset = 0;
7028
7329
  }
@@ -7204,14 +7505,14 @@ function findSearchTarget(messages, query) {
7204
7505
  }
7205
7506
 
7206
7507
  // src/services/conversations/pruneAgentConversations.ts
7207
- var import_fs15 = require("fs");
7508
+ var import_fs16 = require("fs");
7208
7509
  function pruneAgentConversations(cache) {
7209
7510
  const db = cache.getDatabase();
7210
7511
  const rows = db.prepare("SELECT id, file_path FROM conversation_meta").all();
7211
7512
  let pruned = 0;
7212
7513
  let missing = 0;
7213
7514
  for (const row of rows) {
7214
- if (!(0, import_fs15.existsSync)(row.file_path)) {
7515
+ if (!(0, import_fs16.existsSync)(row.file_path)) {
7215
7516
  missing += 1;
7216
7517
  continue;
7217
7518
  }
@@ -7312,22 +7613,22 @@ function refreshConversationCache(deps) {
7312
7613
  }
7313
7614
 
7314
7615
  // src/services/conversations/shouldRefreshProjectsFromHdd.ts
7315
- var import_fs16 = require("fs");
7316
- var import_os8 = require("os");
7616
+ var import_fs17 = require("fs");
7617
+ var import_os9 = require("os");
7317
7618
  var import_path16 = require("path");
7318
- var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os8.homedir)(), ".claude", "projects");
7619
+ var DEFAULT_PROJECTS_DIR = (0, import_path16.join)((0, import_os9.homedir)(), ".claude", "projects");
7319
7620
  function maxProjectsTreeMtimeMs(projectsDir) {
7320
7621
  let maxMs;
7321
7622
  try {
7322
- maxMs = (0, import_fs16.statSync)(projectsDir).mtimeMs;
7623
+ maxMs = (0, import_fs17.statSync)(projectsDir).mtimeMs;
7323
7624
  } catch {
7324
7625
  return null;
7325
7626
  }
7326
7627
  try {
7327
- for (const ent of (0, import_fs16.readdirSync)(projectsDir, { withFileTypes: true })) {
7628
+ for (const ent of (0, import_fs17.readdirSync)(projectsDir, { withFileTypes: true })) {
7328
7629
  if (!ent.isDirectory()) continue;
7329
7630
  try {
7330
- const childMs = (0, import_fs16.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
7631
+ const childMs = (0, import_fs17.statSync)((0, import_path16.join)(projectsDir, ent.name)).mtimeMs;
7331
7632
  if (childMs > maxMs) maxMs = childMs;
7332
7633
  } catch {
7333
7634
  }
@@ -8088,8 +8389,90 @@ function resolveAnswer(pending, body) {
8088
8389
  }
8089
8390
  }
8090
8391
 
8392
+ // src/services/search/searchQuery.ts
8393
+ var DEFAULT_SEARCH_LIMIT = 50;
8394
+ var MAX_SEARCH_LIMIT = 200;
8395
+ var MAX_QUERY_LENGTH = 256;
8396
+ var SearchQueryError = class extends Error {
8397
+ constructor(message, code) {
8398
+ super(message);
8399
+ this.code = code;
8400
+ }
8401
+ code;
8402
+ };
8403
+ function intOr(raw, fallback) {
8404
+ if (raw === null) return fallback;
8405
+ const n = Number.parseInt(raw, 10);
8406
+ return Number.isFinite(n) ? n : fallback;
8407
+ }
8408
+ function parseSearchQuery(params) {
8409
+ const q = (params.get("q") ?? "").trim();
8410
+ if (!q) {
8411
+ throw new SearchQueryError("Missing query parameter: q", "invalid_query");
8412
+ }
8413
+ if (q.length > MAX_QUERY_LENGTH) {
8414
+ throw new SearchQueryError(`Query exceeds ${MAX_QUERY_LENGTH} characters`, "query_too_long");
8415
+ }
8416
+ const limit = Math.min(
8417
+ Math.max(intOr(params.get("limit"), DEFAULT_SEARCH_LIMIT), 1),
8418
+ MAX_SEARCH_LIMIT
8419
+ );
8420
+ const offset = Math.max(intOr(params.get("offset"), 0), 0);
8421
+ const filters = {};
8422
+ const provider = params.get("provider");
8423
+ if (provider !== null) {
8424
+ if (!isProviderName(provider)) {
8425
+ throw new SearchQueryError(`Unknown provider: ${provider}`, "invalid_filter");
8426
+ }
8427
+ filters.provider = provider;
8428
+ }
8429
+ const projectPath = params.get("projectPath");
8430
+ if (projectPath) filters.projectPath = projectPath;
8431
+ const branch = params.get("branch");
8432
+ if (branch) filters.branch = branch;
8433
+ for (const [key, field] of [
8434
+ ["since", "since"],
8435
+ ["until", "until"]
8436
+ ]) {
8437
+ const raw = params.get(key);
8438
+ if (raw === null) continue;
8439
+ const ms = Date.parse(raw);
8440
+ if (Number.isNaN(ms)) {
8441
+ throw new SearchQueryError(`Invalid ${key}: expected an ISO 8601 date`, "invalid_filter");
8442
+ }
8443
+ filters[field] = ms;
8444
+ }
8445
+ if (filters.since != null && filters.until != null && filters.since > filters.until) {
8446
+ throw new SearchQueryError("`since` must not be after `until`", "invalid_filter");
8447
+ }
8448
+ return { q, limit, offset, filters };
8449
+ }
8450
+ function applyFilters(results, filters) {
8451
+ return results.filter((r) => {
8452
+ if (filters.provider && r.provider !== filters.provider) return false;
8453
+ if (filters.projectPath && r.projectPath !== filters.projectPath) return false;
8454
+ if (filters.branch && r.branch !== filters.branch) return false;
8455
+ if (filters.since != null || filters.until != null) {
8456
+ const ts = r.lastActivity == null ? Number.NaN : new Date(r.lastActivity).getTime();
8457
+ if (Number.isNaN(ts)) return false;
8458
+ if (filters.since != null && ts < filters.since) return false;
8459
+ if (filters.until != null && ts > filters.until) return false;
8460
+ }
8461
+ return true;
8462
+ });
8463
+ }
8464
+ function paginate(results, offset, limit) {
8465
+ const items = results.slice(offset, offset + limit);
8466
+ return {
8467
+ items,
8468
+ total: results.length,
8469
+ offset,
8470
+ hasMore: offset + items.length < results.length
8471
+ };
8472
+ }
8473
+
8091
8474
  // src/services/sessions/conversationBusy.ts
8092
- var import_fs17 = require("fs");
8475
+ var import_fs18 = require("fs");
8093
8476
  var RESUME_BUSY_WINDOW_MS = 12e4;
8094
8477
  function resolveResumeBusyWindowMs(env = process.env) {
8095
8478
  const raw = env.THREADBASE_RESUME_BUSY_WINDOW_MS;
@@ -8106,7 +8489,7 @@ function conversationBusy(input) {
8106
8489
  let lastActivityMs = null;
8107
8490
  if (input.jsonlPath) {
8108
8491
  try {
8109
- const mtimeMs = (0, import_fs17.statSync)(input.jsonlPath).mtimeMs;
8492
+ const mtimeMs = (0, import_fs18.statSync)(input.jsonlPath).mtimeMs;
8110
8493
  const age = now - mtimeMs;
8111
8494
  lastActivityMs = Math.max(0, age);
8112
8495
  const isSelfEcho = input.selfPtyEndedAt != null && mtimeMs <= input.selfPtyEndedAt + SELF_ACTIVITY_SKEW_MS;
@@ -8790,6 +9173,8 @@ var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
8790
9173
  var GRACE_MAX_DEFERS = 4;
8791
9174
  var IDLE_REAP_AFTER_MS = 6 * 60 * 60 * 1e3;
8792
9175
  var IDLE_REAP_SWEEP_MS = 5 * 60 * 1e3;
9176
+ var SEARCH_OVERFETCH = 4;
9177
+ var SEARCH_MAX_SCAN = 1e3;
8793
9178
  var RESUME_DISCOVERY_TIMEOUT_MS = 750;
8794
9179
  var DISCOVERY_TTL_MS = 15e3;
8795
9180
  var ADOPT_KILL_TIMEOUT_MS = 5e3;
@@ -9030,7 +9415,7 @@ var StreamerServer = class {
9030
9415
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
9031
9416
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
9032
9417
  this.scanProfiles = config.scanProfiles;
9033
- this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os9.homedir)(), ".codex", "sessions")];
9418
+ this.codexRoots = config.codexRoots ?? [(0, import_path18.join)((0, import_os10.homedir)(), ".codex", "sessions")];
9034
9419
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
9035
9420
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
9036
9421
  this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
@@ -9044,7 +9429,7 @@ var StreamerServer = class {
9044
9429
  this.claudeFlagsPersistable = config.claudeFlags === void 0;
9045
9430
  this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
9046
9431
  this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
9047
- this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os9.homedir)(), ".threadbase", "cache");
9432
+ this.cacheDir = config.cacheDir ?? loadCacheDir() ?? (0, import_path18.join)((0, import_os10.homedir)(), ".threadbase", "cache");
9048
9433
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
9049
9434
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
9050
9435
  this.markScannerStaleDebounced = debounce(() => {
@@ -9095,7 +9480,7 @@ var StreamerServer = class {
9095
9480
  const seqs = cache.extendMessageIndex(
9096
9481
  filePath,
9097
9482
  spans,
9098
- (0, import_fs18.statSync)(filePath),
9483
+ (0, import_fs19.statSync)(filePath),
9099
9484
  readFrom,
9100
9485
  endOffset
9101
9486
  );
@@ -9558,14 +9943,14 @@ var StreamerServer = class {
9558
9943
  }
9559
9944
  this.apnsClient = new ApnsClient(creds);
9560
9945
  const sender = new LiveActivitySender(this.apnsClient, pushRepo);
9561
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os9.hostname)();
9562
- this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os9.hostname)());
9946
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os10.hostname)();
9947
+ this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os10.hostname)());
9563
9948
  this.liveActivityRenewal = new LiveActivityRenewalScheduler({
9564
9949
  repo: pushRepo,
9565
9950
  sender,
9566
9951
  sessionStore: this.sessionStore,
9567
9952
  serverId,
9568
- serverLabel: (0, import_os9.hostname)()
9953
+ serverLabel: (0, import_os10.hostname)()
9569
9954
  });
9570
9955
  this.liveActivityRenewal.start();
9571
9956
  this.log.info("Live Activity push enabled", {
@@ -9904,7 +10289,7 @@ var StreamerServer = class {
9904
10289
  this.fileWatcher.watchDirectory(dir);
9905
10290
  }
9906
10291
  for (const dir of this.codexRoots) {
9907
- if (!(0, import_fs18.existsSync)(dir)) continue;
10292
+ if (!(0, import_fs19.existsSync)(dir)) continue;
9908
10293
  this.fileWatcher.watchDirectory(dir);
9909
10294
  }
9910
10295
  } catch (err) {
@@ -10177,7 +10562,7 @@ var StreamerServer = class {
10177
10562
  }
10178
10563
  let body;
10179
10564
  try {
10180
- body = await readBody(req);
10565
+ body = await readBody2(req);
10181
10566
  } catch (err) {
10182
10567
  const message = err instanceof Error ? err.message : "Invalid body";
10183
10568
  json(res, 400, { error: message });
@@ -10223,7 +10608,7 @@ var StreamerServer = class {
10223
10608
  nonce: sealed.nonce,
10224
10609
  ephemeralPublicKey: sealed.ephemeralPublicKey,
10225
10610
  publicUrl: this.publicUrl,
10226
- machineName: (0, import_os9.hostname)(),
10611
+ machineName: (0, import_os10.hostname)(),
10227
10612
  ...device && {
10228
10613
  deviceId: device.deviceId,
10229
10614
  deviceToken: device.deviceToken,
@@ -10351,7 +10736,7 @@ var StreamerServer = class {
10351
10736
  if (this.scanProfiles && this.scanProfiles.length > 0) {
10352
10737
  return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
10353
10738
  }
10354
- return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
10739
+ return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
10355
10740
  }
10356
10741
  /**
10357
10742
  * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
@@ -10753,20 +11138,20 @@ var StreamerServer = class {
10753
11138
  if (this.scanProfiles && this.scanProfiles.length > 0) {
10754
11139
  return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
10755
11140
  }
10756
- return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
11141
+ return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
10757
11142
  }
10758
11143
  findJsonlPath(uuid) {
10759
11144
  const filename = `${uuid}.jsonl`;
10760
11145
  for (const projectsDir of this.projectsDirs()) {
10761
- if (!(0, import_fs18.existsSync)(projectsDir)) continue;
10762
- for (const dir of (0, import_fs18.readdirSync)(projectsDir)) {
11146
+ if (!(0, import_fs19.existsSync)(projectsDir)) continue;
11147
+ for (const dir of (0, import_fs19.readdirSync)(projectsDir)) {
10763
11148
  const fp = (0, import_path18.join)(projectsDir, dir, filename);
10764
- if ((0, import_fs18.existsSync)(fp)) return fp;
11149
+ if ((0, import_fs19.existsSync)(fp)) return fp;
10765
11150
  const projectDir = (0, import_path18.join)(projectsDir, dir);
10766
11151
  try {
10767
- for (const sub of (0, import_fs18.readdirSync)(projectDir)) {
11152
+ for (const sub of (0, import_fs19.readdirSync)(projectDir)) {
10768
11153
  const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
10769
- if ((0, import_fs18.existsSync)(subagentPath)) return subagentPath;
11154
+ if ((0, import_fs19.existsSync)(subagentPath)) return subagentPath;
10770
11155
  }
10771
11156
  } catch {
10772
11157
  }
@@ -10776,7 +11161,7 @@ var StreamerServer = class {
10776
11161
  }
10777
11162
  async readCwdFromJsonl(filePath) {
10778
11163
  return new Promise((resolve2) => {
10779
- const rl = (0, import_readline.createInterface)({ input: (0, import_fs18.createReadStream)(filePath), crlfDelay: Infinity });
11164
+ const rl = (0, import_readline.createInterface)({ input: (0, import_fs19.createReadStream)(filePath), crlfDelay: Infinity });
10780
11165
  let found = false;
10781
11166
  rl.on("line", (line) => {
10782
11167
  if (found) return;
@@ -10866,7 +11251,7 @@ var StreamerServer = class {
10866
11251
  if (this.isManagedTailPath(key)) return;
10867
11252
  let mtimeMs;
10868
11253
  try {
10869
- mtimeMs = (0, import_fs18.statSync)(filePath).mtimeMs;
11254
+ mtimeMs = (0, import_fs19.statSync)(filePath).mtimeMs;
10870
11255
  } catch {
10871
11256
  return;
10872
11257
  }
@@ -11048,7 +11433,7 @@ var StreamerServer = class {
11048
11433
  if (!conv.filePath) return false;
11049
11434
  let mtimeMs = null;
11050
11435
  try {
11051
- mtimeMs = (0, import_fs18.statSync)(conv.filePath).mtimeMs;
11436
+ mtimeMs = (0, import_fs19.statSync)(conv.filePath).mtimeMs;
11052
11437
  } catch {
11053
11438
  return false;
11054
11439
  }
@@ -11303,7 +11688,7 @@ var StreamerServer = class {
11303
11688
  }
11304
11689
  let body;
11305
11690
  try {
11306
- body = await readBody(req);
11691
+ body = await readBody2(req);
11307
11692
  } catch {
11308
11693
  res.setHeader("Accept-Query", "application/json");
11309
11694
  json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
@@ -11341,17 +11726,27 @@ var StreamerServer = class {
11341
11726
  });
11342
11727
  }
11343
11728
  async handleSearch(url, res) {
11344
- const q = url.searchParams.get("q") ?? "";
11345
- if (!q) {
11346
- json(res, 400, { error: "Missing query parameter: q" });
11347
- return;
11729
+ let parsed;
11730
+ try {
11731
+ parsed = parseSearchQuery(url.searchParams);
11732
+ } catch (err) {
11733
+ if (err instanceof SearchQueryError) {
11734
+ json(res, 400, { error: err.message, code: err.code });
11735
+ return;
11736
+ }
11737
+ throw err;
11348
11738
  }
11349
- const limit = intParam(url, "limit", 50);
11739
+ const { q, limit, offset, filters } = parsed;
11740
+ const startedAt = Date.now();
11350
11741
  const scanner = await this.getScanner();
11351
11742
  const results = await (0, import_scanner3.search)(
11352
11743
  q,
11353
11744
  {
11354
- limit,
11745
+ // Fetch beyond the requested page: filters below are applied AFTER the
11746
+ // scanner returns, so slicing at `limit` here would drop results that a
11747
+ // later page should contain. Bounded so a broad query cannot pull an
11748
+ // unbounded set into memory.
11749
+ limit: Math.min(offset + limit * SEARCH_OVERFETCH, SEARCH_MAX_SCAN),
11355
11750
  include: "conversations",
11356
11751
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
11357
11752
  ...this.codexScanOpts()
@@ -11375,13 +11770,24 @@ var StreamerServer = class {
11375
11770
  lastActivity: r.meta.timestamp,
11376
11771
  firstMessage: r.meta.firstMessage ?? void 0,
11377
11772
  lastMessage: r.meta.lastMessage ?? void 0,
11378
- provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER
11773
+ provider: r.meta.provider ?? CLAUDE_CODE_PROVIDER,
11774
+ // The scanner already computes relevance and match snippets; the previous
11775
+ // adapter discarded both, so results arrived in an unexplained order with
11776
+ // no indication of WHY anything matched.
11777
+ score: r.score,
11778
+ matches: Array.isArray(r.matches) ? r.matches.map((m) => ({
11779
+ field: m.field,
11780
+ snippet: m.snippet
11781
+ })) : []
11379
11782
  }));
11783
+ const page = paginate(applyFilters(adapted, filters), offset, limit);
11380
11784
  json(res, 200, {
11381
- conversations: adapted,
11382
- hasMore: false,
11383
- offset: 0,
11384
- total: adapted.length
11785
+ conversations: page.items,
11786
+ hasMore: page.hasMore,
11787
+ offset: page.offset,
11788
+ total: page.total,
11789
+ // Query timing, so a slow search is diagnosable rather than merely felt.
11790
+ tookMs: Date.now() - startedAt
11385
11791
  });
11386
11792
  }
11387
11793
  async handleListSessions(url, res) {
@@ -11427,7 +11833,7 @@ var StreamerServer = class {
11427
11833
  if (this.rejectIfWarmingUp(res)) return;
11428
11834
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
11429
11835
  if (session) {
11430
- if (!(0, import_fs18.existsSync)(session.projectPath)) {
11836
+ if (!(0, import_fs19.existsSync)(session.projectPath)) {
11431
11837
  session.failureReason = `Project directory not found: ${session.projectPath}`;
11432
11838
  }
11433
11839
  const reconciled = this.withReconciledLifecycle([session])[0];
@@ -11454,7 +11860,7 @@ var StreamerServer = class {
11454
11860
  json(res, 404, { error: "Session not found" });
11455
11861
  }
11456
11862
  async handleResume(req, res) {
11457
- const body = await readBody(req);
11863
+ const body = await readBody2(req);
11458
11864
  const sessionId = body.sessionId ?? body.conversationId;
11459
11865
  if (!sessionId) {
11460
11866
  json(res, 400, { error: "Missing sessionId" });
@@ -11586,7 +11992,7 @@ var StreamerServer = class {
11586
11992
  return;
11587
11993
  }
11588
11994
  if (this.agentConfig.enabled) {
11589
- const body2 = await readBody(req);
11995
+ const body2 = await readBody2(req);
11590
11996
  const cache = this.cache;
11591
11997
  if (!cache) {
11592
11998
  json(res, 503, {
@@ -11605,7 +12011,7 @@ var StreamerServer = class {
11605
12011
  json(res, result.status, result.body);
11606
12012
  return;
11607
12013
  }
11608
- const body = await readBody(req);
12014
+ const body = await readBody2(req);
11609
12015
  const { input, keys } = body;
11610
12016
  let idempotencyKey;
11611
12017
  try {
@@ -11769,7 +12175,7 @@ var StreamerServer = class {
11769
12175
  });
11770
12176
  }
11771
12177
  async handleSendAnswer(sessionId, req, res) {
11772
- const body = await readBody(req);
12178
+ const body = await readBody2(req);
11773
12179
  const pending = this.pendingQuestions.get(sessionId);
11774
12180
  const resolution = resolveAnswer(pending, body);
11775
12181
  if (!resolution.ok) {
@@ -11798,7 +12204,7 @@ var StreamerServer = class {
11798
12204
  json(res, 400, { error: "Session has no project path" });
11799
12205
  return;
11800
12206
  }
11801
- const body = await readBody(req);
12207
+ const body = await readBody2(req);
11802
12208
  const { filename, mimeType, dataBase64 } = body ?? {};
11803
12209
  if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
11804
12210
  json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
@@ -12000,7 +12406,7 @@ var StreamerServer = class {
12000
12406
  return;
12001
12407
  }
12002
12408
  if (this.agentConfig.enabled) {
12003
- const body2 = await readBody(req);
12409
+ const body2 = await readBody2(req);
12004
12410
  const result = await handleStartAgentSession(body2, {
12005
12411
  sessionStore: this.sessionStore,
12006
12412
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
@@ -12014,7 +12420,7 @@ var StreamerServer = class {
12014
12420
  }
12015
12421
  return;
12016
12422
  }
12017
- const body = await readBody(req);
12423
+ const body = await readBody2(req);
12018
12424
  const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
12019
12425
  if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
12020
12426
  json(res, 400, { error: "Invalid provider" });
@@ -12151,7 +12557,7 @@ var StreamerServer = class {
12151
12557
  // file isn't slurped in full.
12152
12558
  readFirstLineSessionId(filePath) {
12153
12559
  try {
12154
- const content = (0, import_fs18.readFileSync)(filePath, "utf8");
12560
+ const content = (0, import_fs19.readFileSync)(filePath, "utf8");
12155
12561
  const nl = content.indexOf("\n");
12156
12562
  const firstLine = nl === -1 ? content : content.slice(0, nl);
12157
12563
  if (!firstLine.trim()) return null;
@@ -12166,7 +12572,7 @@ var StreamerServer = class {
12166
12572
  // was passed to Claude via --session-id so the filename matches from the start.
12167
12573
  watchForJsonl(sessionId, projectPath) {
12168
12574
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
12169
- const projectsDir = (0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects", encoded);
12575
+ const projectsDir = (0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects", encoded);
12170
12576
  const expectedFile = `${sessionId}.jsonl`;
12171
12577
  const filePath = (0, import_path18.join)(projectsDir, expectedFile);
12172
12578
  const deadline = Date.now() + 12e4;
@@ -12186,11 +12592,11 @@ var StreamerServer = class {
12186
12592
  cleanup();
12187
12593
  return;
12188
12594
  }
12189
- let resolvedFilePath = (0, import_fs18.existsSync)(filePath) ? filePath : null;
12190
- if (!resolvedFilePath && (0, import_fs18.existsSync)(projectsDir)) {
12595
+ let resolvedFilePath = (0, import_fs19.existsSync)(filePath) ? filePath : null;
12596
+ if (!resolvedFilePath && (0, import_fs19.existsSync)(projectsDir)) {
12191
12597
  try {
12192
12598
  const now = Date.now();
12193
- const match = (0, import_fs18.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
12599
+ const match = (0, import_fs19.readdirSync)(projectsDir).filter((f) => f.endsWith(".jsonl")).map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(projectsDir, f)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
12194
12600
  ({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
12195
12601
  ).sort((a, b) => b.mtime - a.mtime)[0];
12196
12602
  if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
@@ -12201,7 +12607,7 @@ var StreamerServer = class {
12201
12607
  cleanup();
12202
12608
  this.sessionFileMap.set(sessionId, resolvedFilePath);
12203
12609
  try {
12204
- const existing = (0, import_fs18.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
12610
+ const existing = (0, import_fs19.readFileSync)(resolvedFilePath, "utf8").split("\n").filter(Boolean);
12205
12611
  if (existing.length > 0) {
12206
12612
  this.broadcastConversationLines(sessionId, existing);
12207
12613
  }
@@ -12225,7 +12631,7 @@ var StreamerServer = class {
12225
12631
  if (this.sessionFileMap.has(sessionId)) return;
12226
12632
  try {
12227
12633
  require("fs").mkdirSync(projectsDir, { recursive: true });
12228
- watcher = (0, import_fs18.watch)(projectsDir, tryWire);
12634
+ watcher = (0, import_fs19.watch)(projectsDir, tryWire);
12229
12635
  watcher.on("error", cleanup);
12230
12636
  } catch {
12231
12637
  }
@@ -12254,7 +12660,7 @@ var StreamerServer = class {
12254
12660
  };
12255
12661
  const matchesProjectPath = (candidatePath) => {
12256
12662
  try {
12257
- const firstLine = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
12663
+ const firstLine = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n", 1)[0];
12258
12664
  if (!firstLine) return null;
12259
12665
  const parsed = JSON.parse(firstLine);
12260
12666
  if (parsed?.type !== "session_meta") return null;
@@ -12283,15 +12689,15 @@ var StreamerServer = class {
12283
12689
  );
12284
12690
  for (const root of this.codexRoots) {
12285
12691
  const sessionsDir = (0, import_path18.join)(root, dateDir);
12286
- if (!(0, import_fs18.existsSync)(sessionsDir)) continue;
12692
+ if (!(0, import_fs19.existsSync)(sessionsDir)) continue;
12287
12693
  let candidateFiles;
12288
12694
  try {
12289
- candidateFiles = (0, import_fs18.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
12695
+ candidateFiles = (0, import_fs19.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
12290
12696
  } catch {
12291
12697
  continue;
12292
12698
  }
12293
12699
  const nowMs = Date.now();
12294
- const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs18.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
12700
+ const recentCandidates = candidateFiles.map((f) => ({ f, mtime: (0, import_fs19.statSync)((0, import_path18.join)(sessionsDir, f)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b) => b.mtime - a.mtime);
12295
12701
  for (const { f } of recentCandidates) {
12296
12702
  const candidatePath = (0, import_path18.join)(sessionsDir, f);
12297
12703
  const match = matchesProjectPath(candidatePath);
@@ -12303,7 +12709,7 @@ var StreamerServer = class {
12303
12709
  this.sessionFileMap.set(sessionId, candidatePath);
12304
12710
  this.fileWatcher.watch(candidatePath);
12305
12711
  try {
12306
- const existing = (0, import_fs18.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
12712
+ const existing = (0, import_fs19.readFileSync)(candidatePath, "utf8").split("\n").filter(Boolean);
12307
12713
  if (existing.length > 0) {
12308
12714
  this.broadcastConversationLines(sessionId, existing);
12309
12715
  }
@@ -12372,7 +12778,7 @@ var StreamerServer = class {
12372
12778
  });
12373
12779
  return;
12374
12780
  }
12375
- const body = await readBody(req);
12781
+ const body = await readBody2(req);
12376
12782
  const { path: relativePath, name } = body;
12377
12783
  if (!name || typeof name !== "string") {
12378
12784
  json(res, 400, { error: "Missing name field" });
@@ -12402,7 +12808,7 @@ var StreamerServer = class {
12402
12808
  }
12403
12809
  let parsed;
12404
12810
  try {
12405
- parsed = await readBody(req);
12811
+ parsed = await readBody2(req);
12406
12812
  } catch {
12407
12813
  json(res, 400, { error: "Invalid JSON" });
12408
12814
  return;
@@ -12459,7 +12865,7 @@ var StreamerServer = class {
12459
12865
  }
12460
12866
  let parsed;
12461
12867
  try {
12462
- parsed = await readBody(req);
12868
+ parsed = await readBody2(req);
12463
12869
  } catch {
12464
12870
  json(res, 400, { error: "Invalid JSON" });
12465
12871
  return;
@@ -12518,7 +12924,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
12518
12924
  }
12519
12925
  function classifyResumability(cwd) {
12520
12926
  if (!cwd) return { resumable: true };
12521
- if ((0, import_fs18.existsSync)(cwd)) return { resumable: true };
12927
+ if ((0, import_fs19.existsSync)(cwd)) return { resumable: true };
12522
12928
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
12523
12929
  return {
12524
12930
  resumable: false,
@@ -12636,7 +13042,7 @@ function parseSessionListQuery(url) {
12636
13042
  const cursor = url.searchParams.get("cursor") ?? void 0;
12637
13043
  return { query: { limit, sortBy, order, status, cursor } };
12638
13044
  }
12639
- function readBody(req) {
13045
+ function readBody2(req) {
12640
13046
  return new Promise((resolve2, reject) => {
12641
13047
  const chunks = [];
12642
13048
  req.on("data", (chunk) => chunks.push(chunk));