@threadbase-sh/streamer 1.52.4 → 1.52.6

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/cli.cjs CHANGED
@@ -3608,7 +3608,7 @@ var require_thread_stream = __commonJS({
3608
3608
  var { version: version2 } = require_package();
3609
3609
  var { EventEmitter: EventEmitter5 } = require("events");
3610
3610
  var { Worker } = require("worker_threads");
3611
- var { join: join34 } = require("path");
3611
+ var { join: join35 } = require("path");
3612
3612
  var { pathToFileURL } = require("url");
3613
3613
  var { wait } = require_wait();
3614
3614
  var {
@@ -3659,7 +3659,7 @@ var require_thread_stream = __commonJS({
3659
3659
  function createWorker(stream, opts) {
3660
3660
  const { filename, workerData } = opts;
3661
3661
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
3662
- const toExecute = bundlerOverrides["thread-stream-worker"] || join34(__dirname, "lib", "worker.js");
3662
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join35(__dirname, "lib", "worker.js");
3663
3663
  const worker = new Worker(toExecute, {
3664
3664
  ...opts.workerOpts,
3665
3665
  name: opts.workerOpts?.name || "thread-stream",
@@ -4127,7 +4127,7 @@ var require_transport = __commonJS({
4127
4127
  var { createRequire } = require("module");
4128
4128
  var { existsSync: existsSync21 } = require("fs");
4129
4129
  var getCallers = require_caller();
4130
- var { join: join34, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
4130
+ var { join: join35, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
4131
4131
  var { fileURLToPath: fileURLToPath3 } = require("url");
4132
4132
  var sleep = require_atomic_sleep();
4133
4133
  var onExit = require_on_exit_leak_free();
@@ -4280,7 +4280,7 @@ var require_transport = __commonJS({
4280
4280
  throw new Error("only one of target or targets can be specified");
4281
4281
  }
4282
4282
  if (targets) {
4283
- target = bundlerOverrides["pino-worker"] || join34(__dirname, "worker.js");
4283
+ target = bundlerOverrides["pino-worker"] || join35(__dirname, "worker.js");
4284
4284
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
4285
4285
  return {
4286
4286
  ...dest,
@@ -4298,7 +4298,7 @@ var require_transport = __commonJS({
4298
4298
  });
4299
4299
  });
4300
4300
  } else if (pipeline2) {
4301
- target = bundlerOverrides["pino-worker"] || join34(__dirname, "worker.js");
4301
+ target = bundlerOverrides["pino-worker"] || join35(__dirname, "worker.js");
4302
4302
  options.pipelines = [pipeline2.map((dest) => {
4303
4303
  return {
4304
4304
  ...dest,
@@ -4321,7 +4321,7 @@ var require_transport = __commonJS({
4321
4321
  return origin;
4322
4322
  }
4323
4323
  if (origin === "pino/file") {
4324
- return join34(__dirname, "..", "file.js");
4324
+ return join35(__dirname, "..", "file.js");
4325
4325
  }
4326
4326
  let fixTarget2;
4327
4327
  for (const filePath of callers) {
@@ -5302,7 +5302,7 @@ var require_safe_stable_stringify = __commonJS({
5302
5302
  return circularValue;
5303
5303
  }
5304
5304
  let res = "";
5305
- let join34 = ",";
5305
+ let join35 = ",";
5306
5306
  const originalIndentation = indentation;
5307
5307
  if (Array.isArray(value)) {
5308
5308
  if (value.length === 0) {
@@ -5316,7 +5316,7 @@ var require_safe_stable_stringify = __commonJS({
5316
5316
  indentation += spacer;
5317
5317
  res += `
5318
5318
  ${indentation}`;
5319
- join34 = `,
5319
+ join35 = `,
5320
5320
  ${indentation}`;
5321
5321
  }
5322
5322
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -5324,13 +5324,13 @@ ${indentation}`;
5324
5324
  for (; i < maximumValuesToStringify - 1; i++) {
5325
5325
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
5326
5326
  res += tmp2 !== void 0 ? tmp2 : "null";
5327
- res += join34;
5327
+ res += join35;
5328
5328
  }
5329
5329
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
5330
5330
  res += tmp !== void 0 ? tmp : "null";
5331
5331
  if (value.length - 1 > maximumBreadth) {
5332
5332
  const removedKeys = value.length - maximumBreadth - 1;
5333
- res += `${join34}"... ${getItemCount(removedKeys)} not stringified"`;
5333
+ res += `${join35}"... ${getItemCount(removedKeys)} not stringified"`;
5334
5334
  }
5335
5335
  if (spacer !== "") {
5336
5336
  res += `
@@ -5351,7 +5351,7 @@ ${originalIndentation}`;
5351
5351
  let separator = "";
5352
5352
  if (spacer !== "") {
5353
5353
  indentation += spacer;
5354
- join34 = `,
5354
+ join35 = `,
5355
5355
  ${indentation}`;
5356
5356
  whitespace = " ";
5357
5357
  }
@@ -5365,13 +5365,13 @@ ${indentation}`;
5365
5365
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
5366
5366
  if (tmp !== void 0) {
5367
5367
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
5368
- separator = join34;
5368
+ separator = join35;
5369
5369
  }
5370
5370
  }
5371
5371
  if (keyLength > maximumBreadth) {
5372
5372
  const removedKeys = keyLength - maximumBreadth;
5373
5373
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
5374
- separator = join34;
5374
+ separator = join35;
5375
5375
  }
5376
5376
  if (spacer !== "" && separator.length > 1) {
5377
5377
  res = `
@@ -5412,7 +5412,7 @@ ${originalIndentation}`;
5412
5412
  }
5413
5413
  const originalIndentation = indentation;
5414
5414
  let res = "";
5415
- let join34 = ",";
5415
+ let join35 = ",";
5416
5416
  if (Array.isArray(value)) {
5417
5417
  if (value.length === 0) {
5418
5418
  return "[]";
@@ -5425,7 +5425,7 @@ ${originalIndentation}`;
5425
5425
  indentation += spacer;
5426
5426
  res += `
5427
5427
  ${indentation}`;
5428
- join34 = `,
5428
+ join35 = `,
5429
5429
  ${indentation}`;
5430
5430
  }
5431
5431
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -5433,13 +5433,13 @@ ${indentation}`;
5433
5433
  for (; i < maximumValuesToStringify - 1; i++) {
5434
5434
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
5435
5435
  res += tmp2 !== void 0 ? tmp2 : "null";
5436
- res += join34;
5436
+ res += join35;
5437
5437
  }
5438
5438
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
5439
5439
  res += tmp !== void 0 ? tmp : "null";
5440
5440
  if (value.length - 1 > maximumBreadth) {
5441
5441
  const removedKeys = value.length - maximumBreadth - 1;
5442
- res += `${join34}"... ${getItemCount(removedKeys)} not stringified"`;
5442
+ res += `${join35}"... ${getItemCount(removedKeys)} not stringified"`;
5443
5443
  }
5444
5444
  if (spacer !== "") {
5445
5445
  res += `
@@ -5452,7 +5452,7 @@ ${originalIndentation}`;
5452
5452
  let whitespace = "";
5453
5453
  if (spacer !== "") {
5454
5454
  indentation += spacer;
5455
- join34 = `,
5455
+ join35 = `,
5456
5456
  ${indentation}`;
5457
5457
  whitespace = " ";
5458
5458
  }
@@ -5461,7 +5461,7 @@ ${indentation}`;
5461
5461
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
5462
5462
  if (tmp !== void 0) {
5463
5463
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
5464
- separator = join34;
5464
+ separator = join35;
5465
5465
  }
5466
5466
  }
5467
5467
  if (spacer !== "" && separator.length > 1) {
@@ -5519,20 +5519,20 @@ ${originalIndentation}`;
5519
5519
  indentation += spacer;
5520
5520
  let res2 = `
5521
5521
  ${indentation}`;
5522
- const join35 = `,
5522
+ const join36 = `,
5523
5523
  ${indentation}`;
5524
5524
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
5525
5525
  let i = 0;
5526
5526
  for (; i < maximumValuesToStringify - 1; i++) {
5527
5527
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
5528
5528
  res2 += tmp2 !== void 0 ? tmp2 : "null";
5529
- res2 += join35;
5529
+ res2 += join36;
5530
5530
  }
5531
5531
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
5532
5532
  res2 += tmp !== void 0 ? tmp : "null";
5533
5533
  if (value.length - 1 > maximumBreadth) {
5534
5534
  const removedKeys = value.length - maximumBreadth - 1;
5535
- res2 += `${join35}"... ${getItemCount(removedKeys)} not stringified"`;
5535
+ res2 += `${join36}"... ${getItemCount(removedKeys)} not stringified"`;
5536
5536
  }
5537
5537
  res2 += `
5538
5538
  ${originalIndentation}`;
@@ -5548,16 +5548,16 @@ ${originalIndentation}`;
5548
5548
  return '"[Object]"';
5549
5549
  }
5550
5550
  indentation += spacer;
5551
- const join34 = `,
5551
+ const join35 = `,
5552
5552
  ${indentation}`;
5553
5553
  let res = "";
5554
5554
  let separator = "";
5555
5555
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
5556
5556
  if (isTypedArrayWithEntries(value)) {
5557
- res += stringifyTypedArray(value, join34, maximumBreadth);
5557
+ res += stringifyTypedArray(value, join35, maximumBreadth);
5558
5558
  keys = keys.slice(value.length);
5559
5559
  maximumPropertiesToStringify -= value.length;
5560
- separator = join34;
5560
+ separator = join35;
5561
5561
  }
5562
5562
  if (deterministic) {
5563
5563
  keys = sort(keys, comparator);
@@ -5568,13 +5568,13 @@ ${indentation}`;
5568
5568
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
5569
5569
  if (tmp !== void 0) {
5570
5570
  res += `${separator}${strEscape(key2)}: ${tmp}`;
5571
- separator = join34;
5571
+ separator = join35;
5572
5572
  }
5573
5573
  }
5574
5574
  if (keyLength > maximumBreadth) {
5575
5575
  const removedKeys = keyLength - maximumBreadth;
5576
5576
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
5577
- separator = join34;
5577
+ separator = join35;
5578
5578
  }
5579
5579
  if (separator !== "") {
5580
5580
  res = `
@@ -6285,7 +6285,8 @@ function loadOrCreateApiKey() {
6285
6285
  const key = generateApiKey();
6286
6286
  (0, import_fs.mkdirSync)(configDir(), { recursive: true });
6287
6287
  (0, import_fs.writeFileSync)(configFile(), `api_key: ${key}
6288
- `, "utf-8");
6288
+ `, { encoding: "utf-8", mode: 384 });
6289
+ (0, import_fs.chmodSync)(configFile(), 384);
6289
6290
  return key;
6290
6291
  }
6291
6292
  function loadBrowseRoot() {
@@ -96870,6 +96871,252 @@ var require_lib3 = __commonJS({
96870
96871
  }
96871
96872
  });
96872
96873
 
96874
+ // src/services/security/capabilities.ts
96875
+ function isCapability(value) {
96876
+ return typeof value === "string" && CAPABILITIES.includes(value);
96877
+ }
96878
+ function capabilitiesForPreset(preset) {
96879
+ return preset === "read-only" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];
96880
+ }
96881
+ function legacyPrincipal() {
96882
+ return { kind: "legacy", capabilities: [...FULL_CAPABILITIES, "admin"] };
96883
+ }
96884
+ function hasCapability(principal, required2) {
96885
+ return principal.capabilities.includes(required2);
96886
+ }
96887
+ function requiredCapability(path2, method) {
96888
+ if (path2.startsWith("/api/sessions") && (method === "GET" || method === "HEAD")) {
96889
+ return "history:read";
96890
+ }
96891
+ let best = null;
96892
+ for (const [prefix, cap] of ROUTE_CAPABILITIES) {
96893
+ if (path2.startsWith(prefix) && (best === null || prefix.length > best.len)) {
96894
+ best = { len: prefix.length, cap };
96895
+ }
96896
+ }
96897
+ return best?.cap ?? null;
96898
+ }
96899
+ var CAPABILITIES, FULL_CAPABILITIES, READ_ONLY_CAPABILITIES, ROUTE_CAPABILITIES;
96900
+ var init_capabilities = __esm({
96901
+ "src/services/security/capabilities.ts"() {
96902
+ "use strict";
96903
+ CAPABILITIES = [
96904
+ "history:read",
96905
+ // read conversations, search
96906
+ "session:control",
96907
+ // start, resume, send input, interrupt
96908
+ "fs:browse",
96909
+ // browse the project tree
96910
+ "fs:upload",
96911
+ // upload files into a project
96912
+ "notifications",
96913
+ // register for push
96914
+ "admin"
96915
+ // rotate keys, manage devices
96916
+ ];
96917
+ FULL_CAPABILITIES = [
96918
+ "history:read",
96919
+ "session:control",
96920
+ "fs:browse",
96921
+ "fs:upload",
96922
+ "notifications"
96923
+ ];
96924
+ READ_ONLY_CAPABILITIES = ["history:read"];
96925
+ ROUTE_CAPABILITIES = [
96926
+ // Most specific first for readability; matching sorts by length anyway.
96927
+ ["/api/sessions/", "session:control"],
96928
+ ["/api/sessions", "history:read"],
96929
+ // listing sessions is a read
96930
+ ["/api/conversations", "history:read"],
96931
+ ["/api/projects", "history:read"],
96932
+ ["/api/search", "history:read"],
96933
+ ["/api/providers", "history:read"],
96934
+ ["/api/browse", "fs:browse"],
96935
+ ["/api/upload", "fs:upload"],
96936
+ ["/api/push", "notifications"],
96937
+ ["/api/devices", "admin"],
96938
+ ["/api/config", "admin"],
96939
+ ["/api/auth/rotate", "admin"],
96940
+ ["/api/backup", "admin"],
96941
+ // Server identity and capability discovery. A read-only device must be able
96942
+ // to see WHICH server it is talking to and what it supports, or it cannot
96943
+ // render anything at all.
96944
+ ["/api/info", "history:read"],
96945
+ ["/api/profiles", "history:read"],
96946
+ ["/api/diagnostics", "history:read"],
96947
+ ["/api/cache/alert", "history:read"],
96948
+ // Client log shipping: any authenticated client may report its own errors.
96949
+ // Gating this behind a capability would silence diagnostics from exactly the
96950
+ // devices most likely to be misbehaving.
96951
+ ["/api/__client-log", "history:read"],
96952
+ // Logs viewer is localhost-only and already bypasses this middleware; the
96953
+ // mapping exists so a remote request is classified rather than denied as
96954
+ // unclassified.
96955
+ ["/api/logs", "admin"],
96956
+ // Pairing routes other than the public exchange (e.g. minting a token).
96957
+ ["/api/pair", "admin"],
96958
+ // The live WebSocket. Subscribing is a read — terminal output, session
96959
+ // updates, conversation events. Control still flows through the HTTP input
96960
+ // routes, which carry their own capability check, so a read-only device can
96961
+ // watch a session stream without being able to drive it.
96962
+ ["/ws", "history:read"],
96963
+ // Progress webhook (multi-agent). Authenticated by HMAC in the handler and
96964
+ // already skipped by the middleware; classified so a stray request is denied
96965
+ // by rule rather than as "unclassified".
96966
+ ["/internal/sessions", "admin"]
96967
+ ];
96968
+ }
96969
+ });
96970
+
96971
+ // src/db/repositories/devices.repository.ts
96972
+ var devices_repository_exports = {};
96973
+ __export(devices_repository_exports, {
96974
+ DevicesRepository: () => DevicesRepository,
96975
+ generateDeviceToken: () => generateDeviceToken,
96976
+ hashDeviceToken: () => hashDeviceToken,
96977
+ parseCapabilities: () => parseCapabilities,
96978
+ safeHashEquals: () => safeHashEquals,
96979
+ toDeviceView: () => toDeviceView
96980
+ });
96981
+ function generateDeviceToken() {
96982
+ return (0, import_crypto3.randomBytes)(32).toString("base64url");
96983
+ }
96984
+ function hashDeviceToken(token) {
96985
+ return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
96986
+ }
96987
+ function safeHashEquals(a, b2) {
96988
+ if (a.length !== b2.length) return false;
96989
+ return (0, import_crypto3.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b2, "utf8"));
96990
+ }
96991
+ function parseCapabilities(raw2) {
96992
+ try {
96993
+ const parsed = JSON.parse(raw2);
96994
+ if (!Array.isArray(parsed)) return [];
96995
+ return parsed.filter(isCapability);
96996
+ } catch {
96997
+ return [];
96998
+ }
96999
+ }
97000
+ function toDeviceView(row) {
97001
+ return {
97002
+ deviceId: row.device_id,
97003
+ name: row.name,
97004
+ capabilities: parseCapabilities(row.capabilities),
97005
+ createdAt: row.created_at,
97006
+ lastSeenAt: row.last_seen_at,
97007
+ revokedAt: row.revoked_at
97008
+ };
97009
+ }
97010
+ var import_crypto3, DevicesRepository;
97011
+ var init_devices_repository = __esm({
97012
+ "src/db/repositories/devices.repository.ts"() {
97013
+ "use strict";
97014
+ import_crypto3 = require("crypto");
97015
+ init_capabilities();
97016
+ DevicesRepository = class {
97017
+ insertStmt;
97018
+ byTokenHashStmt;
97019
+ byIdStmt;
97020
+ listStmt;
97021
+ revokeStmt;
97022
+ touchStmt;
97023
+ deleteStmt;
97024
+ deleteRevokedStmt;
97025
+ constructor(db) {
97026
+ this.insertStmt = db.prepare(`
97027
+ INSERT INTO devices (
97028
+ device_id, public_key, token_hash, name, capabilities, created_at
97029
+ ) VALUES (
97030
+ @device_id, @public_key, @token_hash, @name, @capabilities, @created_at
97031
+ )
97032
+ `);
97033
+ this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
97034
+ this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
97035
+ this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
97036
+ this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
97037
+ this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
97038
+ this.deleteStmt = db.prepare("DELETE FROM devices WHERE device_id = ?");
97039
+ this.deleteRevokedStmt = db.prepare("DELETE FROM devices WHERE revoked_at IS NOT NULL");
97040
+ }
97041
+ /**
97042
+ * Record a newly paired device and mint its token.
97043
+ *
97044
+ * The raw token is returned to the caller and never stored — this is the only
97045
+ * moment it exists outside the client.
97046
+ */
97047
+ register(args) {
97048
+ const deviceId = (0, import_crypto3.randomUUID)();
97049
+ const deviceToken = generateDeviceToken();
97050
+ const capabilities = capabilitiesForPreset(args.preset ?? "full");
97051
+ this.insertStmt.run({
97052
+ device_id: deviceId,
97053
+ public_key: args.publicKey,
97054
+ token_hash: hashDeviceToken(deviceToken),
97055
+ name: args.name ?? null,
97056
+ capabilities: JSON.stringify(capabilities),
97057
+ created_at: args.now ?? Date.now()
97058
+ });
97059
+ return { deviceId, deviceToken, capabilities };
97060
+ }
97061
+ /**
97062
+ * Resolve a presented token to a device, or null.
97063
+ *
97064
+ * Returns null for a revoked device, so revocation takes effect on the very
97065
+ * next request with no cache to go stale.
97066
+ */
97067
+ authenticate(token) {
97068
+ const hash2 = hashDeviceToken(token);
97069
+ const row = this.byTokenHashStmt.get(hash2);
97070
+ if (!row) return null;
97071
+ if (!safeHashEquals(row.token_hash, hash2)) return null;
97072
+ if (row.revoked_at != null) return null;
97073
+ return row;
97074
+ }
97075
+ get(deviceId) {
97076
+ return this.byIdStmt.get(deviceId) ?? null;
97077
+ }
97078
+ /** All devices, including revoked ones — an audit surface needs the history. */
97079
+ list() {
97080
+ return this.listStmt.all().map(toDeviceView);
97081
+ }
97082
+ /** Revoke one device. Others are untouched — no key rotation, no collateral. */
97083
+ revoke(deviceId, now = Date.now()) {
97084
+ return this.revokeStmt.run(now, deviceId).changes > 0;
97085
+ }
97086
+ /**
97087
+ * Erase one device's record outright.
97088
+ *
97089
+ * Deliberately separate from `revoke`, which is a soft delete that keeps the
97090
+ * row so `list()` can show what happened. That audit trail is the right
97091
+ * default — but it meant a `devices` row, including the user-supplied `name`
97092
+ * ("Ronen's iPhone"), had no removal path at all once the registry moved to
97093
+ * runtime.db, which no command deletes. This is that path.
97094
+ *
97095
+ * Erasure is NOT revocation: deleting a row frees its `token_hash`, so a
97096
+ * device whose token is still on a phone somewhere stops being *known* rather
97097
+ * than being *refused*. Revoke first, delete second, is the safe order, and
97098
+ * `deleteRevoked()` exists so that is the easy thing to do.
97099
+ */
97100
+ delete(deviceId) {
97101
+ return this.deleteStmt.run(deviceId).changes > 0;
97102
+ }
97103
+ /**
97104
+ * Erase every already-revoked device. The bulk companion to `delete`, and the
97105
+ * one that is safe by construction: a revoked device is already refused, so
97106
+ * removing its row cannot restore access to anything.
97107
+ *
97108
+ * Returns the number of rows removed.
97109
+ */
97110
+ deleteRevoked() {
97111
+ return this.deleteRevokedStmt.run().changes;
97112
+ }
97113
+ touch(deviceId, now = Date.now()) {
97114
+ this.touchStmt.run(now, deviceId);
97115
+ }
97116
+ };
97117
+ }
97118
+ });
97119
+
96873
97120
  // src/platform.ts
96874
97121
  function isWindowsExecutablePath(path2) {
96875
97122
  const dot = path2.lastIndexOf(".");
@@ -102777,6 +103024,132 @@ var require_out4 = __commonJS({
102777
103024
  }
102778
103025
  });
102779
103026
 
103027
+ // src/db/query-timing.ts
103028
+ function deriveLabel(sql) {
103029
+ const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
103030
+ const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
103031
+ return `${verb}:${table}`;
103032
+ }
103033
+ function resolveSlowMs() {
103034
+ const raw2 = process.env.THREADBASE_DB_SLOW_QUERY_MS;
103035
+ if (raw2 === void 0 || raw2 === "") return DEFAULT_SLOW_QUERY_MS;
103036
+ const parsed = Number(raw2);
103037
+ return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
103038
+ }
103039
+ function record2(label, ms2, rows, slowMs) {
103040
+ if (slowMs > 0 && ms2 >= slowMs) {
103041
+ log2.warn(
103042
+ `[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
103043
+ { event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
103044
+ "pino"
103045
+ );
103046
+ return;
103047
+ }
103048
+ if (log2.pino.isLevelEnabled("debug")) {
103049
+ log2.debug(
103050
+ `[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
103051
+ { event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
103052
+ "pino"
103053
+ );
103054
+ }
103055
+ }
103056
+ function rowsOf(method, result) {
103057
+ if (method === "all") return Array.isArray(result) ? result.length : 0;
103058
+ if (method === "run") return result?.changes ?? 0;
103059
+ return result === void 0 ? 0 : 1;
103060
+ }
103061
+ function instrumentDatabase(db, options = {}) {
103062
+ const slowMs = options.slowMs ?? resolveSlowMs();
103063
+ const prepare = db.prepare.bind(db);
103064
+ db.prepare = ((sql) => {
103065
+ const stmt = prepare(sql);
103066
+ const box = { label: deriveLabel(sql) };
103067
+ Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
103068
+ for (const method of ["get", "all", "run"]) {
103069
+ const original = stmt[method].bind(stmt);
103070
+ Object.defineProperty(stmt, method, {
103071
+ configurable: true,
103072
+ writable: true,
103073
+ value: (...args) => {
103074
+ const started = performance.now();
103075
+ const result = original(...args);
103076
+ record2(box.label, performance.now() - started, rowsOf(method, result), slowMs);
103077
+ return result;
103078
+ }
103079
+ });
103080
+ }
103081
+ return stmt;
103082
+ });
103083
+ return db;
103084
+ }
103085
+ function labelStatements(statements) {
103086
+ for (const [name, stmt] of Object.entries(statements)) {
103087
+ const box = stmt?.[LABEL];
103088
+ if (box) box.label = name;
103089
+ }
103090
+ }
103091
+ var log2, DEFAULT_SLOW_QUERY_MS, LABEL;
103092
+ var init_query_timing = __esm({
103093
+ "src/db/query-timing.ts"() {
103094
+ "use strict";
103095
+ init_logger();
103096
+ log2 = getLogger("db");
103097
+ DEFAULT_SLOW_QUERY_MS = 35;
103098
+ LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
103099
+ }
103100
+ });
103101
+
103102
+ // src/db/sqlite-migrate.ts
103103
+ function getMigrationsDir() {
103104
+ if (typeof import_meta2 !== "undefined" && import_meta2.url) {
103105
+ return (0, import_path14.dirname)((0, import_url6.fileURLToPath)(import_meta2.url));
103106
+ }
103107
+ return __dirname;
103108
+ }
103109
+ function resolveMigrationsDir(name = "migrations") {
103110
+ return (0, import_path14.join)(getMigrationsDir(), name);
103111
+ }
103112
+ function runSqliteMigrations(db, migrationsDir) {
103113
+ db.exec(SCHEMA_MIGRATIONS_SQL);
103114
+ const dir = migrationsDir ?? resolveMigrationsDir();
103115
+ const files = (0, import_fs15.readdirSync)(dir).filter((f2) => f2.endsWith(".sql")).sort();
103116
+ const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
103117
+ const appliedSet = new Set(appliedRows.map((r) => r.id));
103118
+ const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
103119
+ const applied = [];
103120
+ const skipped = [];
103121
+ for (const file2 of files) {
103122
+ if (appliedSet.has(file2)) {
103123
+ skipped.push(file2);
103124
+ continue;
103125
+ }
103126
+ const sql = (0, import_fs15.readFileSync)((0, import_path14.join)(dir, file2), "utf-8");
103127
+ const tx = db.transaction(() => {
103128
+ db.exec(sql);
103129
+ recordApplied.run(file2, (/* @__PURE__ */ new Date()).toISOString());
103130
+ });
103131
+ tx();
103132
+ applied.push(file2);
103133
+ }
103134
+ return { applied, skipped };
103135
+ }
103136
+ var import_fs15, import_path14, import_url6, import_meta2, SCHEMA_MIGRATIONS_SQL;
103137
+ var init_sqlite_migrate = __esm({
103138
+ "src/db/sqlite-migrate.ts"() {
103139
+ "use strict";
103140
+ import_fs15 = require("fs");
103141
+ import_path14 = require("path");
103142
+ import_url6 = require("url");
103143
+ import_meta2 = {};
103144
+ SCHEMA_MIGRATIONS_SQL = `
103145
+ CREATE TABLE IF NOT EXISTS schema_migrations (
103146
+ id TEXT PRIMARY KEY,
103147
+ applied_at TEXT NOT NULL
103148
+ );
103149
+ `;
103150
+ }
103151
+ });
103152
+
102780
103153
  // src/services/questions/codexScreen.ts
102781
103154
  function parseCodexNumberedOptions(lines) {
102782
103155
  const options = [];
@@ -114874,6 +115247,113 @@ var init_esm2 = __esm({
114874
115247
  }
114875
115248
  });
114876
115249
 
115250
+ // src/db/runtime-store.ts
115251
+ var runtime_store_exports = {};
115252
+ __export(runtime_store_exports, {
115253
+ RuntimeStore: () => RuntimeStore,
115254
+ resolveRuntimeDbPath: () => resolveRuntimeDbPath
115255
+ });
115256
+ function resolveRuntimeDbPath(override) {
115257
+ return override ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path23.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path23.join)((0, import_os9.homedir)(), ".threadbase"), "runtime.db");
115258
+ }
115259
+ var import_better_sqlite33, import_os9, import_path23, RuntimeStore;
115260
+ var init_runtime_store = __esm({
115261
+ "src/db/runtime-store.ts"() {
115262
+ "use strict";
115263
+ import_better_sqlite33 = __toESM(require("better-sqlite3"), 1);
115264
+ import_os9 = require("os");
115265
+ import_path23 = require("path");
115266
+ init_query_timing();
115267
+ init_sqlite_migrate();
115268
+ RuntimeStore = class _RuntimeStore {
115269
+ constructor(db) {
115270
+ this.db = db;
115271
+ }
115272
+ db;
115273
+ static open(dbPath, migrationsDir) {
115274
+ const db = instrumentDatabase(new import_better_sqlite33.default(dbPath));
115275
+ db.pragma("journal_mode = WAL");
115276
+ runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
115277
+ return new _RuntimeStore(db);
115278
+ }
115279
+ getDatabase() {
115280
+ return this.db;
115281
+ }
115282
+ /**
115283
+ * One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
115284
+ *
115285
+ * Returns the number of rows copied.
115286
+ */
115287
+ importLegacyManagedSessions(source) {
115288
+ return this.importLegacyTable(source, "managed_sessions");
115289
+ }
115290
+ /**
115291
+ * One-time move of `devices` rows out of `cache.db`, where the registry used
115292
+ * to live (migration `011_create_devices.sql`).
115293
+ *
115294
+ * Losing this table invalidates every device token ever issued, and cache.db
115295
+ * is the file `tb-streamer cache clear` deletes and the integrity monitor
115296
+ * rebuilds — see `runtime-migrations/003_create_devices.sql`.
115297
+ *
115298
+ * Unlike `managed_sessions`, this one MOVES rather than copies: the source
115299
+ * rows are deleted once the copy is verified. A `devices` row carries a
115300
+ * user-supplied label ("Ronen's iPhone"), and leaving a second copy of that
115301
+ * on disk indefinitely — in the one file the user is told to delete when
115302
+ * something goes wrong — is more retained personal data than the rollback
115303
+ * path is worth. Recovering from a rollback is re-scanning a pairing QR.
115304
+ *
115305
+ * The delete is conditional on the copy being complete: `INSERT OR IGNORE`
115306
+ * can silently skip a row, so the destination count must match what was read
115307
+ * before anything is removed. A mismatch keeps the source and reports
115308
+ * `purged: false` rather than throwing — the import itself still succeeded,
115309
+ * and keeping data is the safe direction to fail in.
115310
+ */
115311
+ importLegacyDevices(source) {
115312
+ const copied = this.importLegacyTable(source, "devices");
115313
+ if (copied === 0) return { copied: 0, purged: false };
115314
+ const landed = this.db.prepare("SELECT COUNT(*) AS n FROM devices").get().n;
115315
+ if (landed !== copied) return { copied, purged: false };
115316
+ source.prepare("DELETE FROM devices").run();
115317
+ return { copied, purged: true };
115318
+ }
115319
+ /**
115320
+ * Copy a whole table out of a pre-split `cache.db` into this file.
115321
+ *
115322
+ * The copy itself is non-destructive — the source is left in place, so an
115323
+ * older streamer rolled back onto the same machine still finds its data.
115324
+ * `importLegacyDevices` deletes the source afterwards for its own reasons;
115325
+ * `managed_sessions` does not. Runs only when this file's table is empty, so
115326
+ * a second boot is a no-op rather than a re-copy that would resurrect rows
115327
+ * deleted since.
115328
+ *
115329
+ * The table name is interpolated into SQL, so it is typed as a closed union
115330
+ * rather than `string` — the set of tables that can ever be lifted is known
115331
+ * at compile time, and that is what keeps a caller from making this a hole.
115332
+ */
115333
+ importLegacyTable(source, table) {
115334
+ const existing = this.db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
115335
+ if (existing.n > 0) return 0;
115336
+ const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
115337
+ if (!hasTable) return 0;
115338
+ const rows = source.prepare(`SELECT * FROM ${table}`).all();
115339
+ if (rows.length === 0) return 0;
115340
+ const columns = Object.keys(rows[0]);
115341
+ const insert = this.db.prepare(
115342
+ `INSERT OR IGNORE INTO ${table} (${columns.join(", ")})
115343
+ VALUES (${columns.map((c) => `@${c}`).join(", ")})`
115344
+ );
115345
+ this.db.transaction((batch) => {
115346
+ for (const row of batch) insert.run(row);
115347
+ })(rows);
115348
+ return rows.length;
115349
+ }
115350
+ close() {
115351
+ this.db.close();
115352
+ }
115353
+ };
115354
+ }
115355
+ });
115356
+
114877
115357
  // node_modules/@xterm/headless/lib-headless/xterm-headless.js
114878
115358
  var require_xterm_headless = __commonJS({
114879
115359
  "node_modules/@xterm/headless/lib-headless/xterm-headless.js"(exports2) {
@@ -119964,8 +120444,8 @@ var init_pty_shared = __esm({
119964
120444
 
119965
120445
  // src/services/questions/codexGateAnswers.ts
119966
120446
  function gateAnswersPath() {
119967
- const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path23.join)((0, import_os9.homedir)(), ".threadbase");
119968
- return (0, import_path23.join)(dir, "gate-answers.json");
120447
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path24.join)((0, import_os10.homedir)(), ".threadbase");
120448
+ return (0, import_path24.join)(dir, "gate-answers.json");
119969
120449
  }
119970
120450
  function loadGateAnswers() {
119971
120451
  try {
@@ -119977,7 +120457,7 @@ function loadGateAnswers() {
119977
120457
  }
119978
120458
  function saveGateAnswer(key, value) {
119979
120459
  const path2 = gateAnswersPath();
119980
- (0, import_fs24.mkdirSync)((0, import_path23.dirname)(path2), { recursive: true });
120460
+ (0, import_fs24.mkdirSync)((0, import_path24.dirname)(path2), { recursive: true });
119981
120461
  (0, import_fs24.writeFileSync)(path2, `${JSON.stringify({ ...loadGateAnswers(), [key]: value }, null, 2)}
119982
120462
  `);
119983
120463
  }
@@ -119990,13 +120470,13 @@ function rememberedGateDigit(gate) {
119990
120470
  }
119991
120471
  return answers.codexTrustGate === "yes" ? "1" : null;
119992
120472
  }
119993
- var import_fs24, import_os9, import_path23;
120473
+ var import_fs24, import_os10, import_path24;
119994
120474
  var init_codexGateAnswers = __esm({
119995
120475
  "src/services/questions/codexGateAnswers.ts"() {
119996
120476
  "use strict";
119997
120477
  import_fs24 = require("fs");
119998
- import_os9 = require("os");
119999
- import_path23 = require("path");
120478
+ import_os10 = require("os");
120479
+ import_path24 = require("path");
120000
120480
  }
120001
120481
  });
120002
120482
 
@@ -120057,13 +120537,13 @@ function toPublicSession(s3) {
120057
120537
  ...s3.filePath != null && { filePath: s3.filePath }
120058
120538
  };
120059
120539
  }
120060
- var import_crypto9, import_fs25, import_path24, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
120540
+ var import_crypto9, import_fs25, import_path25, OUTPUT_BUFFER_MAX, INPUT_HISTORY_MAX, QUIET_DETECT_MS, CODEX_READY_FALLBACK_MS, SUBMIT_BYTES, CODEX_SUBMIT_DELAY_MS, CODEX_SUBMIT_MAX_WAIT_MS, CODEX_SUBMIT_STALE_MS, CodexPtyRunner;
120061
120541
  var init_codex_pty_runner = __esm({
120062
120542
  "src/codex-pty-runner.ts"() {
120063
120543
  "use strict";
120064
120544
  import_crypto9 = require("crypto");
120065
120545
  import_fs25 = require("fs");
120066
- import_path24 = require("path");
120546
+ import_path25 = require("path");
120067
120547
  init_logger();
120068
120548
  init_platform();
120069
120549
  init_providers();
@@ -120164,7 +120644,7 @@ var init_codex_pty_runner = __esm({
120164
120644
  // resume, fresh and fork is argv.
120165
120645
  async launch(sessionId, args, options) {
120166
120646
  const nodePty = await loadPty();
120167
- const projectName = options.projectName ?? (0, import_path24.basename)(options.projectPath);
120647
+ const projectName = options.projectName ?? (0, import_path25.basename)(options.projectPath);
120168
120648
  let proc;
120169
120649
  try {
120170
120650
  proc = nodePty.spawn(resolveCodexExe(), args, {
@@ -121407,13 +121887,13 @@ function toPublicSession2(s3) {
121407
121887
  ...s3.firstMessageText != null && { firstMessageText: s3.firstMessageText }
121408
121888
  };
121409
121889
  }
121410
- var import_crypto10, import_fs26, import_path25, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
121890
+ var import_crypto10, import_fs26, import_path26, OUTPUT_BUFFER_MAX2, INPUT_HISTORY_MAX2, CLAUDE_PROMPT_MARKERS, QUIET_DETECT_MS2, OSC_TAIL_CHARS, SCRAPE_THROTTLE_MS, CLAUDE_READY_FALLBACK_MS, SUBMIT_BYTES2, SUBMIT_DELAY_MS, SUBMIT_MAX_WAIT_MS, PTYManager;
121411
121891
  var init_pty_manager = __esm({
121412
121892
  "src/pty-manager.ts"() {
121413
121893
  "use strict";
121414
121894
  import_crypto10 = require("crypto");
121415
121895
  import_fs26 = require("fs");
121416
- import_path25 = require("path");
121896
+ import_path26 = require("path");
121417
121897
  init_claude_flags();
121418
121898
  init_logger();
121419
121899
  init_platform();
@@ -121535,7 +122015,7 @@ var init_pty_manager = __esm({
121535
122015
  }
121536
122016
  async doStart(sessionId, options) {
121537
122017
  const nodePty = await loadPty();
121538
- const projectName = options.projectName ?? (0, import_path25.basename)(options.projectPath);
122018
+ const projectName = options.projectName ?? (0, import_path26.basename)(options.projectPath);
121539
122019
  const permissionMode = options.permissionMode ?? "acceptEdits";
121540
122020
  const args = [
121541
122021
  "--permission-mode",
@@ -121599,7 +122079,7 @@ var init_pty_manager = __esm({
121599
122079
  async startFresh(options) {
121600
122080
  const nodePty = await loadPty();
121601
122081
  const sessionId = (0, import_crypto10.randomUUID)();
121602
- const projectName = options.projectName ?? (0, import_path25.basename)(options.projectPath);
122082
+ const projectName = options.projectName ?? (0, import_path26.basename)(options.projectPath);
121603
122083
  const permissionMode = options.permissionMode ?? "acceptEdits";
121604
122084
  const args = [
121605
122085
  "--permission-mode",
@@ -122205,11 +122685,11 @@ var init_pty_manager = __esm({
122205
122685
  });
122206
122686
 
122207
122687
  // src/live-session-manager.ts
122208
- var import_path26, LiveSessionManager;
122688
+ var import_path27, LiveSessionManager;
122209
122689
  var init_live_session_manager = __esm({
122210
122690
  "src/live-session-manager.ts"() {
122211
122691
  "use strict";
122212
- import_path26 = require("path");
122692
+ import_path27 = require("path");
122213
122693
  init_codex_pty_runner();
122214
122694
  init_providers();
122215
122695
  init_remote_session_runner();
@@ -122352,7 +122832,7 @@ var init_live_session_manager = __esm({
122352
122832
  const runner = this.runners.get(provider);
122353
122833
  if (runner) return runner;
122354
122834
  const err = new Error(
122355
- `Live ${provider} sessions are not implemented yet for ${(0, import_path26.basename)(projectPath)}`
122835
+ `Live ${provider} sessions are not implemented yet for ${(0, import_path27.basename)(projectPath)}`
122356
122836
  );
122357
122837
  err.statusCode = 501;
122358
122838
  throw err;
@@ -131471,8 +131951,8 @@ var import_events2 = require("events");
131471
131951
  var import_fs38 = require("fs");
131472
131952
  var import_promises16 = require("fs/promises");
131473
131953
  var import_http = require("http");
131474
- var import_os15 = require("os");
131475
- var import_path33 = require("path");
131954
+ var import_os16 = require("os");
131955
+ var import_path34 = require("path");
131476
131956
 
131477
131957
  // src/agent/agent-client.ts
131478
131958
  var userInputSignal = { type: "signal", name: "userInput" };
@@ -133697,203 +134177,8 @@ init_logger();
133697
134177
 
133698
134178
  // src/api/middleware/auth.middleware.ts
133699
134179
  init_auth();
133700
-
133701
- // src/db/repositories/devices.repository.ts
133702
- var import_crypto3 = require("crypto");
133703
-
133704
- // src/services/security/capabilities.ts
133705
- var CAPABILITIES = [
133706
- "history:read",
133707
- // read conversations, search
133708
- "session:control",
133709
- // start, resume, send input, interrupt
133710
- "fs:browse",
133711
- // browse the project tree
133712
- "fs:upload",
133713
- // upload files into a project
133714
- "notifications",
133715
- // register for push
133716
- "admin"
133717
- // rotate keys, manage devices
133718
- ];
133719
- function isCapability(value) {
133720
- return typeof value === "string" && CAPABILITIES.includes(value);
133721
- }
133722
- var FULL_CAPABILITIES = [
133723
- "history:read",
133724
- "session:control",
133725
- "fs:browse",
133726
- "fs:upload",
133727
- "notifications"
133728
- ];
133729
- var READ_ONLY_CAPABILITIES = ["history:read"];
133730
- function capabilitiesForPreset(preset) {
133731
- return preset === "read-only" ? [...READ_ONLY_CAPABILITIES] : [...FULL_CAPABILITIES];
133732
- }
133733
- function legacyPrincipal() {
133734
- return { kind: "legacy", capabilities: [...FULL_CAPABILITIES, "admin"] };
133735
- }
133736
- function hasCapability(principal, required2) {
133737
- return principal.capabilities.includes(required2);
133738
- }
133739
- var ROUTE_CAPABILITIES = [
133740
- // Most specific first for readability; matching sorts by length anyway.
133741
- ["/api/sessions/", "session:control"],
133742
- ["/api/sessions", "history:read"],
133743
- // listing sessions is a read
133744
- ["/api/conversations", "history:read"],
133745
- ["/api/projects", "history:read"],
133746
- ["/api/search", "history:read"],
133747
- ["/api/providers", "history:read"],
133748
- ["/api/browse", "fs:browse"],
133749
- ["/api/upload", "fs:upload"],
133750
- ["/api/push", "notifications"],
133751
- ["/api/devices", "admin"],
133752
- ["/api/config", "admin"],
133753
- ["/api/auth/rotate", "admin"],
133754
- ["/api/backup", "admin"],
133755
- // Server identity and capability discovery. A read-only device must be able
133756
- // to see WHICH server it is talking to and what it supports, or it cannot
133757
- // render anything at all.
133758
- ["/api/info", "history:read"],
133759
- ["/api/profiles", "history:read"],
133760
- ["/api/diagnostics", "history:read"],
133761
- ["/api/cache/alert", "history:read"],
133762
- // Client log shipping: any authenticated client may report its own errors.
133763
- // Gating this behind a capability would silence diagnostics from exactly the
133764
- // devices most likely to be misbehaving.
133765
- ["/api/__client-log", "history:read"],
133766
- // Logs viewer is localhost-only and already bypasses this middleware; the
133767
- // mapping exists so a remote request is classified rather than denied as
133768
- // unclassified.
133769
- ["/api/logs", "admin"],
133770
- // Pairing routes other than the public exchange (e.g. minting a token).
133771
- ["/api/pair", "admin"],
133772
- // The live WebSocket. Subscribing is a read — terminal output, session
133773
- // updates, conversation events. Control still flows through the HTTP input
133774
- // routes, which carry their own capability check, so a read-only device can
133775
- // watch a session stream without being able to drive it.
133776
- ["/ws", "history:read"],
133777
- // Progress webhook (multi-agent). Authenticated by HMAC in the handler and
133778
- // already skipped by the middleware; classified so a stray request is denied
133779
- // by rule rather than as "unclassified".
133780
- ["/internal/sessions", "admin"]
133781
- ];
133782
- function requiredCapability(path2, method) {
133783
- if (path2.startsWith("/api/sessions") && (method === "GET" || method === "HEAD")) {
133784
- return "history:read";
133785
- }
133786
- let best = null;
133787
- for (const [prefix, cap] of ROUTE_CAPABILITIES) {
133788
- if (path2.startsWith(prefix) && (best === null || prefix.length > best.len)) {
133789
- best = { len: prefix.length, cap };
133790
- }
133791
- }
133792
- return best?.cap ?? null;
133793
- }
133794
-
133795
- // src/db/repositories/devices.repository.ts
133796
- function generateDeviceToken() {
133797
- return (0, import_crypto3.randomBytes)(32).toString("base64url");
133798
- }
133799
- function hashDeviceToken(token) {
133800
- return (0, import_crypto3.createHash)("sha256").update(token).digest("hex");
133801
- }
133802
- function safeHashEquals(a, b2) {
133803
- if (a.length !== b2.length) return false;
133804
- return (0, import_crypto3.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b2, "utf8"));
133805
- }
133806
- function parseCapabilities(raw2) {
133807
- try {
133808
- const parsed = JSON.parse(raw2);
133809
- if (!Array.isArray(parsed)) return [];
133810
- return parsed.filter(isCapability);
133811
- } catch {
133812
- return [];
133813
- }
133814
- }
133815
- function toDeviceView(row) {
133816
- return {
133817
- deviceId: row.device_id,
133818
- name: row.name,
133819
- capabilities: parseCapabilities(row.capabilities),
133820
- createdAt: row.created_at,
133821
- lastSeenAt: row.last_seen_at,
133822
- revokedAt: row.revoked_at
133823
- };
133824
- }
133825
- var DevicesRepository = class {
133826
- insertStmt;
133827
- byTokenHashStmt;
133828
- byIdStmt;
133829
- listStmt;
133830
- revokeStmt;
133831
- touchStmt;
133832
- constructor(db) {
133833
- this.insertStmt = db.prepare(`
133834
- INSERT INTO devices (
133835
- device_id, public_key, token_hash, name, capabilities, created_at
133836
- ) VALUES (
133837
- @device_id, @public_key, @token_hash, @name, @capabilities, @created_at
133838
- )
133839
- `);
133840
- this.byTokenHashStmt = db.prepare("SELECT * FROM devices WHERE token_hash = ?");
133841
- this.byIdStmt = db.prepare("SELECT * FROM devices WHERE device_id = ?");
133842
- this.listStmt = db.prepare("SELECT * FROM devices ORDER BY created_at DESC");
133843
- this.revokeStmt = db.prepare("UPDATE devices SET revoked_at = ? WHERE device_id = ?");
133844
- this.touchStmt = db.prepare("UPDATE devices SET last_seen_at = ? WHERE device_id = ?");
133845
- }
133846
- /**
133847
- * Record a newly paired device and mint its token.
133848
- *
133849
- * The raw token is returned to the caller and never stored — this is the only
133850
- * moment it exists outside the client.
133851
- */
133852
- register(args) {
133853
- const deviceId = (0, import_crypto3.randomUUID)();
133854
- const deviceToken = generateDeviceToken();
133855
- const capabilities = capabilitiesForPreset(args.preset ?? "full");
133856
- this.insertStmt.run({
133857
- device_id: deviceId,
133858
- public_key: args.publicKey,
133859
- token_hash: hashDeviceToken(deviceToken),
133860
- name: args.name ?? null,
133861
- capabilities: JSON.stringify(capabilities),
133862
- created_at: args.now ?? Date.now()
133863
- });
133864
- return { deviceId, deviceToken, capabilities };
133865
- }
133866
- /**
133867
- * Resolve a presented token to a device, or null.
133868
- *
133869
- * Returns null for a revoked device, so revocation takes effect on the very
133870
- * next request with no cache to go stale.
133871
- */
133872
- authenticate(token) {
133873
- const hash2 = hashDeviceToken(token);
133874
- const row = this.byTokenHashStmt.get(hash2);
133875
- if (!row) return null;
133876
- if (!safeHashEquals(row.token_hash, hash2)) return null;
133877
- if (row.revoked_at != null) return null;
133878
- return row;
133879
- }
133880
- get(deviceId) {
133881
- return this.byIdStmt.get(deviceId) ?? null;
133882
- }
133883
- /** All devices, including revoked ones — an audit surface needs the history. */
133884
- list() {
133885
- return this.listStmt.all().map(toDeviceView);
133886
- }
133887
- /** Revoke one device. Others are untouched — no key rotation, no collateral. */
133888
- revoke(deviceId, now = Date.now()) {
133889
- return this.revokeStmt.run(now, deviceId).changes > 0;
133890
- }
133891
- touch(deviceId, now = Date.now()) {
133892
- this.touchStmt.run(now, deviceId);
133893
- }
133894
- };
133895
-
133896
- // src/api/middleware/auth.middleware.ts
134180
+ init_devices_repository();
134181
+ init_capabilities();
133897
134182
  function isLocalRequest(remoteAddr) {
133898
134183
  const addr = remoteAddr ?? "";
133899
134184
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
@@ -134404,6 +134689,34 @@ var createDeviceRoutes = (deps) => {
134404
134689
  repo.revoke(id);
134405
134690
  return c.json({ ok: true, alreadyRevoked: false });
134406
134691
  });
134692
+ app.delete("/:id", (c) => {
134693
+ const repo = deps.devicesRepo();
134694
+ if (!repo) {
134695
+ return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
134696
+ }
134697
+ const id = c.req.param("id");
134698
+ const existing = repo.get(id);
134699
+ if (!existing) return c.json({ ok: true, alreadyDeleted: true });
134700
+ const force = c.req.query("force") === "1" || c.req.query("force") === "true";
134701
+ if (existing.revoked_at == null && !force) {
134702
+ return c.json(
134703
+ {
134704
+ error: "Revoke the device before deleting it, or pass ?force=1",
134705
+ code: "DEVICE_ACTIVE"
134706
+ },
134707
+ 409
134708
+ );
134709
+ }
134710
+ repo.delete(id);
134711
+ return c.json({ ok: true, alreadyDeleted: false });
134712
+ });
134713
+ app.delete("/", (c) => {
134714
+ const repo = deps.devicesRepo();
134715
+ if (!repo) {
134716
+ return c.json({ error: "Device registry is unavailable", code: "STORE_UNAVAILABLE" }, 503);
134717
+ }
134718
+ return c.json({ ok: true, deleted: repo.deleteRevoked() });
134719
+ });
134407
134720
  return app;
134408
134721
  };
134409
134722
 
@@ -135278,6 +135591,13 @@ var createMiscRoutes = (deps) => {
135278
135591
  // Same contract: this server serves GET /api/projects/summary, which the
135279
135592
  // Hub's grouped views need before they can draw a tree.
135280
135593
  projectSummary: true,
135594
+ // The paired-device registry lives in runtime.db, so it survives
135595
+ // `tb-streamer cache clear` and the integrity monitor's reset-and-rescan.
135596
+ // A client may only prefer its scoped device token over the shared API
135597
+ // key when this is true: on an older server the registry is inside
135598
+ // cache.db, where a documented troubleshooting step deletes it and every
135599
+ // device token with it. Absent means "old server, assume not durable".
135600
+ devicesDurable: true,
135281
135601
  // Delivery capability, not endpoint support: whether this server can
135282
135602
  // actually send a push, so mobile can hide an affordance instead of
135283
135603
  // registering tokens nothing will ever send to. Absent on older servers,
@@ -135817,7 +136137,13 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
135817
136137
  const app = new Hono2();
135818
136138
  app.get(
135819
136139
  "/ws",
135820
- upgradeWebSocket(() => {
136140
+ // The principal is read here, at the upgrade, and captured for the life of
136141
+ // the socket. authMiddleware sets it because /ws is classified
136142
+ // `history:read`, but it only ever reaches the HTTP request — without
136143
+ // capturing it the socket has no principal at all, so every frame after
136144
+ // the upgrade is unauthorized-by-omission.
136145
+ upgradeWebSocket((c) => {
136146
+ const principal = c.get("principal") ?? null;
135821
136147
  let openWs = null;
135822
136148
  return {
135823
136149
  onOpen(_evt, ws2) {
@@ -135827,7 +136153,7 @@ var createWsRoutes = (deps, upgradeWebSocket) => {
135827
136153
  deps.handleWsOpen(raw2);
135828
136154
  },
135829
136155
  onMessage(evt, _ws) {
135830
- if (openWs) deps.handleWsMessage(openWs, evt.data);
136156
+ if (openWs) deps.handleWsMessage(openWs, evt.data, principal);
135831
136157
  },
135832
136158
  onClose(_evt, _ws) {
135833
136159
  if (openWs) deps.handleWsClose(openWs);
@@ -143257,122 +143583,8 @@ var import_fs17 = require("fs");
143257
143583
  var import_promises11 = require("fs/promises");
143258
143584
  var import_path16 = require("path");
143259
143585
  var import_promises12 = require("timers/promises");
143260
-
143261
- // src/db/query-timing.ts
143262
- init_logger();
143263
- var log2 = getLogger("db");
143264
- var DEFAULT_SLOW_QUERY_MS = 35;
143265
- var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
143266
- function deriveLabel(sql) {
143267
- const verb = /^\s*(\w+)/.exec(sql)?.[1]?.toLowerCase() ?? "sql";
143268
- const table = /(?:from|into|update)\s+([A-Za-z_]\w*)/i.exec(sql)?.[1] ?? "?";
143269
- return `${verb}:${table}`;
143270
- }
143271
- function resolveSlowMs() {
143272
- const raw2 = process.env.THREADBASE_DB_SLOW_QUERY_MS;
143273
- if (raw2 === void 0 || raw2 === "") return DEFAULT_SLOW_QUERY_MS;
143274
- const parsed = Number(raw2);
143275
- return Number.isFinite(parsed) ? parsed : DEFAULT_SLOW_QUERY_MS;
143276
- }
143277
- function record2(label, ms2, rows, slowMs) {
143278
- if (slowMs > 0 && ms2 >= slowMs) {
143279
- log2.warn(
143280
- `[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
143281
- { event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143282
- "pino"
143283
- );
143284
- return;
143285
- }
143286
- if (log2.pino.isLevelEnabled("debug")) {
143287
- log2.debug(
143288
- `[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
143289
- { event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143290
- "pino"
143291
- );
143292
- }
143293
- }
143294
- function rowsOf(method, result) {
143295
- if (method === "all") return Array.isArray(result) ? result.length : 0;
143296
- if (method === "run") return result?.changes ?? 0;
143297
- return result === void 0 ? 0 : 1;
143298
- }
143299
- function instrumentDatabase(db, options = {}) {
143300
- const slowMs = options.slowMs ?? resolveSlowMs();
143301
- const prepare = db.prepare.bind(db);
143302
- db.prepare = ((sql) => {
143303
- const stmt = prepare(sql);
143304
- const box = { label: deriveLabel(sql) };
143305
- Object.defineProperty(stmt, LABEL, { value: box, configurable: true });
143306
- for (const method of ["get", "all", "run"]) {
143307
- const original = stmt[method].bind(stmt);
143308
- Object.defineProperty(stmt, method, {
143309
- configurable: true,
143310
- writable: true,
143311
- value: (...args) => {
143312
- const started = performance.now();
143313
- const result = original(...args);
143314
- record2(box.label, performance.now() - started, rowsOf(method, result), slowMs);
143315
- return result;
143316
- }
143317
- });
143318
- }
143319
- return stmt;
143320
- });
143321
- return db;
143322
- }
143323
- function labelStatements(statements) {
143324
- for (const [name, stmt] of Object.entries(statements)) {
143325
- const box = stmt?.[LABEL];
143326
- if (box) box.label = name;
143327
- }
143328
- }
143329
-
143330
- // src/db/sqlite-migrate.ts
143331
- var import_fs15 = require("fs");
143332
- var import_path14 = require("path");
143333
- var import_url6 = require("url");
143334
- var import_meta2 = {};
143335
- function getMigrationsDir() {
143336
- if (typeof import_meta2 !== "undefined" && import_meta2.url) {
143337
- return (0, import_path14.dirname)((0, import_url6.fileURLToPath)(import_meta2.url));
143338
- }
143339
- return __dirname;
143340
- }
143341
- function resolveMigrationsDir(name = "migrations") {
143342
- return (0, import_path14.join)(getMigrationsDir(), name);
143343
- }
143344
- var SCHEMA_MIGRATIONS_SQL = `
143345
- CREATE TABLE IF NOT EXISTS schema_migrations (
143346
- id TEXT PRIMARY KEY,
143347
- applied_at TEXT NOT NULL
143348
- );
143349
- `;
143350
- function runSqliteMigrations(db, migrationsDir) {
143351
- db.exec(SCHEMA_MIGRATIONS_SQL);
143352
- const dir = migrationsDir ?? resolveMigrationsDir();
143353
- const files = (0, import_fs15.readdirSync)(dir).filter((f2) => f2.endsWith(".sql")).sort();
143354
- const appliedRows = db.prepare("SELECT id FROM schema_migrations").all();
143355
- const appliedSet = new Set(appliedRows.map((r) => r.id));
143356
- const recordApplied = db.prepare("INSERT INTO schema_migrations (id, applied_at) VALUES (?, ?)");
143357
- const applied = [];
143358
- const skipped = [];
143359
- for (const file2 of files) {
143360
- if (appliedSet.has(file2)) {
143361
- skipped.push(file2);
143362
- continue;
143363
- }
143364
- const sql = (0, import_fs15.readFileSync)((0, import_path14.join)(dir, file2), "utf-8");
143365
- const tx = db.transaction(() => {
143366
- db.exec(sql);
143367
- recordApplied.run(file2, (/* @__PURE__ */ new Date()).toISOString());
143368
- });
143369
- tx();
143370
- applied.push(file2);
143371
- }
143372
- return { applied, skipped };
143373
- }
143374
-
143375
- // src/conversation-cache.ts
143586
+ init_query_timing();
143587
+ init_sqlite_migrate();
143376
143588
  init_logger();
143377
143589
  init_providers();
143378
143590
 
@@ -147942,6 +148154,9 @@ var ConversationsRepository = class {
147942
148154
  }
147943
148155
  };
147944
148156
 
148157
+ // src/server.ts
148158
+ init_devices_repository();
148159
+
147945
148160
  // src/db/repositories/managed-sessions.repository.ts
147946
148161
  var PROBE_SET_MAX = 200;
147947
148162
  var DIAGNOSTICS_MAX = 200;
@@ -148257,53 +148472,8 @@ var SessionsRepository = class {
148257
148472
  }
148258
148473
  };
148259
148474
 
148260
- // src/db/runtime-store.ts
148261
- var import_better_sqlite33 = __toESM(require("better-sqlite3"), 1);
148262
- var RuntimeStore = class _RuntimeStore {
148263
- constructor(db) {
148264
- this.db = db;
148265
- }
148266
- db;
148267
- static open(dbPath, migrationsDir) {
148268
- const db = instrumentDatabase(new import_better_sqlite33.default(dbPath));
148269
- db.pragma("journal_mode = WAL");
148270
- runSqliteMigrations(db, migrationsDir ?? resolveMigrationsDir("runtime-migrations"));
148271
- return new _RuntimeStore(db);
148272
- }
148273
- getDatabase() {
148274
- return this.db;
148275
- }
148276
- /**
148277
- * One-time move of `managed_sessions` rows out of a pre-split `cache.db`.
148278
- *
148279
- * Non-destructive by design: the source table is left in place so an older
148280
- * streamer rolled back onto the same machine still finds its registry. Runs
148281
- * only when this file's table is empty, so a second boot is a no-op rather
148282
- * than a re-copy that would resurrect rows deleted since.
148283
- *
148284
- * Returns the number of rows copied.
148285
- */
148286
- importLegacyManagedSessions(source) {
148287
- const existing = this.db.prepare("SELECT COUNT(*) AS n FROM managed_sessions").get();
148288
- if (existing.n > 0) return 0;
148289
- const hasTable = source.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'managed_sessions'").get();
148290
- if (!hasTable) return 0;
148291
- const rows = source.prepare("SELECT * FROM managed_sessions").all();
148292
- if (rows.length === 0) return 0;
148293
- const columns = Object.keys(rows[0]);
148294
- const insert = this.db.prepare(
148295
- `INSERT OR IGNORE INTO managed_sessions (${columns.join(", ")})
148296
- VALUES (${columns.map((c) => `@${c}`).join(", ")})`
148297
- );
148298
- this.db.transaction((batch) => {
148299
- for (const row of batch) insert.run(row);
148300
- })(rows);
148301
- return rows.length;
148302
- }
148303
- close() {
148304
- this.db.close();
148305
- }
148306
- };
148475
+ // src/server.ts
148476
+ init_runtime_store();
148307
148477
 
148308
148478
  // src/external-tails.ts
148309
148479
  var import_fs23 = require("fs");
@@ -148576,8 +148746,8 @@ function spawnDetachedHost(socketPath, entryPoint) {
148576
148746
 
148577
148747
  // src/scanner-manager.ts
148578
148748
  var import_fs28 = require("fs");
148579
- var import_os11 = require("os");
148580
- var import_path28 = require("path");
148749
+ var import_os12 = require("os");
148750
+ var import_path29 = require("path");
148581
148751
  init_logger();
148582
148752
  init_providers();
148583
148753
 
@@ -148899,9 +149069,9 @@ function refreshConversationCache(deps) {
148899
149069
 
148900
149070
  // src/services/conversations/shouldRefreshProjectsFromHdd.ts
148901
149071
  var import_fs27 = require("fs");
148902
- var import_os10 = require("os");
148903
- var import_path27 = require("path");
148904
- var DEFAULT_PROJECTS_DIR = (0, import_path27.join)((0, import_os10.homedir)(), ".claude", "projects");
149072
+ var import_os11 = require("os");
149073
+ var import_path28 = require("path");
149074
+ var DEFAULT_PROJECTS_DIR = (0, import_path28.join)((0, import_os11.homedir)(), ".claude", "projects");
148905
149075
  function maxProjectsTreeMtimeMs(projectsDir) {
148906
149076
  let maxMs;
148907
149077
  try {
@@ -148913,7 +149083,7 @@ function maxProjectsTreeMtimeMs(projectsDir) {
148913
149083
  for (const ent of (0, import_fs27.readdirSync)(projectsDir, { withFileTypes: true })) {
148914
149084
  if (!ent.isDirectory()) continue;
148915
149085
  try {
148916
- const childMs = (0, import_fs27.statSync)((0, import_path27.join)(projectsDir, ent.name)).mtimeMs;
149086
+ const childMs = (0, import_fs27.statSync)((0, import_path28.join)(projectsDir, ent.name)).mtimeMs;
148917
149087
  if (childMs > maxMs) maxMs = childMs;
148918
149088
  } catch {
148919
149089
  }
@@ -149088,9 +149258,9 @@ var ScannerManager = class {
149088
149258
  projectsDirs() {
149089
149259
  const profiles = this.deps.scanProfiles;
149090
149260
  if (profiles && profiles.length > 0) {
149091
- return profiles.filter((p2) => p2.enabled).map((p2) => (0, import_path28.join)(p2.configDir, "projects"));
149261
+ return profiles.filter((p2) => p2.enabled).map((p2) => (0, import_path29.join)(p2.configDir, "projects"));
149092
149262
  }
149093
- return [(0, import_path28.join)((0, import_os11.homedir)(), ".claude", "projects")];
149263
+ return [(0, import_path29.join)((0, import_os12.homedir)(), ".claude", "projects")];
149094
149264
  }
149095
149265
  // ─── staleness ────────────────────────────────────────────────────
149096
149266
  // Drain the stale set and disarm the flag together. The caller owns the
@@ -149371,8 +149541,8 @@ var import_fs30 = require("fs");
149371
149541
 
149372
149542
  // src/handlers/handleListProjects.ts
149373
149543
  var import_fs29 = require("fs");
149374
- var import_os12 = require("os");
149375
- var import_path29 = require("path");
149544
+ var import_os13 = require("os");
149545
+ var import_path30 = require("path");
149376
149546
  var HEAD_BYTES = 64 * 1024;
149377
149547
  var MAX_FILES_PROBED = 3;
149378
149548
  function readRecordedCwd(dir) {
@@ -149385,7 +149555,7 @@ function readRecordedCwd(dir) {
149385
149555
  for (const file2 of files.slice(0, MAX_FILES_PROBED)) {
149386
149556
  let fd;
149387
149557
  try {
149388
- fd = (0, import_fs29.openSync)((0, import_path29.join)(dir, file2), "r");
149558
+ fd = (0, import_fs29.openSync)((0, import_path30.join)(dir, file2), "r");
149389
149559
  const buf = Buffer.alloc(HEAD_BYTES);
149390
149560
  const bytes = (0, import_fs29.readSync)(fd, buf, 0, HEAD_BYTES, 0);
149391
149561
  for (const line of buf.subarray(0, bytes).toString("utf8").split("\n")) {
@@ -149409,11 +149579,11 @@ function decodeProjectPath(dirName) {
149409
149579
  function handleListProjects(url2, res) {
149410
149580
  const limit = Math.max(1, parseInt(url2.searchParams.get("limit") ?? "50", 10) || 50);
149411
149581
  const offset = Math.max(0, parseInt(url2.searchParams.get("offset") ?? "0", 10) || 0);
149412
- const projectsDir = (0, import_path29.join)((0, import_os12.homedir)(), ".claude", "projects");
149582
+ const projectsDir = (0, import_path30.join)((0, import_os13.homedir)(), ".claude", "projects");
149413
149583
  let entries;
149414
149584
  try {
149415
149585
  entries = (0, import_fs29.readdirSync)(projectsDir).map((dirName) => {
149416
- const fullPath = (0, import_path29.join)(projectsDir, dirName);
149586
+ const fullPath = (0, import_path30.join)(projectsDir, dirName);
149417
149587
  let mtime = 0;
149418
149588
  try {
149419
149589
  mtime = (0, import_fs29.statSync)(fullPath).mtimeMs;
@@ -149428,7 +149598,7 @@ function handleListProjects(url2, res) {
149428
149598
  }
149429
149599
  const total = entries.length;
149430
149600
  const page = entries.slice(offset, offset + limit).map(({ dirName }) => {
149431
- const path2 = readRecordedCwd((0, import_path29.join)(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
149601
+ const path2 = readRecordedCwd((0, import_path30.join)(projectsDir, dirName)) ?? decodeProjectPath(String(dirName));
149432
149602
  const name = path2.split(/[\\/]/).filter(Boolean).pop() ?? dirName;
149433
149603
  return { name, path: path2, dirName };
149434
149604
  });
@@ -149438,6 +149608,10 @@ function handleListProjects(url2, res) {
149438
149608
 
149439
149609
  // src/server-wiring.ts
149440
149610
  init_logger();
149611
+ init_capabilities();
149612
+ function wsAllows(principal, required2) {
149613
+ return principal === null || hasCapability(principal, required2);
149614
+ }
149441
149615
  function createConversationWatcherEvents(deps) {
149442
149616
  return {
149443
149617
  onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
@@ -149719,7 +149893,15 @@ function createApiDeps(deps) {
149719
149893
  const alertMsg = deps.cacheMonitor()?.wsMessage();
149720
149894
  if (alertMsg) deps.wsHub.unicast(ws2, alertMsg);
149721
149895
  },
149722
- handleWsMessage: async (ws2, raw2) => {
149896
+ handleWsMessage: async (ws2, raw2, principal) => {
149897
+ const deny = (type, required2) => {
149898
+ deps.log().warn(`[ws.capability_denied] ${type} requires ${required2}`, {
149899
+ event: "ws.capability_denied",
149900
+ type,
149901
+ required: required2,
149902
+ ...principal?.deviceId ? { deviceId: principal.deviceId } : {}
149903
+ });
149904
+ };
149723
149905
  try {
149724
149906
  const msg = JSON.parse(String(raw2));
149725
149907
  if (msg.type === "register" && typeof msg.clientId === "string") {
@@ -149729,6 +149911,10 @@ function createApiDeps(deps) {
149729
149911
  deps.wsToClientId.set(ws2, msg.clientId);
149730
149912
  }
149731
149913
  if (msg.type === "subscribe_session" && typeof msg.sessionId === "string") {
149914
+ if (!wsAllows(principal, "history:read")) {
149915
+ deny(msg.type, "history:read");
149916
+ return;
149917
+ }
149732
149918
  deps.addSessionSubscriber(msg.sessionId, ws2);
149733
149919
  if (deps.ptyManager.hasSession(msg.sessionId)) {
149734
149920
  const lines = await deps.ptyManager.getOutputLines(msg.sessionId, 200);
@@ -149777,6 +149963,10 @@ function createApiDeps(deps) {
149777
149963
  }
149778
149964
  }
149779
149965
  if (msg.type === "hold_session" && typeof msg.sessionId === "string") {
149966
+ if (!wsAllows(principal, "session:control")) {
149967
+ deny(msg.type, "session:control");
149968
+ return;
149969
+ }
149780
149970
  deps.startGraceTimer(msg.sessionId, deps.ptyGracePeriodMs);
149781
149971
  }
149782
149972
  } catch {
@@ -149804,11 +149994,11 @@ var import_fs33 = require("fs");
149804
149994
 
149805
149995
  // src/services/cache-integrity/alertStore.ts
149806
149996
  var import_fs31 = require("fs");
149807
- var import_os13 = require("os");
149808
- var import_path30 = require("path");
149997
+ var import_os14 = require("os");
149998
+ var import_path31 = require("path");
149809
149999
  function alertStatePath() {
149810
- const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path30.join)((0, import_os13.homedir)(), ".threadbase");
149811
- return (0, import_path30.join)(dir, "cache-alert.json");
150000
+ const dir = process.env.THREADBASE_CONFIG_DIR ?? (0, import_path31.join)((0, import_os14.homedir)(), ".threadbase");
150001
+ return (0, import_path31.join)(dir, "cache-alert.json");
149812
150002
  }
149813
150003
  function loadAlertState() {
149814
150004
  try {
@@ -149820,14 +150010,14 @@ function loadAlertState() {
149820
150010
  }
149821
150011
  function saveAlertState(state) {
149822
150012
  const path2 = alertStatePath();
149823
- (0, import_fs31.mkdirSync)((0, import_path30.dirname)(path2), { recursive: true });
150013
+ (0, import_fs31.mkdirSync)((0, import_path31.dirname)(path2), { recursive: true });
149824
150014
  (0, import_fs31.writeFileSync)(path2, `${JSON.stringify(state, null, 2)}
149825
150015
  `);
149826
150016
  }
149827
150017
 
149828
150018
  // src/services/cache-integrity/backup.ts
149829
150019
  var import_fs32 = require("fs");
149830
- var import_path31 = require("path");
150020
+ var import_path32 = require("path");
149831
150021
  var DEFAULT_RETAIN = 3;
149832
150022
  function retainCount() {
149833
150023
  const parsed = Number.parseInt(process.env.THREADBASE_CACHE_BACKUP_RETAIN ?? "", 10);
@@ -149838,13 +150028,13 @@ function timestamp(d) {
149838
150028
  return `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
149839
150029
  }
149840
150030
  async function backupCacheDb(db, cacheDir) {
149841
- const backupsDir = (0, import_path31.join)(cacheDir, "backups");
150031
+ const backupsDir = (0, import_path32.join)(cacheDir, "backups");
149842
150032
  (0, import_fs32.mkdirSync)(backupsDir, { recursive: true });
149843
- const destPath = (0, import_path31.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
150033
+ const destPath = (0, import_path32.join)(backupsDir, `cache-${timestamp(/* @__PURE__ */ new Date())}.db`);
149844
150034
  await db.backup(destPath);
149845
150035
  const retain = retainCount();
149846
150036
  const backups = (0, import_fs32.readdirSync)(backupsDir).filter((f2) => f2.startsWith("cache-") && f2.endsWith(".db")).map((f2) => {
149847
- const full = (0, import_path31.join)(backupsDir, f2);
150037
+ const full = (0, import_path32.join)(backupsDir, f2);
149848
150038
  return { full, mtime: (0, import_fs32.statSync)(full).mtimeMs };
149849
150039
  }).sort((a, b2) => b2.mtime - a.mtime);
149850
150040
  for (const stale of backups.slice(retain)) {
@@ -151047,6 +151237,7 @@ function autoResumeSkipReason(row, opts) {
151047
151237
  if (opts.now - row.status_updated_at > AUTO_RESUME_WINDOW_MS) return "too_old";
151048
151238
  if (!opts.projectExists(row.project_path)) return "project_missing";
151049
151239
  if (resumeIdForRow(row) == null) return "resume_identity_missing";
151240
+ if (!opts.historyExists(row)) return "history_missing";
151050
151241
  return null;
151051
151242
  }
151052
151243
  function planAutoResume(rows, opts) {
@@ -151308,7 +151499,34 @@ var SessionRegistryBoot = class {
151308
151499
  /** Resume only the recent sessions the user explicitly allowed us to start at boot. */
151309
151500
  async autoResumePreviousSessions(rows) {
151310
151501
  if (!this.autoResumeOnBoot) return;
151311
- const plan = planAutoResume(rows, { now: Date.now(), projectExists: import_fs36.existsSync });
151502
+ const now = Date.now();
151503
+ const baseOptions = { now, projectExists: import_fs36.existsSync, historyExists: () => true };
151504
+ const historyExists = /* @__PURE__ */ new Map();
151505
+ const preflights = /* @__PURE__ */ new Set();
151506
+ for (const row of rows) {
151507
+ if (autoResumeSkipReason(row, baseOptions) != null) {
151508
+ historyExists.set(row.session_id, false);
151509
+ continue;
151510
+ }
151511
+ while (preflights.size >= AUTO_RESUME_CONCURRENCY) {
151512
+ await Promise.race(preflights);
151513
+ }
151514
+ const preflight = (async () => {
151515
+ try {
151516
+ const target = await this.deps.resolveConversationTarget(row.session_id);
151517
+ historyExists.set(row.session_id, target.ok || target.reason !== "history_file_missing");
151518
+ } catch {
151519
+ historyExists.set(row.session_id, true);
151520
+ }
151521
+ })();
151522
+ preflights.add(preflight);
151523
+ void preflight.then(() => preflights.delete(preflight));
151524
+ }
151525
+ await Promise.all(preflights);
151526
+ const plan = planAutoResume(rows, {
151527
+ ...baseOptions,
151528
+ historyExists: (row) => historyExists.get(row.session_id) ?? false
151529
+ });
151312
151530
  const skippedBy = {};
151313
151531
  for (const { row, reason } of plan.skipped) {
151314
151532
  skippedBy[reason] = (skippedBy[reason] ?? 0) + 1;
@@ -151810,8 +152028,8 @@ function discoveredToResponse(d, conversationId) {
151810
152028
 
151811
152029
  // src/session-watchers.ts
151812
152030
  var import_fs37 = require("fs");
151813
- var import_os14 = require("os");
151814
- var import_path32 = require("path");
152031
+ var import_os15 = require("os");
152032
+ var import_path33 = require("path");
151815
152033
  init_logger();
151816
152034
  var SessionWatchers = class {
151817
152035
  constructor(deps) {
@@ -151893,9 +152111,9 @@ var SessionWatchers = class {
151893
152111
  // was passed to Claude via --session-id so the filename matches from the start.
151894
152112
  watchForJsonl(sessionId, projectPath) {
151895
152113
  const encoded = projectPath.replace(/[/\\:.]/g, "-");
151896
- const projectsDir = (0, import_path32.join)((0, import_os14.homedir)(), ".claude", "projects", encoded);
152114
+ const projectsDir = (0, import_path33.join)((0, import_os15.homedir)(), ".claude", "projects", encoded);
151897
152115
  const expectedFile = `${sessionId}.jsonl`;
151898
- const filePath = (0, import_path32.join)(projectsDir, expectedFile);
152116
+ const filePath = (0, import_path33.join)(projectsDir, expectedFile);
151899
152117
  const deadline = Date.now() + 12e4;
151900
152118
  let watcher = null;
151901
152119
  const cleanup = () => {
@@ -151917,10 +152135,10 @@ var SessionWatchers = class {
151917
152135
  if (!resolvedFilePath && (0, import_fs37.existsSync)(projectsDir)) {
151918
152136
  try {
151919
152137
  const now = Date.now();
151920
- const match2 = (0, import_fs37.readdirSync)(projectsDir).filter((f2) => f2.endsWith(".jsonl")).map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path32.join)(projectsDir, f2)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
151921
- ({ f: f2 }) => (0, import_path32.basename)(f2, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path32.join)(projectsDir, f2)) === sessionId
152138
+ const match2 = (0, import_fs37.readdirSync)(projectsDir).filter((f2) => f2.endsWith(".jsonl")).map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path33.join)(projectsDir, f2)).mtimeMs })).filter(({ mtime }) => now - mtime < 5e3).filter(
152139
+ ({ f: f2 }) => (0, import_path33.basename)(f2, ".jsonl") === sessionId || this.readFirstLineSessionId((0, import_path33.join)(projectsDir, f2)) === sessionId
151922
152140
  ).sort((a, b2) => b2.mtime - a.mtime)[0];
151923
- if (match2) resolvedFilePath = (0, import_path32.join)(projectsDir, match2.f);
152141
+ if (match2) resolvedFilePath = (0, import_path33.join)(projectsDir, match2.f);
151924
152142
  } catch {
151925
152143
  }
151926
152144
  }
@@ -151964,7 +152182,7 @@ var SessionWatchers = class {
151964
152182
  watchForCodexRollout(sessionId, projectPath) {
151965
152183
  const deadline = Date.now() + 12e4;
151966
152184
  const now = /* @__PURE__ */ new Date();
151967
- const dateDir = (0, import_path32.join)(
152185
+ const dateDir = (0, import_path33.join)(
151968
152186
  String(now.getFullYear()),
151969
152187
  String(now.getMonth() + 1).padStart(2, "0"),
151970
152188
  String(now.getDate()).padStart(2, "0")
@@ -152005,7 +152223,7 @@ var SessionWatchers = class {
152005
152223
  this.deps.sessionStore.listManaged().filter((s3) => s3.id !== sessionId && s3.boundConversationId != null).map((s3) => s3.boundConversationId)
152006
152224
  );
152007
152225
  for (const root of this.deps.codexRoots) {
152008
- const sessionsDir = (0, import_path32.join)(root, dateDir);
152226
+ const sessionsDir = (0, import_path33.join)(root, dateDir);
152009
152227
  if (!(0, import_fs37.existsSync)(sessionsDir)) continue;
152010
152228
  let candidateFiles;
152011
152229
  try {
@@ -152014,9 +152232,9 @@ var SessionWatchers = class {
152014
152232
  continue;
152015
152233
  }
152016
152234
  const nowMs = Date.now();
152017
- const recentCandidates = candidateFiles.map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path32.join)(sessionsDir, f2)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b2) => b2.mtime - a.mtime);
152235
+ const recentCandidates = candidateFiles.map((f2) => ({ f: f2, mtime: (0, import_fs37.statSync)((0, import_path33.join)(sessionsDir, f2)).mtimeMs })).filter(({ mtime }) => nowMs - mtime < 1e4).sort((a, b2) => b2.mtime - a.mtime);
152018
152236
  for (const { f: f2 } of recentCandidates) {
152019
- const candidatePath = (0, import_path32.join)(sessionsDir, f2);
152237
+ const candidatePath = (0, import_path33.join)(sessionsDir, f2);
152020
152238
  const match2 = matchesProjectPath(candidatePath);
152021
152239
  if (!match2) continue;
152022
152240
  if (boundElsewhere.has(match2.id)) continue;
@@ -152420,7 +152638,7 @@ var StreamerServer = class {
152420
152638
  this.skipStartupWarmup = config2.skipStartupWarmup ?? false;
152421
152639
  this.autoResumeOnBoot = config2.autoResumeOnBoot ?? false;
152422
152640
  this.scanProfiles = config2.scanProfiles;
152423
- this.codexRoots = config2.codexRoots ?? [(0, import_path33.join)((0, import_os15.homedir)(), ".codex", "sessions")];
152641
+ this.codexRoots = config2.codexRoots ?? [(0, import_path34.join)((0, import_os16.homedir)(), ".codex", "sessions")];
152424
152642
  this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
152425
152643
  this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
152426
152644
  const flagResolution = resolveFeatureFlags({
@@ -152437,8 +152655,8 @@ var StreamerServer = class {
152437
152655
  this.claudeFlagsPersistable = config2.claudeFlags === void 0;
152438
152656
  this.claudeFlags = config2.claudeFlags ?? loadClaudeFlags();
152439
152657
  this.claudeExtraArgs = config2.claudeExtraArgs ?? loadClaudeExtraArgs();
152440
- this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path33.join)((0, import_os15.homedir)(), ".threadbase", "cache");
152441
- this.runtimeDbPath = config2.runtimeDbPath ?? process.env.THREADBASE_RUNTIME_DB ?? (0, import_path33.join)(process.env.THREADBASE_CONFIG_DIR ?? (0, import_path33.join)((0, import_os15.homedir)(), ".threadbase"), "runtime.db");
152658
+ this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path34.join)((0, import_os16.homedir)(), ".threadbase", "cache");
152659
+ this.runtimeDbPath = resolveRuntimeDbPath(config2.runtimeDbPath);
152442
152660
  this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
152443
152661
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
152444
152662
  this.scannerManager = new ScannerManager({
@@ -152470,7 +152688,8 @@ var StreamerServer = class {
152470
152688
  selfPtyEndedAt: this.selfPtyEndedAt,
152471
152689
  resumeSession: (opts) => this.sessionHandlers.resumeSession(opts),
152472
152690
  watchConversationFile: (sessionId, historyId) => this.sessionWatchers.watchConversationFile(sessionId, historyId),
152473
- broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload())
152691
+ broadcastSessionList: () => this.wsHub.broadcast(this.sessionListPayload()),
152692
+ resolveConversationTarget: (sessionId) => this.resolveConversationTarget(sessionId)
152474
152693
  });
152475
152694
  this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
152476
152695
  this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
@@ -152613,7 +152832,7 @@ var StreamerServer = class {
152613
152832
  temporalClient,
152614
152833
  taskQueue: agentConfig.temporal.taskQueue
152615
152834
  });
152616
- const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path33.join)((0, import_path33.dirname)(this.cacheDir), "conversations");
152835
+ const conversationsBaseDir = agentConfig.conversationsDir || (0, import_path34.join)((0, import_path34.dirname)(this.cacheDir), "conversations");
152617
152836
  conversationWriter = createConversationWriter({
152618
152837
  baseDir: conversationsBaseDir
152619
152838
  });
@@ -152865,14 +153084,14 @@ var StreamerServer = class {
152865
153084
  }
152866
153085
  this.apnsClient = new ApnsClient(creds);
152867
153086
  const sender = new LiveActivitySender(this.apnsClient, pushRepo);
152868
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)();
152869
- this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os15.hostname)());
153087
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)();
153088
+ this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os16.hostname)());
152870
153089
  this.liveActivityRenewal = new LiveActivityRenewalScheduler({
152871
153090
  repo: pushRepo,
152872
153091
  sender,
152873
153092
  sessionStore: this.sessionStore,
152874
153093
  serverId,
152875
- serverLabel: (0, import_os15.hostname)()
153094
+ serverLabel: (0, import_os16.hostname)()
152876
153095
  });
152877
153096
  this.liveActivityRenewal.start();
152878
153097
  this.log.info("Live Activity push enabled", {
@@ -152892,7 +153111,7 @@ var StreamerServer = class {
152892
153111
  */
152893
153112
  initWaitingInputPush(pushRepo) {
152894
153113
  const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
152895
- const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)();
153114
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)();
152896
153115
  this.waitingInputNotifier = new WaitingInputNotifier(
152897
153116
  sender,
152898
153117
  serverId,
@@ -153029,7 +153248,7 @@ var StreamerServer = class {
153029
153248
  let sessions = null;
153030
153249
  for (let attempt = 0; attempt < 2; attempt += 1) {
153031
153250
  const transport = await connectOrSpawnHost({
153032
- instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os15.hostname)()
153251
+ instanceId: process.env.THREADBASE_INSTANCE_ID ?? (0, import_os16.hostname)()
153033
153252
  });
153034
153253
  try {
153035
153254
  sessions = await this.ptyManager.useRemoteRunner(transport);
@@ -153086,6 +153305,7 @@ var StreamerServer = class {
153086
153305
  try {
153087
153306
  this.runtimeStore = RuntimeStore.open(this.runtimeDbPath);
153088
153307
  this.managedSessionsRepo = new ManagedSessionsRepository(this.runtimeStore.getDatabase());
153308
+ this.devicesRepo = new DevicesRepository(this.runtimeStore.getDatabase());
153089
153309
  } catch (err) {
153090
153310
  const message = err instanceof Error ? err.message : String(err);
153091
153311
  const abiMismatch = message.includes("NODE_MODULE_VERSION") || message.includes("was compiled against a different Node.js version");
@@ -153105,7 +153325,7 @@ var StreamerServer = class {
153105
153325
  }
153106
153326
  try {
153107
153327
  this.cache = ConversationCache.open(
153108
- (0, import_path33.join)(this.cacheDir, "cache.db"),
153328
+ (0, import_path34.join)(this.cacheDir, "cache.db"),
153109
153329
  this.tailSize,
153110
153330
  void 0,
153111
153331
  {
@@ -153135,18 +153355,39 @@ var StreamerServer = class {
153135
153355
  if (copied > 0) {
153136
153356
  this.log.info(`Copied ${copied} managed session row(s) from cache.db to runtime.db`, {
153137
153357
  copied,
153358
+ table: "managed_sessions",
153138
153359
  event: "runtime.legacy_import"
153139
153360
  });
153140
153361
  }
153141
153362
  } catch (err) {
153142
153363
  this.log.warn("[registry] legacy managed_sessions copy failed", {
153143
153364
  event: "runtime.legacy_import_failed",
153365
+ table: "managed_sessions",
153366
+ err
153367
+ });
153368
+ }
153369
+ try {
153370
+ const result = this.runtimeStore?.importLegacyDevices(db);
153371
+ if (result && result.copied > 0) {
153372
+ this.log.info(
153373
+ `Moved ${result.copied} device row(s) from cache.db to runtime.db` + (result.purged ? "; removed the cache-side copy" : "; KEPT the cache-side copy (row count did not match after copy)"),
153374
+ {
153375
+ copied: result.copied,
153376
+ purged: result.purged,
153377
+ table: "devices",
153378
+ event: "runtime.legacy_import"
153379
+ }
153380
+ );
153381
+ }
153382
+ } catch (err) {
153383
+ this.log.warn("[registry] legacy devices move failed", {
153384
+ event: "runtime.legacy_import_failed",
153385
+ table: "devices",
153144
153386
  err
153145
153387
  });
153146
153388
  }
153147
153389
  this.cacheMetadataRepo = new CacheMetadataRepository(db);
153148
153390
  this.pushRepo = new PushRepository(db);
153149
- this.devicesRepo = new DevicesRepository(db);
153150
153391
  this.initLiveActivityPush(this.pushRepo);
153151
153392
  this.initWaitingInputPush(this.pushRepo);
153152
153393
  this.cacheMonitor = new CacheIntegrityMonitor(
@@ -153472,7 +153713,7 @@ var StreamerServer = class {
153472
153713
  nonce: sealed.nonce,
153473
153714
  ephemeralPublicKey: sealed.ephemeralPublicKey,
153474
153715
  publicUrl: this.publicUrl,
153475
- machineName: (0, import_os15.hostname)(),
153716
+ machineName: (0, import_os16.hostname)(),
153476
153717
  ...device && {
153477
153718
  deviceId: device.deviceId,
153478
153719
  deviceToken: device.deviceToken,
@@ -153679,13 +153920,17 @@ var StreamerServer = class {
153679
153920
  conv = await this.conversationHandlers.findConversationByUuid(boundId);
153680
153921
  }
153681
153922
  }
153923
+ const cachedConvMeta = this.cache?.getMetaById(historyId);
153924
+ const cachedPath = cachedConvMeta?.filePath ? toNativeFilePath(cachedConvMeta.filePath) : null;
153925
+ const cachedCodexPath = (registryProvider === CODEX_CLI_PROVIDER || cachedConvMeta?.provider === CODEX_CLI_PROVIDER) && cachedPath != null && (0, import_fs38.existsSync)(cachedPath) ? cachedPath : null;
153682
153926
  const jsonlCwd = jsonlPath ? await this.conversationHandlers.readCwdFromJsonl(jsonlPath) : null;
153683
- const projectPath = jsonlCwd ?? conv?.projectPath;
153927
+ const projectPath = jsonlCwd ?? conv?.projectPath ?? (cachedCodexPath ? cachedConvMeta?.projectPath : null);
153684
153928
  if (!projectPath) {
153685
- if (!conv && !jsonlPath) return { ok: false, reason: "history_file_missing" };
153929
+ if (!conv && !jsonlPath && !cachedCodexPath) {
153930
+ return { ok: false, reason: "history_file_missing" };
153931
+ }
153686
153932
  return { ok: false, reason: "no_project_path" };
153687
153933
  }
153688
- const cachedConvMeta = this.cache?.getMetaById(historyId);
153689
153934
  const provider = coerceProviderForRunner(
153690
153935
  conv?.provider ?? cachedConvMeta?.provider ?? registryProvider
153691
153936
  );
@@ -153699,7 +153944,7 @@ var StreamerServer = class {
153699
153944
  // conversation. Kept separate from `jsonlPath` deliberately: feeding it to
153700
153945
  // conversationBusy() would newly arm the mtime heuristic for Codex, which
153701
153946
  // is exactly the over-broad signal the report ruled out.
153702
- historyPath: jsonlPath ?? conv?.filePath ?? null,
153947
+ historyPath: jsonlPath ?? conv?.filePath ?? cachedCodexPath ?? null,
153703
153948
  conv,
153704
153949
  projectPath,
153705
153950
  provider
@@ -154535,7 +154780,7 @@ var import_node_stream2 = __toESM(require("stream"), 1);
154535
154780
  var import_node_string_decoder = require("string_decoder");
154536
154781
  var import_node_path15 = __toESM(require("path"), 1);
154537
154782
  var import_node_fs13 = __toESM(require("fs"), 1);
154538
- var import_path34 = require("path");
154783
+ var import_path35 = require("path");
154539
154784
  var import_events4 = require("events");
154540
154785
  var import_assert = __toESM(require("assert"), 1);
154541
154786
  var import_buffer2 = require("buffer");
@@ -154545,9 +154790,9 @@ var import_node_path16 = require("path");
154545
154790
  var import_node_path17 = require("path");
154546
154791
  var import_fs40 = __toESM(require("fs"), 1);
154547
154792
  var import_fs41 = __toESM(require("fs"), 1);
154548
- var import_path35 = __toESM(require("path"), 1);
154549
- var import_node_path18 = require("path");
154550
154793
  var import_path36 = __toESM(require("path"), 1);
154794
+ var import_node_path18 = require("path");
154795
+ var import_path37 = __toESM(require("path"), 1);
154551
154796
  var import_node_fs14 = __toESM(require("fs"), 1);
154552
154797
  var import_node_assert = __toESM(require("assert"), 1);
154553
154798
  var import_node_crypto6 = require("crypto");
@@ -155919,11 +156164,11 @@ var vn = (s3) => {
155919
156164
  var Qi = (s3, t) => {
155920
156165
  let e = new Map(t.map((o) => [mt(o), true])), i = s3.filter, r = 100, n = (o, h = "", a = 0) => {
155921
156166
  if (a >= r) return e.set(o, false), false;
155922
- let l = h || (0, import_path34.parse)(o).root || ".", c;
156167
+ let l = h || (0, import_path35.parse)(o).root || ".", c;
155923
156168
  if (o === l) c = false;
155924
156169
  else {
155925
156170
  let d = e.get(o);
155926
- c = d !== void 0 ? d : n((0, import_path34.dirname)(o), l, a + 1);
156171
+ c = d !== void 0 ? d : n((0, import_path35.dirname)(o), l, a + 1);
155927
156172
  }
155928
156173
  return e.set(o, c), c;
155929
156174
  };
@@ -156043,7 +156288,7 @@ var de = class extends A {
156043
156288
  let [o, h] = ce(this.path);
156044
156289
  o && typeof h == "string" && (this.path = h, r = o);
156045
156290
  }
156046
- this.win32 = !!i.win32 || process.platform === "win32", this.win32 && (this.path = Qs(this.path.replaceAll(/\\/g, "/")), t = t.replaceAll(/\\/g, "/")), this.absolute = f(i.absolute || import_path35.default.resolve(this.cwd, t)), this.path === "" && (this.path = "./"), r && this.warn("TAR_ENTRY_INFO", `stripping ${r} from absolute path`, { entry: this, path: r + this.path });
156291
+ this.win32 = !!i.win32 || process.platform === "win32", this.win32 && (this.path = Qs(this.path.replaceAll(/\\/g, "/")), t = t.replaceAll(/\\/g, "/")), this.absolute = f(i.absolute || import_path36.default.resolve(this.cwd, t)), this.path === "" && (this.path = "./"), r && this.warn("TAR_ENTRY_INFO", `stripping ${r} from absolute path`, { entry: this, path: r + this.path });
156047
156292
  let n = this.statCache.get(this.absolute);
156048
156293
  n ? this[si](n) : this[ss]();
156049
156294
  }
@@ -156102,7 +156347,7 @@ var de = class extends A {
156102
156347
  }
156103
156348
  [sr](t) {
156104
156349
  if (!this.stat) throw new Error("cannot create link entry without stat");
156105
- this.type = "Link", this.linkpath = f(import_path35.default.relative(this.cwd, t)), this.stat.size = 0, this[fe](), this.end();
156350
+ this.type = "Link", this.linkpath = f(import_path36.default.relative(this.cwd, t)), this.stat.size = 0, this[fe](), this.end();
156106
156351
  }
156107
156352
  [er]() {
156108
156353
  if (!this.stat) throw new Error("cannot create file entry without stat");
@@ -156516,7 +156761,7 @@ var wt = class extends A {
156516
156761
  return typeof t == "string" ? this[ci](t) : this[or](t), this.flowing;
156517
156762
  }
156518
156763
  [or](t) {
156519
- let e = f(import_path36.default.resolve(this.cwd, t.path));
156764
+ let e = f(import_path37.default.resolve(this.cwd, t.path));
156520
156765
  if (!this.filter(t.path, t)) t.resume();
156521
156766
  else {
156522
156767
  let i = new pi(t.path, e);
@@ -156525,7 +156770,7 @@ var wt = class extends A {
156525
156770
  this[Ft]();
156526
156771
  }
156527
156772
  [ci](t) {
156528
- let e = f(import_path36.default.resolve(this.cwd, t));
156773
+ let e = f(import_path37.default.resolve(this.cwd, t));
156529
156774
  this[W2].push(new pi(t, e)), this[Ft]();
156530
156775
  }
156531
156776
  [ds](t) {
@@ -158180,8 +158425,8 @@ program2.command("cache").description("Manage the local SQLite conversation cach
158180
158425
  `${process.env.HOME}/.threadbase/cache`
158181
158426
  ).action((opts) => {
158182
158427
  const { rmSync: rmSync6, existsSync: existsSync21 } = require("fs");
158183
- const { join: join34 } = require("path");
158184
- const dbPath = join34(opts.cacheDir, "cache.db");
158428
+ const { join: join35 } = require("path");
158429
+ const dbPath = join35(opts.cacheDir, "cache.db");
158185
158430
  for (const suffix of ["", "-shm", "-wal"]) {
158186
158431
  const f2 = dbPath + suffix;
158187
158432
  if (existsSync21(f2)) {
@@ -158192,6 +158437,75 @@ program2.command("cache").description("Manage the local SQLite conversation cach
158192
158437
  log14.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
158193
158438
  })
158194
158439
  );
158440
+ program2.command("devices").description("List, revoke and erase paired devices").addCommand(
158441
+ new Command("list").description("Show every paired device, including revoked ones").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (opts) => {
158442
+ const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
158443
+ const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
158444
+ const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
158445
+ const devices = new DevicesRepository2(store.getDatabase()).list();
158446
+ store.close();
158447
+ if (devices.length === 0) {
158448
+ log14.info("No paired devices.", void 0, "console");
158449
+ return;
158450
+ }
158451
+ for (const d of devices) {
158452
+ const state = d.revokedAt ? "revoked" : "active";
158453
+ const seen = d.lastSeenAt ? new Date(d.lastSeenAt).toISOString() : "never";
158454
+ log14.info(
158455
+ `${d.deviceId} ${state.padEnd(7)} ${d.name ?? "(unnamed)"} last seen ${seen} [${d.capabilities.join(", ")}]`,
158456
+ void 0,
158457
+ "console"
158458
+ );
158459
+ }
158460
+ })
158461
+ ).addCommand(
158462
+ new Command("revoke").argument("<deviceId>").description("Refuse this device's token, keeping its record for the audit trail").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (deviceId, opts) => {
158463
+ const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
158464
+ const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
158465
+ const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
158466
+ const ok = new DevicesRepository2(store.getDatabase()).revoke(deviceId);
158467
+ store.close();
158468
+ log14.info(ok ? `Revoked ${deviceId}.` : `No such device: ${deviceId}`, void 0, "console");
158469
+ if (!ok) process.exitCode = 1;
158470
+ })
158471
+ ).addCommand(
158472
+ new Command("delete").argument("[deviceId]").description("Erase a device record. With --revoked, erase every revoked device instead").option("--revoked", "Erase all revoked devices rather than one by id").option("--force", "Erase an ACTIVE device (revoking first is the safe order)").option("--db <path>", "runtime.db path (default: ~/.threadbase/runtime.db)").action(async (deviceId, opts) => {
158473
+ const { RuntimeStore: RuntimeStore2, resolveRuntimeDbPath: resolveRuntimeDbPath2 } = await Promise.resolve().then(() => (init_runtime_store(), runtime_store_exports));
158474
+ const { DevicesRepository: DevicesRepository2 } = await Promise.resolve().then(() => (init_devices_repository(), devices_repository_exports));
158475
+ const store = RuntimeStore2.open(resolveRuntimeDbPath2(opts.db));
158476
+ const repo = new DevicesRepository2(store.getDatabase());
158477
+ if (opts.revoked) {
158478
+ const n = repo.deleteRevoked();
158479
+ store.close();
158480
+ log14.info(`Erased ${n} revoked device record(s).`, void 0, "console");
158481
+ return;
158482
+ }
158483
+ if (!deviceId) {
158484
+ store.close();
158485
+ log14.error("Pass a device id, or --revoked to erase all revoked devices.");
158486
+ process.exitCode = 1;
158487
+ return;
158488
+ }
158489
+ const existing = repo.get(deviceId);
158490
+ if (!existing) {
158491
+ store.close();
158492
+ log14.info(`No such device: ${deviceId}`, void 0, "console");
158493
+ process.exitCode = 1;
158494
+ return;
158495
+ }
158496
+ if (existing.revoked_at == null && !opts.force) {
158497
+ store.close();
158498
+ log14.error(
158499
+ `${deviceId} is still active. Run 'devices revoke ${deviceId}' first, or pass --force.`
158500
+ );
158501
+ process.exitCode = 1;
158502
+ return;
158503
+ }
158504
+ repo.delete(deviceId);
158505
+ store.close();
158506
+ log14.info(`Erased ${deviceId}.`, void 0, "console");
158507
+ })
158508
+ );
158195
158509
  program2.command("pair").description("Print a pairing QR code (server must already be running)").option("-p, --port <number>", "Port the server is listening on", "8766").action(async (opts) => {
158196
158510
  const port = Number.parseInt(opts.port, 10);
158197
158511
  const apiKey = loadOrCreateApiKey();