@threadbase-sh/streamer 1.39.0 → 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;
@@ -8867,6 +9252,20 @@ var StreamerServer = class {
8867
9252
  // Set by onConversationChanged while a scan is in-flight; getScanner() does
8868
9253
  // a single rescan after the current one completes instead of restarting it.
8869
9254
  scannerStale = false;
9255
+ // WHICH files scannerStale is about. A directory event names exactly one
9256
+ // JSONL, and the only correct response is refreshFile() on that one file —
9257
+ // but scannerStale alone carries no identity, so honoring it used to mean a
9258
+ // full-tree rescan. On the non-persistent scanner this server actually runs
9259
+ // (buildStatCache => persistent:false, see listen()), scan() opens by
9260
+ // clearing metadataCache AND conversationLRU — so one live session appending
9261
+ // to its own transcript threw away every OTHER conversation's parsed
9262
+ // snapshot, and the next full fetch of an unrelated conversation re-parsed
9263
+ // it from disk (745-2877ms on a 5MB/1112-message history) while the
9264
+ // per-file paginated path stayed at ~20ms throughout. Populated alongside
9265
+ // scannerStale and drained with it by takeStaleFiles(); an armed flag with
9266
+ // an EMPTY set means "stale, source unknown" and still falls back to the
9267
+ // full rescan.
9268
+ staleFiles = /* @__PURE__ */ new Set();
8870
9269
  // Single-flight guard for the background disk reconcile: a burst of list
8871
9270
  // polls during active session writes shares one rescan instead of queueing
8872
9271
  // a full rescan per request.
@@ -9016,7 +9415,7 @@ var StreamerServer = class {
9016
9415
  this.skipStartupWarmup = config.skipStartupWarmup ?? false;
9017
9416
  this.scannerPersistenceDisabled = config.scannerPersistent === false;
9018
9417
  this.scanProfiles = config.scanProfiles;
9019
- 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")];
9020
9419
  this.ptyGracePeriodMs = config.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
9021
9420
  this.defaultSystemPrompt = config.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
9022
9421
  this.featureFlags = resolveFeatureFlags({ cli: config.featureFlags, yaml: loadFeatureFlags() });
@@ -9030,12 +9429,15 @@ var StreamerServer = class {
9030
9429
  this.claudeFlagsPersistable = config.claudeFlags === void 0;
9031
9430
  this.claudeFlags = config.claudeFlags ?? loadClaudeFlags();
9032
9431
  this.claudeExtraArgs = config.claudeExtraArgs ?? loadClaudeExtraArgs();
9033
- 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");
9034
9433
  this.tailSize = config.tailSize ?? loadTailSize() ?? 10;
9035
9434
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config.directoryScanDebounceMs ?? 1e3;
9036
9435
  this.markScannerStaleDebounced = debounce(() => {
9037
9436
  if (this.scannerReady) this.scannerStale = true;
9038
- else this.scanner = null;
9437
+ else {
9438
+ this.scanner = null;
9439
+ this.staleFiles.clear();
9440
+ }
9039
9441
  }, this.directoryDebounceMs);
9040
9442
  this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
9041
9443
  this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
@@ -9078,7 +9480,7 @@ var StreamerServer = class {
9078
9480
  const seqs = cache.extendMessageIndex(
9079
9481
  filePath,
9080
9482
  spans,
9081
- (0, import_fs18.statSync)(filePath),
9483
+ (0, import_fs19.statSync)(filePath),
9082
9484
  readFrom,
9083
9485
  endOffset
9084
9486
  );
@@ -9128,6 +9530,7 @@ var StreamerServer = class {
9128
9530
  if (!tailed) this.maybeAttachExternalTail(filePath);
9129
9531
  this.sweepIdleExternalTails();
9130
9532
  this.cache?.invalidateByFilePath(filePath, { skipIfTailed: true });
9533
+ this.staleFiles.add(filePath);
9131
9534
  this.markScannerStaleDebounced();
9132
9535
  this.log.debug?.(`Scanner invalidated by directory event: ${filePath}`, {
9133
9536
  filePath,
@@ -9540,14 +9943,14 @@ var StreamerServer = class {
9540
9943
  }
9541
9944
  this.apnsClient = new ApnsClient(creds);
9542
9945
  const sender = new LiveActivitySender(this.apnsClient, pushRepo);
9543
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os9.hostname)();
9544
- 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)());
9545
9948
  this.liveActivityRenewal = new LiveActivityRenewalScheduler({
9546
9949
  repo: pushRepo,
9547
9950
  sender,
9548
9951
  sessionStore: this.sessionStore,
9549
9952
  serverId,
9550
- serverLabel: (0, import_os9.hostname)()
9953
+ serverLabel: (0, import_os10.hostname)()
9551
9954
  });
9552
9955
  this.liveActivityRenewal.start();
9553
9956
  this.log.info("Live Activity push enabled", {
@@ -9886,7 +10289,7 @@ var StreamerServer = class {
9886
10289
  this.fileWatcher.watchDirectory(dir);
9887
10290
  }
9888
10291
  for (const dir of this.codexRoots) {
9889
- if (!(0, import_fs18.existsSync)(dir)) continue;
10292
+ if (!(0, import_fs19.existsSync)(dir)) continue;
9890
10293
  this.fileWatcher.watchDirectory(dir);
9891
10294
  }
9892
10295
  } catch (err) {
@@ -10159,7 +10562,7 @@ var StreamerServer = class {
10159
10562
  }
10160
10563
  let body;
10161
10564
  try {
10162
- body = await readBody(req);
10565
+ body = await readBody2(req);
10163
10566
  } catch (err) {
10164
10567
  const message = err instanceof Error ? err.message : "Invalid body";
10165
10568
  json(res, 400, { error: message });
@@ -10205,7 +10608,7 @@ var StreamerServer = class {
10205
10608
  nonce: sealed.nonce,
10206
10609
  ephemeralPublicKey: sealed.ephemeralPublicKey,
10207
10610
  publicUrl: this.publicUrl,
10208
- machineName: (0, import_os9.hostname)(),
10611
+ machineName: (0, import_os10.hostname)(),
10209
10612
  ...device && {
10210
10613
  deviceId: device.deviceId,
10211
10614
  deviceToken: device.deviceToken,
@@ -10333,7 +10736,7 @@ var StreamerServer = class {
10333
10736
  if (this.scanProfiles && this.scanProfiles.length > 0) {
10334
10737
  return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
10335
10738
  }
10336
- return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
10739
+ return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
10337
10740
  }
10338
10741
  /**
10339
10742
  * Full-glob scan + cache upsert/delete reconcile. Used by ?refresh=1 and by
@@ -10374,21 +10777,46 @@ var StreamerServer = class {
10374
10777
  // so a burst of list polls during active session writes shares one rescan
10375
10778
  // rather than queueing a full rescan each; tracked so close() awaits the
10376
10779
  // in-flight cache write before shutting the DB.
10377
- startBackgroundConversationReconcile() {
10780
+ startBackgroundConversationReconcile(mode = "full") {
10378
10781
  if (this.conversationReconcileInFlight) return;
10379
- const task = this.reconcileConversationsCacheFromDisk().finally(() => {
10782
+ const paths = mode === "files" ? this.takeStaleFiles() : [];
10783
+ const task = (paths.length > 0 ? this.reconcileStaleFilesFromDisk(paths) : this.reconcileConversationsCacheFromDisk()).finally(() => {
10380
10784
  this.conversationReconcileInFlight = null;
10381
10785
  });
10382
10786
  this.conversationReconcileInFlight = task;
10383
10787
  this.trackCacheWrite(task);
10384
10788
  }
10385
- shouldAutoReconcileConversationList() {
10386
- if (!this.cache) return false;
10387
- if (this.scannerStale) return true;
10388
- if (!this.conversationsRepo || !this.cacheMetadataRepo) return false;
10789
+ // "files": a directory event named specific JSONLs, so refresh only those.
10790
+ // "full": disk drifted in ways a per-file refresh can't see (a project dir
10791
+ // appeared, rows vanished), so walk the tree. Order matters — the staleness
10792
+ // check short-circuits first so the HDD freshness probe stays off the hot
10793
+ // poll path, exactly as it did when this returned a boolean.
10794
+ conversationReconcileMode() {
10795
+ if (!this.cache) return null;
10796
+ if (this.scannerStale) return "files";
10797
+ if (!this.conversationsRepo || !this.cacheMetadataRepo) return null;
10389
10798
  return shouldRefreshProjectsFromHdd(this.conversationsRepo, this.cacheMetadataRepo, {
10390
10799
  projectsDirs: this.projectsDirsForFreshnessCheck()
10391
- });
10800
+ }) ? "full" : null;
10801
+ }
10802
+ // The per-file half of reconcileConversationsCacheFromDisk: re-index just the
10803
+ // changed JSONLs and upsert their rows. No reconcileDeletions here — that
10804
+ // needs the whole live-path set, and deletions already have their own path
10805
+ // (onFileDeleted -> invalidateByFilePath). New projects still arrive via the
10806
+ // HDD-freshness "full" mode.
10807
+ async reconcileStaleFilesFromDisk(paths) {
10808
+ if (!this.cache) return;
10809
+ const scanner = await this.getScanner(true);
10810
+ const metas = await this.refreshStaleFiles(scanner, paths);
10811
+ if (metas.length === 0) return;
10812
+ try {
10813
+ this.cache.upsertFromScannerMeta(metas);
10814
+ } catch (err) {
10815
+ this.log.warn(
10816
+ `stale-file reconcile failed: ${err instanceof Error ? err.message : String(err)}`,
10817
+ { event: "conversations.reconcile_failed" }
10818
+ );
10819
+ }
10392
10820
  }
10393
10821
  async handleListConversations(url, res) {
10394
10822
  if (this.rejectIfWarmingUp(res)) return;
@@ -10398,10 +10826,11 @@ var StreamerServer = class {
10398
10826
  const project = url.searchParams.get("project") ?? void 0;
10399
10827
  const providerFilter = url.searchParams.get("provider") ?? void 0;
10400
10828
  const bustCache = url.searchParams.get("refresh") === "1";
10401
- if (this.cache && (bustCache || this.shouldAutoReconcileConversationList())) {
10829
+ const reconcileMode = this.conversationReconcileMode();
10830
+ if (this.cache && (bustCache || reconcileMode)) {
10402
10831
  const canServeStale = !bustCache && this.cache.listConversations({ limit: 0, offset: 0 }).total > 0;
10403
10832
  if (canServeStale) {
10404
- this.startBackgroundConversationReconcile();
10833
+ this.startBackgroundConversationReconcile(reconcileMode ?? "full");
10405
10834
  } else {
10406
10835
  const shouldEmitProgress = createScanProgressThrottle();
10407
10836
  await this.withWarmup(
@@ -10601,21 +11030,54 @@ var StreamerServer = class {
10601
11030
  options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
10602
11031
  );
10603
11032
  }
11033
+ // Drain the stale set and disarm the flag together. The caller owns the
11034
+ // returned paths: clearing before the refresh means events that land DURING
11035
+ // it re-arm the flag and get their own pass instead of being swallowed.
11036
+ takeStaleFiles() {
11037
+ const paths = [...this.staleFiles];
11038
+ this.staleFiles.clear();
11039
+ this.scannerStale = false;
11040
+ return paths;
11041
+ }
11042
+ // Reconcile exactly the JSONLs a directory event named. Failures are logged
11043
+ // and swallowed per file: one unreadable transcript must not abort the
11044
+ // others, and the file simply stays on its previous snapshot until the next
11045
+ // event — the same outcome the full rescan gave on a parse failure.
11046
+ async refreshStaleFiles(scanner, paths) {
11047
+ const metas = await Promise.all(
11048
+ paths.map(
11049
+ (filePath) => scanner.refreshFile(filePath).catch((err) => {
11050
+ this.log.warn("scanner.refreshFile: failed", {
11051
+ event: "scanner.refresh_failed",
11052
+ filePath,
11053
+ trigger: "directory-event",
11054
+ err
11055
+ });
11056
+ return null;
11057
+ })
11058
+ )
11059
+ );
11060
+ return metas.filter((m) => m !== null);
11061
+ }
10604
11062
  async getScanner(skipStaleRescan = false) {
10605
11063
  if (this.scannerReady) {
10606
11064
  await this.scannerReady;
10607
11065
  if (this.scanner) {
10608
11066
  if (skipStaleRescan) return this.scanner;
10609
11067
  if (this.scannerStale) {
10610
- this.scannerStale = false;
10611
- this.scanner = null;
10612
- this.scannerReady = null;
10613
- return this.getScanner();
11068
+ const paths = this.takeStaleFiles();
11069
+ if (paths.length === 0) {
11070
+ this.scanner = null;
11071
+ this.scannerReady = null;
11072
+ return this.getScanner();
11073
+ }
11074
+ await this.refreshStaleFiles(this.scanner, paths);
11075
+ return this.scanner ?? this.getScanner();
10614
11076
  }
10615
11077
  return this.scanner;
10616
11078
  }
10617
11079
  }
10618
- this.scannerStale = false;
11080
+ this.takeStaleFiles();
10619
11081
  const statCache = this.buildStatCache(this.scanner);
10620
11082
  this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
10621
11083
  this.allScanners.add(this.scanner);
@@ -10649,7 +11111,7 @@ var StreamerServer = class {
10649
11111
  // getScanner() anti-infinite-loop guard is preserved.
10650
11112
  async rescanForRefresh(onProgress) {
10651
11113
  if (this.scannerReady) await this.scannerReady;
10652
- this.scannerStale = false;
11114
+ this.takeStaleFiles();
10653
11115
  if (!this.scanner) {
10654
11116
  this.scanner = new import_scanner3.ConversationScanner();
10655
11117
  this.allScanners.add(this.scanner);
@@ -10676,20 +11138,20 @@ var StreamerServer = class {
10676
11138
  if (this.scanProfiles && this.scanProfiles.length > 0) {
10677
11139
  return this.scanProfiles.filter((p) => p.enabled).map((p) => (0, import_path18.join)(p.configDir, "projects"));
10678
11140
  }
10679
- return [(0, import_path18.join)((0, import_os9.homedir)(), ".claude", "projects")];
11141
+ return [(0, import_path18.join)((0, import_os10.homedir)(), ".claude", "projects")];
10680
11142
  }
10681
11143
  findJsonlPath(uuid) {
10682
11144
  const filename = `${uuid}.jsonl`;
10683
11145
  for (const projectsDir of this.projectsDirs()) {
10684
- if (!(0, import_fs18.existsSync)(projectsDir)) continue;
10685
- 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)) {
10686
11148
  const fp = (0, import_path18.join)(projectsDir, dir, filename);
10687
- if ((0, import_fs18.existsSync)(fp)) return fp;
11149
+ if ((0, import_fs19.existsSync)(fp)) return fp;
10688
11150
  const projectDir = (0, import_path18.join)(projectsDir, dir);
10689
11151
  try {
10690
- for (const sub of (0, import_fs18.readdirSync)(projectDir)) {
11152
+ for (const sub of (0, import_fs19.readdirSync)(projectDir)) {
10691
11153
  const subagentPath = (0, import_path18.join)(projectDir, sub, "subagents", filename);
10692
- if ((0, import_fs18.existsSync)(subagentPath)) return subagentPath;
11154
+ if ((0, import_fs19.existsSync)(subagentPath)) return subagentPath;
10693
11155
  }
10694
11156
  } catch {
10695
11157
  }
@@ -10699,7 +11161,7 @@ var StreamerServer = class {
10699
11161
  }
10700
11162
  async readCwdFromJsonl(filePath) {
10701
11163
  return new Promise((resolve2) => {
10702
- 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 });
10703
11165
  let found = false;
10704
11166
  rl.on("line", (line) => {
10705
11167
  if (found) return;
@@ -10789,7 +11251,7 @@ var StreamerServer = class {
10789
11251
  if (this.isManagedTailPath(key)) return;
10790
11252
  let mtimeMs;
10791
11253
  try {
10792
- mtimeMs = (0, import_fs18.statSync)(filePath).mtimeMs;
11254
+ mtimeMs = (0, import_fs19.statSync)(filePath).mtimeMs;
10793
11255
  } catch {
10794
11256
  return;
10795
11257
  }
@@ -10971,7 +11433,7 @@ var StreamerServer = class {
10971
11433
  if (!conv.filePath) return false;
10972
11434
  let mtimeMs = null;
10973
11435
  try {
10974
- mtimeMs = (0, import_fs18.statSync)(conv.filePath).mtimeMs;
11436
+ mtimeMs = (0, import_fs19.statSync)(conv.filePath).mtimeMs;
10975
11437
  } catch {
10976
11438
  return false;
10977
11439
  }
@@ -11226,7 +11688,7 @@ var StreamerServer = class {
11226
11688
  }
11227
11689
  let body;
11228
11690
  try {
11229
- body = await readBody(req);
11691
+ body = await readBody2(req);
11230
11692
  } catch {
11231
11693
  res.setHeader("Accept-Query", "application/json");
11232
11694
  json(res, 422, { error: "Malformed JSON body", code: "invalid_query" });
@@ -11264,17 +11726,27 @@ var StreamerServer = class {
11264
11726
  });
11265
11727
  }
11266
11728
  async handleSearch(url, res) {
11267
- const q = url.searchParams.get("q") ?? "";
11268
- if (!q) {
11269
- json(res, 400, { error: "Missing query parameter: q" });
11270
- 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;
11271
11738
  }
11272
- const limit = intParam(url, "limit", 50);
11739
+ const { q, limit, offset, filters } = parsed;
11740
+ const startedAt = Date.now();
11273
11741
  const scanner = await this.getScanner();
11274
11742
  const results = await (0, import_scanner3.search)(
11275
11743
  q,
11276
11744
  {
11277
- 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),
11278
11750
  include: "conversations",
11279
11751
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
11280
11752
  ...this.codexScanOpts()
@@ -11298,13 +11770,24 @@ var StreamerServer = class {
11298
11770
  lastActivity: r.meta.timestamp,
11299
11771
  firstMessage: r.meta.firstMessage ?? void 0,
11300
11772
  lastMessage: r.meta.lastMessage ?? void 0,
11301
- 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
+ })) : []
11302
11782
  }));
11783
+ const page = paginate(applyFilters(adapted, filters), offset, limit);
11303
11784
  json(res, 200, {
11304
- conversations: adapted,
11305
- hasMore: false,
11306
- offset: 0,
11307
- 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
11308
11791
  });
11309
11792
  }
11310
11793
  async handleListSessions(url, res) {
@@ -11350,7 +11833,7 @@ var StreamerServer = class {
11350
11833
  if (this.rejectIfWarmingUp(res)) return;
11351
11834
  const session = this.sessionStore.get(sessionId, this.ptyAttachedIds());
11352
11835
  if (session) {
11353
- if (!(0, import_fs18.existsSync)(session.projectPath)) {
11836
+ if (!(0, import_fs19.existsSync)(session.projectPath)) {
11354
11837
  session.failureReason = `Project directory not found: ${session.projectPath}`;
11355
11838
  }
11356
11839
  const reconciled = this.withReconciledLifecycle([session])[0];
@@ -11377,7 +11860,7 @@ var StreamerServer = class {
11377
11860
  json(res, 404, { error: "Session not found" });
11378
11861
  }
11379
11862
  async handleResume(req, res) {
11380
- const body = await readBody(req);
11863
+ const body = await readBody2(req);
11381
11864
  const sessionId = body.sessionId ?? body.conversationId;
11382
11865
  if (!sessionId) {
11383
11866
  json(res, 400, { error: "Missing sessionId" });
@@ -11509,7 +11992,7 @@ var StreamerServer = class {
11509
11992
  return;
11510
11993
  }
11511
11994
  if (this.agentConfig.enabled) {
11512
- const body2 = await readBody(req);
11995
+ const body2 = await readBody2(req);
11513
11996
  const cache = this.cache;
11514
11997
  if (!cache) {
11515
11998
  json(res, 503, {
@@ -11528,7 +12011,7 @@ var StreamerServer = class {
11528
12011
  json(res, result.status, result.body);
11529
12012
  return;
11530
12013
  }
11531
- const body = await readBody(req);
12014
+ const body = await readBody2(req);
11532
12015
  const { input, keys } = body;
11533
12016
  let idempotencyKey;
11534
12017
  try {
@@ -11692,7 +12175,7 @@ var StreamerServer = class {
11692
12175
  });
11693
12176
  }
11694
12177
  async handleSendAnswer(sessionId, req, res) {
11695
- const body = await readBody(req);
12178
+ const body = await readBody2(req);
11696
12179
  const pending = this.pendingQuestions.get(sessionId);
11697
12180
  const resolution = resolveAnswer(pending, body);
11698
12181
  if (!resolution.ok) {
@@ -11721,7 +12204,7 @@ var StreamerServer = class {
11721
12204
  json(res, 400, { error: "Session has no project path" });
11722
12205
  return;
11723
12206
  }
11724
- const body = await readBody(req);
12207
+ const body = await readBody2(req);
11725
12208
  const { filename, mimeType, dataBase64 } = body ?? {};
11726
12209
  if (typeof filename !== "string" || typeof mimeType !== "string" || typeof dataBase64 !== "string") {
11727
12210
  json(res, 400, { error: "Missing filename, mimeType, or dataBase64" });
@@ -11923,7 +12406,7 @@ var StreamerServer = class {
11923
12406
  return;
11924
12407
  }
11925
12408
  if (this.agentConfig.enabled) {
11926
- const body2 = await readBody(req);
12409
+ const body2 = await readBody2(req);
11927
12410
  const result = await handleStartAgentSession(body2, {
11928
12411
  sessionStore: this.sessionStore,
11929
12412
  // biome-ignore lint/style/noNonNullAssertion: agentClient is set when agentConfig.enabled is true
@@ -11937,7 +12420,7 @@ var StreamerServer = class {
11937
12420
  }
11938
12421
  return;
11939
12422
  }
11940
- const body = await readBody(req);
12423
+ const body = await readBody2(req);
11941
12424
  const { path: relativePath, provider: requestedProvider, systemPrompt: clientPrompt } = body;
11942
12425
  if (requestedProvider !== void 0 && !isProviderName(requestedProvider)) {
11943
12426
  json(res, 400, { error: "Invalid provider" });
@@ -12074,7 +12557,7 @@ var StreamerServer = class {
12074
12557
  // file isn't slurped in full.
12075
12558
  readFirstLineSessionId(filePath) {
12076
12559
  try {
12077
- const content = (0, import_fs18.readFileSync)(filePath, "utf8");
12560
+ const content = (0, import_fs19.readFileSync)(filePath, "utf8");
12078
12561
  const nl = content.indexOf("\n");
12079
12562
  const firstLine = nl === -1 ? content : content.slice(0, nl);
12080
12563
  if (!firstLine.trim()) return null;
@@ -12089,7 +12572,7 @@ var StreamerServer = class {
12089
12572
  // was passed to Claude via --session-id so the filename matches from the start.
12090
12573
  watchForJsonl(sessionId, projectPath) {
12091
12574
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
12092
- 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);
12093
12576
  const expectedFile = `${sessionId}.jsonl`;
12094
12577
  const filePath = (0, import_path18.join)(projectsDir, expectedFile);
12095
12578
  const deadline = Date.now() + 12e4;
@@ -12109,11 +12592,11 @@ var StreamerServer = class {
12109
12592
  cleanup();
12110
12593
  return;
12111
12594
  }
12112
- let resolvedFilePath = (0, import_fs18.existsSync)(filePath) ? filePath : null;
12113
- 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)) {
12114
12597
  try {
12115
12598
  const now = Date.now();
12116
- 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(
12117
12600
  ({ f }) => (0, import_path18.basename)(f, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path18.join)(projectsDir, f)) === sessionId
12118
12601
  ).sort((a, b) => b.mtime - a.mtime)[0];
12119
12602
  if (match) resolvedFilePath = (0, import_path18.join)(projectsDir, match.f);
@@ -12124,7 +12607,7 @@ var StreamerServer = class {
12124
12607
  cleanup();
12125
12608
  this.sessionFileMap.set(sessionId, resolvedFilePath);
12126
12609
  try {
12127
- 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);
12128
12611
  if (existing.length > 0) {
12129
12612
  this.broadcastConversationLines(sessionId, existing);
12130
12613
  }
@@ -12148,7 +12631,7 @@ var StreamerServer = class {
12148
12631
  if (this.sessionFileMap.has(sessionId)) return;
12149
12632
  try {
12150
12633
  require("fs").mkdirSync(projectsDir, { recursive: true });
12151
- watcher = (0, import_fs18.watch)(projectsDir, tryWire);
12634
+ watcher = (0, import_fs19.watch)(projectsDir, tryWire);
12152
12635
  watcher.on("error", cleanup);
12153
12636
  } catch {
12154
12637
  }
@@ -12177,7 +12660,7 @@ var StreamerServer = class {
12177
12660
  };
12178
12661
  const matchesProjectPath = (candidatePath) => {
12179
12662
  try {
12180
- 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];
12181
12664
  if (!firstLine) return null;
12182
12665
  const parsed = JSON.parse(firstLine);
12183
12666
  if (parsed?.type !== "session_meta") return null;
@@ -12206,15 +12689,15 @@ var StreamerServer = class {
12206
12689
  );
12207
12690
  for (const root of this.codexRoots) {
12208
12691
  const sessionsDir = (0, import_path18.join)(root, dateDir);
12209
- if (!(0, import_fs18.existsSync)(sessionsDir)) continue;
12692
+ if (!(0, import_fs19.existsSync)(sessionsDir)) continue;
12210
12693
  let candidateFiles;
12211
12694
  try {
12212
- candidateFiles = (0, import_fs18.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
12695
+ candidateFiles = (0, import_fs19.readdirSync)(sessionsDir).filter((f) => f.endsWith(".jsonl"));
12213
12696
  } catch {
12214
12697
  continue;
12215
12698
  }
12216
12699
  const nowMs = Date.now();
12217
- 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);
12218
12701
  for (const { f } of recentCandidates) {
12219
12702
  const candidatePath = (0, import_path18.join)(sessionsDir, f);
12220
12703
  const match = matchesProjectPath(candidatePath);
@@ -12226,7 +12709,7 @@ var StreamerServer = class {
12226
12709
  this.sessionFileMap.set(sessionId, candidatePath);
12227
12710
  this.fileWatcher.watch(candidatePath);
12228
12711
  try {
12229
- 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);
12230
12713
  if (existing.length > 0) {
12231
12714
  this.broadcastConversationLines(sessionId, existing);
12232
12715
  }
@@ -12295,7 +12778,7 @@ var StreamerServer = class {
12295
12778
  });
12296
12779
  return;
12297
12780
  }
12298
- const body = await readBody(req);
12781
+ const body = await readBody2(req);
12299
12782
  const { path: relativePath, name } = body;
12300
12783
  if (!name || typeof name !== "string") {
12301
12784
  json(res, 400, { error: "Missing name field" });
@@ -12325,7 +12808,7 @@ var StreamerServer = class {
12325
12808
  }
12326
12809
  let parsed;
12327
12810
  try {
12328
- parsed = await readBody(req);
12811
+ parsed = await readBody2(req);
12329
12812
  } catch {
12330
12813
  json(res, 400, { error: "Invalid JSON" });
12331
12814
  return;
@@ -12382,7 +12865,7 @@ var StreamerServer = class {
12382
12865
  }
12383
12866
  let parsed;
12384
12867
  try {
12385
- parsed = await readBody(req);
12868
+ parsed = await readBody2(req);
12386
12869
  } catch {
12387
12870
  json(res, 400, { error: "Invalid JSON" });
12388
12871
  return;
@@ -12441,7 +12924,7 @@ async function waitForProcessExit(pid, timeoutMs, pollMs = ADOPT_KILL_POLL_MS) {
12441
12924
  }
12442
12925
  function classifyResumability(cwd) {
12443
12926
  if (!cwd) return { resumable: true };
12444
- if ((0, import_fs18.existsSync)(cwd)) return { resumable: true };
12927
+ if ((0, import_fs19.existsSync)(cwd)) return { resumable: true };
12445
12928
  const ranInWorktree = /\/\.worktrees\//.test(cwd) || /\/\.claude\/worktrees\//.test(cwd);
12446
12929
  return {
12447
12930
  resumable: false,
@@ -12559,7 +13042,7 @@ function parseSessionListQuery(url) {
12559
13042
  const cursor = url.searchParams.get("cursor") ?? void 0;
12560
13043
  return { query: { limit, sortBy, order, status, cursor } };
12561
13044
  }
12562
- function readBody(req) {
13045
+ function readBody2(req) {
12563
13046
  return new Promise((resolve2, reject) => {
12564
13047
  const chunks = [];
12565
13048
  req.on("data", (chunk) => chunks.push(chunk));