@threadbase-sh/streamer 1.47.3 → 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -4637,11 +4637,11 @@ var require_tools = __commonJS({
4637
4637
  }
4638
4638
  }
4639
4639
  }
4640
- function buildFormatters(level, bindings, log13) {
4640
+ function buildFormatters(level, bindings, log15) {
4641
4641
  return {
4642
4642
  level,
4643
4643
  bindings,
4644
- log: log13
4644
+ log: log15
4645
4645
  };
4646
4646
  }
4647
4647
  function normalizeDestFileDescriptor(destination) {
@@ -5023,11 +5023,11 @@ var require_proto = __commonJS({
5023
5023
  }
5024
5024
  } else instance[serializersSym] = serializers;
5025
5025
  if (options.hasOwnProperty("formatters")) {
5026
- const { level, bindings: chindings, log: log13 } = options.formatters;
5026
+ const { level, bindings: chindings, log: log15 } = options.formatters;
5027
5027
  instance[formattersSym] = buildFormatters(
5028
5028
  level || formatters.level,
5029
5029
  chindings || resetChildingsFormatter,
5030
- log13 || formatters.log
5030
+ log15 || formatters.log
5031
5031
  );
5032
5032
  } else {
5033
5033
  instance[formattersSym] = buildFormatters(
@@ -8206,7 +8206,7 @@ var require_merge = __commonJS({
8206
8206
  var require_addPairToJSMap = __commonJS({
8207
8207
  "node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
8208
8208
  "use strict";
8209
- var log13 = require_log();
8209
+ var log15 = require_log();
8210
8210
  var merge2 = require_merge();
8211
8211
  var stringify = require_stringify();
8212
8212
  var identity = require_identity();
@@ -8255,7 +8255,7 @@ var require_addPairToJSMap = __commonJS({
8255
8255
  let jsonStr = JSON.stringify(strKey);
8256
8256
  if (jsonStr.length > 40)
8257
8257
  jsonStr = jsonStr.substring(0, 36) + '..."';
8258
- log13.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
8258
+ log15.warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`);
8259
8259
  ctx.mapKeyWarned = true;
8260
8260
  }
8261
8261
  return strKey;
@@ -13671,7 +13671,7 @@ var require_public_api = __commonJS({
13671
13671
  var composer = require_composer();
13672
13672
  var Document = require_Document();
13673
13673
  var errors = require_errors();
13674
- var log13 = require_log();
13674
+ var log15 = require_log();
13675
13675
  var identity = require_identity();
13676
13676
  var lineCounter = require_line_counter();
13677
13677
  var parser = require_parser();
@@ -13723,7 +13723,7 @@ var require_public_api = __commonJS({
13723
13723
  const doc = parseDocument(src, options);
13724
13724
  if (!doc)
13725
13725
  return null;
13726
- doc.warnings.forEach((warning) => log13.warn(doc.options.logLevel, warning));
13726
+ doc.warnings.forEach((warning) => log15.warn(doc.options.logLevel, warning));
13727
13727
  if (doc.errors.length > 0) {
13728
13728
  if (doc.options.logLevel !== "silent")
13729
13729
  throw doc.errors[0];
@@ -37243,7 +37243,7 @@ var require_logging = __commonJS({
37243
37243
  _logVerbosity = verbosity;
37244
37244
  };
37245
37245
  exports2.setLoggerVerbosity = setLoggerVerbosity;
37246
- var log13 = (severity, ...args) => {
37246
+ var log15 = (severity, ...args) => {
37247
37247
  let logFunction;
37248
37248
  if (severity >= _logVerbosity) {
37249
37249
  switch (severity) {
@@ -37265,7 +37265,7 @@ var require_logging = __commonJS({
37265
37265
  }
37266
37266
  }
37267
37267
  };
37268
- exports2.log = log13;
37268
+ exports2.log = log15;
37269
37269
  var tracersString = (_d = (_c = process.env.GRPC_NODE_TRACE) !== null && _c !== void 0 ? _c : process.env.GRPC_TRACE) !== null && _d !== void 0 ? _d : "";
37270
37270
  var enabledTracers = /* @__PURE__ */ new Set();
37271
37271
  var disabledTracers = /* @__PURE__ */ new Set();
@@ -127026,7 +127026,7 @@ function readMarker() {
127026
127026
  const parsed = JSON.parse(raw2);
127027
127027
  return MarkerSchema.parse(parsed);
127028
127028
  } catch (err) {
127029
- log8.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
127029
+ log10.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
127030
127030
  error: err instanceof Error ? err.message : String(err)
127031
127031
  });
127032
127032
  return null;
@@ -127043,7 +127043,7 @@ function writeMarker(marker) {
127043
127043
  function clearMarker() {
127044
127044
  if ((0, import_node_fs22.existsSync)(markerPath())) (0, import_node_fs22.rmSync)(markerPath());
127045
127045
  }
127046
- var import_node_fs22, import_node_path25, log8;
127046
+ var import_node_fs22, import_node_path25, log10;
127047
127047
  var init_marker = __esm({
127048
127048
  "src/lifecycle/marker.ts"() {
127049
127049
  "use strict";
@@ -127052,7 +127052,7 @@ var init_marker = __esm({
127052
127052
  init_logger();
127053
127053
  init_constants();
127054
127054
  init_marker_schema();
127055
- log8 = getLogger("lifecycle.marker");
127055
+ log10 = getLogger("lifecycle.marker");
127056
127056
  }
127057
127057
  });
127058
127058
 
@@ -127292,7 +127292,7 @@ function readPrefs() {
127292
127292
  try {
127293
127293
  return PrefsSchema.parse(JSON.parse((0, import_node_fs25.readFileSync)(path2, "utf8")));
127294
127294
  } catch (err) {
127295
- log10.warn(`prefs at ${path2} are malformed; treating as empty`, {
127295
+ log12.warn(`prefs at ${path2} are malformed; treating as empty`, {
127296
127296
  error: err instanceof Error ? err.message : String(err)
127297
127297
  });
127298
127298
  return { repos: {} };
@@ -127327,7 +127327,7 @@ function forgetAll() {
127327
127327
  const path2 = prefsPath();
127328
127328
  if ((0, import_node_fs25.existsSync)(path2)) (0, import_node_fs25.rmSync)(path2);
127329
127329
  }
127330
- var import_node_fs25, import_node_path26, log10, PrefsSchema;
127330
+ var import_node_fs25, import_node_path26, log12, PrefsSchema;
127331
127331
  var init_prefs = __esm({
127332
127332
  "src/lifecycle/prefs.ts"() {
127333
127333
  "use strict";
@@ -127336,7 +127336,7 @@ var init_prefs = __esm({
127336
127336
  init_zod();
127337
127337
  init_logger();
127338
127338
  init_constants();
127339
- log10 = getLogger("lifecycle.prefs");
127339
+ log12 = getLogger("lifecycle.prefs");
127340
127340
  PrefsSchema = external_exports.object({
127341
127341
  repos: external_exports.record(
127342
127342
  external_exports.string(),
@@ -127439,7 +127439,7 @@ function takeoverProd(opts) {
127439
127439
  `prod is already suspended by dev pid ${existing.devPid} (since ${existing.suspendedAt}). Stop that dev session first, or run 'tb-streamer prod doctor'.`
127440
127440
  );
127441
127441
  }
127442
- log11.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
127442
+ log13.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
127443
127443
  }
127444
127444
  getSupervisor().bootoutAgent();
127445
127445
  writeMarker({
@@ -127454,7 +127454,7 @@ function takeoverProd(opts) {
127454
127454
  const m2 = readMarker();
127455
127455
  if (m2 && m2.devPid === process.pid) {
127456
127456
  writeMarker({ ...m2, userHeld: true });
127457
- log11.info(
127457
+ log13.info(
127458
127458
  `prod is suspended (userHeld). Run 'tb-streamer prod start' to restore the supervised instance.`
127459
127459
  );
127460
127460
  }
@@ -127473,14 +127473,14 @@ function takeoverProd(opts) {
127473
127473
  });
127474
127474
  process.on("uncaughtException", (err) => {
127475
127475
  flipUserHeld();
127476
- log11.error(`uncaught: ${err.message}`);
127476
+ log13.error(`uncaught: ${err.message}`);
127477
127477
  process.exit(1);
127478
127478
  });
127479
127479
  process.on("exit", () => {
127480
127480
  flipUserHeld();
127481
127481
  });
127482
127482
  }
127483
- var import_node_net2, log11;
127483
+ var import_node_net2, log13;
127484
127484
  var init_dev_takeover = __esm({
127485
127485
  "src/lifecycle/dev-takeover.ts"() {
127486
127486
  "use strict";
@@ -127489,7 +127489,7 @@ var init_dev_takeover = __esm({
127489
127489
  init_marker();
127490
127490
  init_platform2();
127491
127491
  init_prefs();
127492
- log11 = getLogger("lifecycle.dev-takeover");
127492
+ log13 = getLogger("lifecycle.dev-takeover");
127493
127493
  }
127494
127494
  });
127495
127495
 
@@ -135584,7 +135584,7 @@ var REF_PREFIX = "ref: refs/heads/";
135584
135584
  var MAX_DEPTH = 6;
135585
135585
  function readGitBranch(projectPath) {
135586
135586
  if (!projectPath) return null;
135587
- const log13 = getLogger2();
135587
+ const log15 = getLogger2();
135588
135588
  let dir = projectPath;
135589
135589
  let depth = 0;
135590
135590
  while (depth < MAX_DEPTH) {
@@ -135593,11 +135593,11 @@ function readGitBranch(projectPath) {
135593
135593
  const content = (0, import_fs2.readFileSync)(headPath, "utf-8").trim();
135594
135594
  if (content.startsWith(REF_PREFIX)) {
135595
135595
  const branch = content.slice(REF_PREFIX.length);
135596
- log13.trace({ projectPath, dir, branch }, "git: branch resolved");
135596
+ log15.trace({ projectPath, dir, branch }, "git: branch resolved");
135597
135597
  return branch;
135598
135598
  }
135599
135599
  if (content.length >= 7) {
135600
- log13.trace({ projectPath, dir }, "git: detached HEAD");
135600
+ log15.trace({ projectPath, dir }, "git: detached HEAD");
135601
135601
  return "(detached)";
135602
135602
  }
135603
135603
  return null;
@@ -135608,7 +135608,7 @@ function readGitBranch(projectPath) {
135608
135608
  dir = parent;
135609
135609
  depth++;
135610
135610
  }
135611
- log13.trace({ projectPath }, "git: no .git found within depth");
135611
+ log15.trace({ projectPath }, "git: no .git found within depth");
135612
135612
  return null;
135613
135613
  }
135614
135614
  function generateMatches(meta3, query) {
@@ -135901,8 +135901,8 @@ function cleanSystemTags(text) {
135901
135901
  return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
135902
135902
  }
135903
135903
  async function parseMeta(filePath, account, tier) {
135904
- const log13 = getLogger2();
135905
- log13.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
135904
+ const log15 = getLogger2();
135905
+ log15.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
135906
135906
  const state = initialReducerState();
135907
135907
  const fileStream = (0, import_fs3.createReadStream)(filePath);
135908
135908
  const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
@@ -135919,22 +135919,22 @@ async function parseMeta(filePath, account, tier) {
135919
135919
  reduceLine(state, entry, tier);
135920
135920
  }
135921
135921
  } catch (err) {
135922
- log13.warn({ filePath, err }, "parseMeta: read failed");
135922
+ log15.warn({ filePath, err }, "parseMeta: read failed");
135923
135923
  return null;
135924
135924
  }
135925
135925
  if (state.badJsonLines > 0) {
135926
- log13.warn(
135926
+ log15.warn(
135927
135927
  { filePath, badJsonLines: state.badJsonLines },
135928
135928
  "parseMeta: skipped malformed JSON lines"
135929
135929
  );
135930
135930
  }
135931
135931
  const meta3 = finalizeMeta(state, filePath, account, tier);
135932
- if (!meta3) log13.trace({ filePath }, "parseMeta: no messages");
135932
+ if (!meta3) log15.trace({ filePath }, "parseMeta: no messages");
135933
135933
  return meta3;
135934
135934
  }
135935
135935
  async function parseConversation(filePath, account) {
135936
- const log13 = getLogger2();
135937
- log13.trace({ filePath, account }, "parseConversation: start");
135936
+ const log15 = getLogger2();
135937
+ log15.trace({ filePath, account }, "parseConversation: start");
135938
135938
  const messages = [];
135939
135939
  let badJsonLines = 0;
135940
135940
  const textParts = [];
@@ -135967,17 +135967,17 @@ async function parseConversation(filePath, account) {
135967
135967
  }
135968
135968
  }
135969
135969
  } catch (err) {
135970
- log13.warn({ filePath, err }, "parseConversation: read failed");
135970
+ log15.warn({ filePath, err }, "parseConversation: read failed");
135971
135971
  return null;
135972
135972
  }
135973
135973
  if (badJsonLines > 0) {
135974
- log13.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
135974
+ log15.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
135975
135975
  }
135976
135976
  if (messages.length === 0) {
135977
- log13.trace({ filePath }, "parseConversation: no messages");
135977
+ log15.trace({ filePath }, "parseConversation: no messages");
135978
135978
  return null;
135979
135979
  }
135980
- log13.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
135980
+ log15.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
135981
135981
  applyTeamInfo(messages, state);
135982
135982
  return {
135983
135983
  id: filePath,
@@ -136242,15 +136242,15 @@ async function detectDefaultProfile() {
136242
136242
  };
136243
136243
  }
136244
136244
  async function loadProfiles(configPath) {
136245
- const log13 = getLogger2();
136245
+ const log15 = getLogger2();
136246
136246
  try {
136247
136247
  const resolved = resolveConfigDir(configPath);
136248
136248
  const data = await (0, import_promises4.readFile)((0, import_path5.join)(resolved, PROFILES_FILE), "utf-8");
136249
136249
  const profiles = JSON.parse(data);
136250
- log13.debug({ configPath, count: profiles.length }, "profiles: loaded");
136250
+ log15.debug({ configPath, count: profiles.length }, "profiles: loaded");
136251
136251
  return profiles;
136252
136252
  } catch (err) {
136253
- log13.debug({ configPath, err }, "profiles: load failed, using default");
136253
+ log15.debug({ configPath, err }, "profiles: load failed, using default");
136254
136254
  const defaultProfile = await detectDefaultProfile();
136255
136255
  return [defaultProfile];
136256
136256
  }
@@ -136261,7 +136261,7 @@ function canonicalPath(p2) {
136261
136261
  var CodexCliProvider = class {
136262
136262
  name = CODEX_CLI_PROVIDER;
136263
136263
  async discover(roots) {
136264
- const log13 = getLogger2();
136264
+ const log15 = getLogger2();
136265
136265
  const results = [];
136266
136266
  for (const root of roots) {
136267
136267
  let paths;
@@ -136273,7 +136273,7 @@ var CodexCliProvider = class {
136273
136273
  unique: true
136274
136274
  });
136275
136275
  } catch (err) {
136276
- log13.warn({ root, err }, "codex discovery: glob failed");
136276
+ log15.warn({ root, err }, "codex discovery: glob failed");
136277
136277
  continue;
136278
136278
  }
136279
136279
  for (const filePath of paths) {
@@ -136281,7 +136281,7 @@ var CodexCliProvider = class {
136281
136281
  const s3 = await (0, import_promises5.stat)(filePath);
136282
136282
  if (s3.size > 0) results.push({ filePath: canonicalPath(filePath), account: "codex" });
136283
136283
  } catch (err) {
136284
- log13.warn({ filePath, err }, "codex discovery: stat failed");
136284
+ log15.warn({ filePath, err }, "codex discovery: stat failed");
136285
136285
  }
136286
136286
  }
136287
136287
  }
@@ -136425,7 +136425,7 @@ function getShortProjectName3(fullPath) {
136425
136425
  return fullPath.split("/").filter(Boolean).slice(-3).join("/");
136426
136426
  }
136427
136427
  async function parseCodexConversation(filePath, account) {
136428
- const log13 = getLogger2();
136428
+ const log15 = getLogger2();
136429
136429
  const messages = [];
136430
136430
  const textParts = [];
136431
136431
  let sessionId = "";
@@ -136461,7 +136461,7 @@ async function parseCodexConversation(filePath, account) {
136461
136461
  if (role === "user") lastUserText = text;
136462
136462
  }
136463
136463
  } catch (err) {
136464
- log13.warn({ filePath, err }, "parseCodexConversation: read failed");
136464
+ log15.warn({ filePath, err }, "parseCodexConversation: read failed");
136465
136465
  return null;
136466
136466
  }
136467
136467
  if (messages.length === 0) return null;
@@ -136483,7 +136483,7 @@ async function parseCodexConversation(filePath, account) {
136483
136483
  var EXCLUDED_SEGMENTS = ["/memory/", "/tool-results/"];
136484
136484
  var STAT_CONCURRENCY = 32;
136485
136485
  async function discoverJsonlFiles(dirs, onProgress) {
136486
- const log13 = getLogger2();
136486
+ const log15 = getLogger2();
136487
136487
  const results = [];
136488
136488
  for (const { projectsDir, account } of dirs) {
136489
136489
  let filePaths;
@@ -136494,7 +136494,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
136494
136494
  dot: false
136495
136495
  });
136496
136496
  } catch (err) {
136497
- log13.warn({ projectsDir, account, err }, "discovery: glob failed");
136497
+ log15.warn({ projectsDir, account, err }, "discovery: glob failed");
136498
136498
  continue;
136499
136499
  }
136500
136500
  const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));
@@ -136509,7 +136509,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
136509
136509
  const s3 = await (0, import_promises6.stat)(filePath);
136510
136510
  return { filePath, size: s3.size };
136511
136511
  } catch (err) {
136512
- log13.warn({ filePath, err }, "discovery: stat failed");
136512
+ log15.warn({ filePath, err }, "discovery: stat failed");
136513
136513
  return { filePath, size: -1 };
136514
136514
  }
136515
136515
  })
@@ -136525,7 +136525,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
136525
136525
  }
136526
136526
  }
136527
136527
  }
136528
- log13.debug(
136528
+ log15.debug(
136529
136529
  {
136530
136530
  projectsDir,
136531
136531
  account,
@@ -136539,7 +136539,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
136539
136539
  );
136540
136540
  onProgress?.(results.length);
136541
136541
  }
136542
- log13.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
136542
+ log15.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
136543
136543
  return results;
136544
136544
  }
136545
136545
  var ThreadbaseProvider = class {
@@ -136849,7 +136849,7 @@ function classify(filePath, existing) {
136849
136849
  return { change: "reindex", stat: stat42 };
136850
136850
  }
136851
136851
  async function parseMetaWithProvider(provider, filePath, account, tier) {
136852
- const log13 = getLogger2();
136852
+ const log15 = getLogger2();
136853
136853
  const acc = provider.createEmptyAccumulator();
136854
136854
  const rl = (0, import_readline3.createInterface)({
136855
136855
  input: (0, import_fs11.createReadStream)(filePath),
@@ -136867,11 +136867,11 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
136867
136867
  try {
136868
136868
  provider.reduceEntry(acc, entry, tier);
136869
136869
  } catch (err) {
136870
- log13.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
136870
+ log15.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
136871
136871
  }
136872
136872
  }
136873
136873
  } catch (err) {
136874
- log13.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
136874
+ log15.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
136875
136875
  return null;
136876
136876
  }
136877
136877
  return provider.finalize(acc, filePath, account, tier);
@@ -137045,8 +137045,8 @@ CREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_r
137045
137045
  function runMigrations(db) {
137046
137046
  const current = db.pragma("user_version", { simple: true });
137047
137047
  if (current >= SCHEMA_VERSION) return;
137048
- const log13 = getLogger2();
137049
- log13.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
137048
+ const log15 = getLogger2();
137049
+ log15.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
137050
137050
  if (current >= 1 && current < 2 && tableExists(db, "conversations")) {
137051
137051
  for (const [col, ddl] of [
137052
137052
  [
@@ -137088,7 +137088,7 @@ function openDatabase(dbPath) {
137088
137088
  }
137089
137089
  var FULL_RECONCILE_EVERY_N_SCANS = 20;
137090
137090
  async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
137091
- const log13 = getLogger2();
137091
+ const log15 = getLogger2();
137092
137092
  if (options.fullRescan) {
137093
137093
  return discoverJsonlFiles(dirs);
137094
137094
  }
@@ -137130,7 +137130,7 @@ async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
137130
137130
  }
137131
137131
  }
137132
137132
  }
137133
- log13.debug(
137133
+ log15.debug(
137134
137134
  { totalFiles: results.length, dirs: dirs.length },
137135
137135
  "dir-watermark: gated discovery complete"
137136
137136
  );
@@ -137636,7 +137636,7 @@ var PersistentEngine = class {
137636
137636
  // changed since the last index, and upsert their metadata. Returns the number
137637
137637
  // of files seen on disk this pass (the scan "scanned" count).
137638
137638
  async indexAll(activeProfiles, options) {
137639
- const log13 = getLogger2();
137639
+ const log15 = getLogger2();
137640
137640
  const tier = resolveTier(options.tier ?? "standard", options.tiers);
137641
137641
  const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];
137642
137642
  const discovered = [];
@@ -137700,7 +137700,7 @@ var PersistentEngine = class {
137700
137700
  for (const path2 of this.files.activePathsByAccounts([...coveredAccounts])) {
137701
137701
  if (!seen.has(path2)) this.markDeleted(path2);
137702
137702
  }
137703
- log13.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
137703
+ log15.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
137704
137704
  return { scanned };
137705
137705
  }
137706
137706
  // (Re)index a single file. Classifies the change vs. the persisted cursor:
@@ -137711,7 +137711,7 @@ var PersistentEngine = class {
137711
137711
  // alongside the meta so callers (refreshFile) can keep, extend, or evict
137712
137712
  // their own per-file caches without re-stat'ing the file (racy) themselves.
137713
137713
  async indexFile(rawFilePath, account, tierName, customTiers, resolveGitBranch, force = false, provider) {
137714
- const log13 = getLogger2();
137714
+ const log15 = getLogger2();
137715
137715
  const tier = resolveTier(tierName, customTiers);
137716
137716
  const filePath = canonicalPath(rawFilePath);
137717
137717
  const existing = this.files.getByPath(filePath);
@@ -137742,7 +137742,7 @@ var PersistentEngine = class {
137742
137742
  try {
137743
137743
  result = await tailReduce(filePath, startOffset, startLine, state, tier);
137744
137744
  } catch (err) {
137745
- log13.warn({ filePath, err }, "persistent: tail read failed");
137745
+ log15.warn({ filePath, err }, "persistent: tail read failed");
137746
137746
  return { meta: null, change };
137747
137747
  }
137748
137748
  const meta3 = finalizeMeta(state, filePath, account, tier);
@@ -137785,7 +137785,7 @@ var PersistentEngine = class {
137785
137785
  )
137786
137786
  );
137787
137787
  }
137788
- log13.debug(
137788
+ log15.debug(
137789
137789
  { filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta3.messageCount },
137790
137790
  "persistent: indexed file"
137791
137791
  );
@@ -137797,7 +137797,7 @@ var PersistentEngine = class {
137797
137797
  // so the next pass classifies an unchanged file as "unchanged" and skips it;
137798
137798
  // any change reparses from 0 again. No reducer_state is persisted.
137799
137799
  async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
137800
- const log13 = getLogger2();
137800
+ const log15 = getLogger2();
137801
137801
  const meta3 = await parseMetaWithProvider(provider, filePath, account, tier);
137802
137802
  if (!meta3) {
137803
137803
  this.markDeleted(filePath);
@@ -137825,7 +137825,7 @@ var PersistentEngine = class {
137825
137825
  });
137826
137826
  });
137827
137827
  upsert();
137828
- log13.debug(
137828
+ log15.debug(
137829
137829
  { filePath, provider: provider.name, msgs: meta3.messageCount },
137830
137830
  "persistent: indexed provider file"
137831
137831
  );
@@ -137939,7 +137939,7 @@ var FileWatcher = class {
137939
137939
  // Resolves once every underlying chokidar watcher has finished its initial
137940
137940
  // scan, so the caller knows subsequent FS changes will be observed.
137941
137941
  async start() {
137942
- const log13 = getLogger2();
137942
+ const log15 = getLogger2();
137943
137943
  const ready = [];
137944
137944
  for (const profile of this.profiles) {
137945
137945
  const dir = getProjectsDir(profile);
@@ -137951,7 +137951,7 @@ var FileWatcher = class {
137951
137951
  watcher.on("add", (p2) => this.dispatch(p2, profile.id, "add")).on("change", (p2) => this.dispatch(p2, profile.id, "change")).on("unlink", (p2) => this.dispatch(p2, profile.id, "unlink"));
137952
137952
  ready.push(new Promise((resolve4) => watcher.once("ready", () => resolve4())));
137953
137953
  this.watchers.push(watcher);
137954
- log13.debug({ dir, account: profile.id }, "watcher: watching");
137954
+ log15.debug({ dir, account: profile.id }, "watcher: watching");
137955
137955
  }
137956
137956
  await Promise.all(ready);
137957
137957
  }
@@ -137998,14 +137998,14 @@ var IndexQueue = class {
137998
137998
  async drain() {
137999
137999
  if (this.running) return;
138000
138000
  this.running = true;
138001
- const log13 = getLogger2();
138001
+ const log15 = getLogger2();
138002
138002
  while (this.pending.size > 0) {
138003
138003
  const [path2, job] = this.pending.entries().next().value;
138004
138004
  this.pending.delete(path2);
138005
138005
  try {
138006
138006
  await this.process(job);
138007
138007
  } catch (err) {
138008
- log13.warn({ filePath: job.filePath, err }, "index-queue: job failed");
138008
+ log15.warn({ filePath: job.filePath, err }, "index-queue: job failed");
138009
138009
  }
138010
138010
  }
138011
138011
  this.running = false;
@@ -138114,13 +138114,13 @@ var ConversationScanner = class {
138114
138114
  // active metas and run the identical filter/sort/view/paginate pipeline as
138115
138115
  // the in-memory path — guaranteeing an identical ScanResult shape.
138116
138116
  async scanPersistent(activeProfiles, options) {
138117
- const log13 = getLogger2();
138117
+ const log15 = getLogger2();
138118
138118
  const startedAt = Date.now();
138119
138119
  const engine = this.engine();
138120
138120
  const { scanned } = await engine.indexAll(activeProfiles, options);
138121
138121
  const allMetas = engine.allActive();
138122
138122
  const { conversations, total } = this.finalize(allMetas, options);
138123
- log13.info(
138123
+ log15.info(
138124
138124
  { scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },
138125
138125
  "scan: complete (persistent)"
138126
138126
  );
@@ -138147,10 +138147,10 @@ var ConversationScanner = class {
138147
138147
  return { conversations, total };
138148
138148
  }
138149
138149
  async scanInMemory(activeProfiles, options) {
138150
- const log13 = getLogger2();
138150
+ const log15 = getLogger2();
138151
138151
  const startedAt = Date.now();
138152
138152
  const tier = this.lastTier;
138153
- log13.info(
138153
+ log15.info(
138154
138154
  {
138155
138155
  activeProfiles: activeProfiles.length,
138156
138156
  tier: tier.name,
@@ -138204,7 +138204,7 @@ var ConversationScanner = class {
138204
138204
  return meta3;
138205
138205
  } catch (err) {
138206
138206
  parseFailures++;
138207
- log13.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
138207
+ log15.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
138208
138208
  return null;
138209
138209
  }
138210
138210
  })
@@ -138224,12 +138224,12 @@ var ConversationScanner = class {
138224
138224
  options.onBatch?.(batchMetas);
138225
138225
  }
138226
138226
  scanned += batch.length;
138227
- log13.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
138227
+ log15.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
138228
138228
  options.onProgress?.(scanned, totalFiles);
138229
138229
  }
138230
138230
  const { conversations, total } = this.finalize(allMetas, options);
138231
138231
  const elapsedMs = Date.now() - startedAt;
138232
- log13.info(
138232
+ log15.info(
138233
138233
  {
138234
138234
  totalFiles,
138235
138235
  scanned,
@@ -138243,13 +138243,13 @@ var ConversationScanner = class {
138243
138243
  return { conversations, total, scanned };
138244
138244
  }
138245
138245
  async search(query, options = {}) {
138246
- const log13 = getLogger2();
138247
- log13.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
138246
+ const log15 = getLogger2();
138247
+ log15.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
138248
138248
  let results;
138249
138249
  if (this.persistent) {
138250
138250
  const engine = this.engine();
138251
138251
  if (engine.conversations.count() === 0) {
138252
- log13.debug("search: persistent index empty, triggering scan");
138252
+ log15.debug("search: persistent index empty, triggering scan");
138253
138253
  const profiles = await this.resolveProfiles(options.profiles);
138254
138254
  const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
138255
138255
  await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
@@ -138262,7 +138262,7 @@ var ConversationScanner = class {
138262
138262
  }));
138263
138263
  } else {
138264
138264
  if (this.indexer.getDocumentCount() === 0) {
138265
- log13.debug("search: index empty, triggering scan");
138265
+ log15.debug("search: index empty, triggering scan");
138266
138266
  await this.scan({ ...options, limit: void 0, offset: void 0 });
138267
138267
  }
138268
138268
  results = this.indexer.search(query, {
@@ -138305,23 +138305,23 @@ var ConversationScanner = class {
138305
138305
  const limit = options.limit ?? 50;
138306
138306
  const offset = options.offset ?? 0;
138307
138307
  const sliced = results.slice(offset, offset + limit);
138308
- log13.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
138308
+ log15.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
138309
138309
  return sliced;
138310
138310
  }
138311
138311
  async getConversation(id, _options) {
138312
- const log13 = getLogger2();
138312
+ const log15 = getLogger2();
138313
138313
  const cid = canonicalPath(id);
138314
138314
  const cached4 = this.conversationLRU.get(cid);
138315
138315
  if (cached4) {
138316
- log13.debug({ id }, "getConversation: cache hit");
138316
+ log15.debug({ id }, "getConversation: cache hit");
138317
138317
  return cached4.conversation;
138318
138318
  }
138319
138319
  const meta3 = this.persistent ? this.engine().getByIdOrSession(cid) : this.metadataCache.get(cid) ?? this.resolveSessionId(cid);
138320
138320
  if (!meta3) {
138321
- log13.debug({ id }, "getConversation: not found in metadata");
138321
+ log15.debug({ id }, "getConversation: not found in metadata");
138322
138322
  return null;
138323
138323
  }
138324
- log13.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
138324
+ log15.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
138325
138325
  try {
138326
138326
  if (this.persistent && meta3.provider !== CODEX_CLI_PROVIDER) {
138327
138327
  const parsed = await parseConversationResumable(meta3.filePath, meta3.account);
@@ -138334,7 +138334,7 @@ var ConversationScanner = class {
138334
138334
  }
138335
138335
  return conversation;
138336
138336
  } catch (err) {
138337
- log13.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
138337
+ log15.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
138338
138338
  return null;
138339
138339
  }
138340
138340
  }
@@ -138420,7 +138420,7 @@ var ConversationScanner = class {
138420
138420
  return refresh;
138421
138421
  }
138422
138422
  async doRefreshFile(filePath, account) {
138423
- const log13 = getLogger2();
138423
+ const log15 = getLogger2();
138424
138424
  if (this.persistent) {
138425
138425
  const engine = this.engine();
138426
138426
  const previous2 = engine.getByIdOrSession(filePath);
@@ -138447,7 +138447,7 @@ var ConversationScanner = class {
138447
138447
  } else if (change === "appended") {
138448
138448
  await this.extendCachedConversations(cacheKeys, filePath, meta22.account);
138449
138449
  }
138450
- log13.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
138450
+ log15.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
138451
138451
  return meta22;
138452
138452
  }
138453
138453
  const previous = this.metadataCache.get(filePath);
@@ -138456,7 +138456,7 @@ var ConversationScanner = class {
138456
138456
  try {
138457
138457
  meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier);
138458
138458
  } catch (err) {
138459
- log13.warn({ filePath, err }, "refreshFile: parseMeta threw");
138459
+ log15.warn({ filePath, err }, "refreshFile: parseMeta threw");
138460
138460
  meta3 = null;
138461
138461
  }
138462
138462
  const evict = (m2) => {
@@ -138472,7 +138472,7 @@ var ConversationScanner = class {
138472
138472
  this.removeFromSessionIndex(previous);
138473
138473
  this.indexer.removeDocument(previous.id);
138474
138474
  }
138475
- log13.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
138475
+ log15.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
138476
138476
  return null;
138477
138477
  }
138478
138478
  meta3.gitBranch = readGitBranch(meta3.projectPath);
@@ -138485,7 +138485,7 @@ var ConversationScanner = class {
138485
138485
  } else {
138486
138486
  this.indexer.addDocument(meta3);
138487
138487
  }
138488
- log13.debug(
138488
+ log15.debug(
138489
138489
  { filePath, messageCount: meta3.messageCount },
138490
138490
  "refreshFile: updated in-memory indexes"
138491
138491
  );
@@ -142369,7 +142369,7 @@ function createLogsRoutes() {
142369
142369
 
142370
142370
  // src/api/routes/misc.routes.ts
142371
142371
  var import_node_child_process2 = require("child_process");
142372
- var import_node_crypto = require("crypto");
142372
+ var import_node_crypto2 = require("crypto");
142373
142373
  var import_os7 = require("os");
142374
142374
 
142375
142375
  // src/db/repositories/push.repository.ts
@@ -142628,6 +142628,170 @@ var PushRepository = class {
142628
142628
 
142629
142629
  // src/api/routes/misc.routes.ts
142630
142630
  init_logger();
142631
+
142632
+ // src/services/push/apnsClient.ts
142633
+ var import_node_crypto = require("crypto");
142634
+ var import_node_http2 = require("http2");
142635
+ init_logger();
142636
+ var log3 = getLogger("apns");
142637
+ var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
142638
+ var APNS_MAX_PAYLOAD_BYTES = 4096;
142639
+ var JWT_TTL_SECONDS = 3e3;
142640
+ var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
142641
+ "BadDeviceToken",
142642
+ "DeviceTokenNotForTopic",
142643
+ "Unregistered",
142644
+ "ExpiredToken"
142645
+ ]);
142646
+ function base64url3(input) {
142647
+ return Buffer.from(input).toString("base64url");
142648
+ }
142649
+ function readApnsCredentialsFromEnv(env = process.env) {
142650
+ const key = env.APNS_KEY;
142651
+ if (!key || key.trim().length === 0) return null;
142652
+ const keyId = env.APNS_KEY_ID?.trim();
142653
+ const teamId = env.APNS_TEAM_ID?.trim();
142654
+ const bundleId = env.APNS_BUNDLE_ID?.trim();
142655
+ if (!keyId || !teamId || !bundleId) return null;
142656
+ const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
142657
+ return { key, keyId, teamId, bundleId, host };
142658
+ }
142659
+ function describeMissingApnsCredentials(env = process.env) {
142660
+ if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
142661
+ return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
142662
+ }
142663
+ const missing = [
142664
+ ["APNS_KEY_ID", env.APNS_KEY_ID],
142665
+ ["APNS_TEAM_ID", env.APNS_TEAM_ID],
142666
+ ["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
142667
+ ].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
142668
+ if (missing.length === 0) return null;
142669
+ return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
142670
+ }
142671
+ var ApnsClient = class {
142672
+ constructor(creds) {
142673
+ this.creds = creds;
142674
+ }
142675
+ creds;
142676
+ session = null;
142677
+ cachedJwt = null;
142678
+ /**
142679
+ * The `apns-topic` for Live Activity pushes.
142680
+ *
142681
+ * The `.push-type.liveactivity` suffix is mandatory and is why the signing key
142682
+ * must be Team Scoped (All Topics) — a key scoped to the bundle id alone
142683
+ * cannot sign this topic.
142684
+ */
142685
+ get topic() {
142686
+ return `${this.creds.bundleId}.push-type.liveactivity`;
142687
+ }
142688
+ /**
142689
+ * Mint or reuse the provider JWT.
142690
+ *
142691
+ * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
142692
+ * token older than an hour, but minting one per request is wasteful and can
142693
+ * trip APNs' provider-token-update throttle.
142694
+ */
142695
+ getJwt(now = Date.now()) {
142696
+ const nowSeconds = Math.floor(now / 1e3);
142697
+ if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
142698
+ return this.cachedJwt.token;
142699
+ }
142700
+ const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
142701
+ const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
142702
+ const signingInput = `${header}.${payload}`;
142703
+ const signature = (0, import_node_crypto.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
142704
+ const token = `${signingInput}.${base64url3(signature)}`;
142705
+ this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
142706
+ return token;
142707
+ }
142708
+ /**
142709
+ * Reuse one HTTP/2 session across sends.
142710
+ *
142711
+ * APNs expects a long-lived connection; a fresh TLS handshake per push is slow
142712
+ * and Apple treats connection churn as abuse.
142713
+ */
142714
+ getSession() {
142715
+ if (this.session && !this.session.closed && !this.session.destroyed) {
142716
+ return this.session;
142717
+ }
142718
+ const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
142719
+ session.on("error", (err) => {
142720
+ log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
142721
+ });
142722
+ this.session = session;
142723
+ return session;
142724
+ }
142725
+ /**
142726
+ * Send one push.
142727
+ *
142728
+ * Resolves with a result rather than rejecting on an APNs rejection: a
142729
+ * rejected push is an expected outcome the caller must act on (retire the
142730
+ * token), not an exception. Only a genuinely unexpected local failure throws,
142731
+ * and the caller logs it.
142732
+ */
142733
+ async send(args) {
142734
+ const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
142735
+ if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
142736
+ throw new Error(
142737
+ `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
142738
+ );
142739
+ }
142740
+ const session = this.getSession();
142741
+ const headers = {
142742
+ [import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
142743
+ [import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
142744
+ [import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
142745
+ "apns-push-type": "liveactivity",
142746
+ "apns-topic": this.topic,
142747
+ "apns-priority": String(args.priority ?? 10),
142748
+ ...args.expirationSeconds != null && {
142749
+ "apns-expiration": String(args.expirationSeconds)
142750
+ },
142751
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
142752
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
142753
+ };
142754
+ return new Promise((resolve4, reject) => {
142755
+ const req = session.request(headers);
142756
+ req.setTimeout(args.timeoutMs ?? 1e4, () => {
142757
+ req.close(import_node_http2.constants.NGHTTP2_CANCEL);
142758
+ resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
142759
+ });
142760
+ let status = 0;
142761
+ req.on("response", (resHeaders) => {
142762
+ status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
142763
+ });
142764
+ const chunks = [];
142765
+ req.on("data", (chunk) => chunks.push(chunk));
142766
+ req.on("error", reject);
142767
+ req.on("end", () => {
142768
+ const raw2 = Buffer.concat(chunks).toString("utf-8");
142769
+ let reason;
142770
+ if (raw2.length > 0) {
142771
+ try {
142772
+ reason = JSON.parse(raw2).reason;
142773
+ } catch {
142774
+ reason = raw2.slice(0, 200);
142775
+ }
142776
+ }
142777
+ resolve4({
142778
+ ok: status === 200,
142779
+ status,
142780
+ reason,
142781
+ tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
142782
+ });
142783
+ });
142784
+ req.end(body);
142785
+ });
142786
+ }
142787
+ /** Close the shared connection. Called on server shutdown. */
142788
+ close() {
142789
+ this.session?.close();
142790
+ this.session = null;
142791
+ }
142792
+ };
142793
+
142794
+ // src/api/routes/misc.routes.ts
142631
142795
  function numberOrNull(value) {
142632
142796
  return typeof value === "number" && Number.isFinite(value) ? value : null;
142633
142797
  }
@@ -142657,11 +142821,22 @@ function readRawBody3(req) {
142657
142821
  function verifyWebhookSignature(body, header, secret) {
142658
142822
  if (!header) return false;
142659
142823
  const provided = header.startsWith("sha256=") ? header.slice(7) : header;
142660
- const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
142824
+ const expected = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
142661
142825
  const a = Buffer.from(provided, "utf-8");
142662
142826
  const b2 = Buffer.from(expected, "utf-8");
142663
142827
  if (a.length !== b2.length) return false;
142664
- return (0, import_node_crypto.timingSafeEqual)(a, b2);
142828
+ return (0, import_node_crypto2.timingSafeEqual)(a, b2);
142829
+ }
142830
+ function describePushCapability(liveActivityEnabled, env = process.env) {
142831
+ if (liveActivityEnabled) return { liveActivity: true, notifications: false };
142832
+ return {
142833
+ liveActivity: false,
142834
+ notifications: false,
142835
+ // describeMissingApnsCredentials only explains a *credential* gap and
142836
+ // returns null once the credentials are complete — reachable here, because
142837
+ // an unavailable token store disables the feature with the key still set.
142838
+ liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
142839
+ };
142665
142840
  }
142666
142841
  var clientLog = getLogger("client");
142667
142842
  var createMiscRoutes = (deps) => {
@@ -142684,7 +142859,12 @@ var createMiscRoutes = (deps) => {
142684
142859
  featureFlags: true,
142685
142860
  // Same contract: this server serves GET /api/projects/summary, which the
142686
142861
  // Hub's grouped views need before they can draw a tree.
142687
- projectSummary: true
142862
+ projectSummary: true,
142863
+ // Delivery capability, not endpoint support: whether this server can
142864
+ // actually send a push, so mobile can hide an affordance instead of
142865
+ // registering tokens nothing will ever send to. Absent on older servers,
142866
+ // which a client should read as "unknown", not "unavailable".
142867
+ push: describePushCapability(deps.liveActivityPushEnabled())
142688
142868
  });
142689
142869
  });
142690
142870
  app.get("/api/profiles", (c) => c.json([]));
@@ -142745,9 +142925,10 @@ var createMiscRoutes = (deps) => {
142745
142925
  return c.json({ ok: true });
142746
142926
  });
142747
142927
  app.get("/api/push/health", (c) => {
142928
+ const push = describePushCapability(deps.liveActivityPushEnabled());
142748
142929
  const repo = deps.pushRepo();
142749
- if (!repo) return c.json({ tokens: [], available: false });
142750
- return c.json({ tokens: repo.listHealth(), available: true });
142930
+ if (!repo) return c.json({ tokens: [], available: false, push });
142931
+ return c.json({ tokens: repo.listHealth(), available: true, push });
142751
142932
  });
142752
142933
  app.post("/api/__update", async (c) => {
142753
142934
  const cfg = loadUpdateConfig();
@@ -142815,7 +142996,7 @@ var createPairRoutes = (deps) => {
142815
142996
  };
142816
142997
 
142817
142998
  // src/api/routes/progress.routes.ts
142818
- var import_node_crypto2 = __toESM(require("crypto"), 1);
142999
+ var import_node_crypto3 = __toESM(require("crypto"), 1);
142819
143000
  function readRawBody4(req) {
142820
143001
  return new Promise((resolve4, reject) => {
142821
143002
  const chunks = [];
@@ -142826,10 +143007,10 @@ function readRawBody4(req) {
142826
143007
  }
142827
143008
  function verifySignature(rawBody, signature, secret) {
142828
143009
  if (!signature || signature.length === 0) return false;
142829
- const expected = import_node_crypto2.default.createHmac("sha256", secret).update(rawBody).digest("hex");
143010
+ const expected = import_node_crypto3.default.createHmac("sha256", secret).update(rawBody).digest("hex");
142830
143011
  if (expected.length !== signature.length) return false;
142831
143012
  try {
142832
- return import_node_crypto2.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
143013
+ return import_node_crypto3.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
142833
143014
  } catch {
142834
143015
  return false;
142835
143016
  }
@@ -143382,7 +143563,7 @@ var import_promises13 = require("timers/promises");
143382
143563
 
143383
143564
  // src/db/query-timing.ts
143384
143565
  init_logger();
143385
- var log3 = getLogger("db");
143566
+ var log4 = getLogger("db");
143386
143567
  var DEFAULT_SLOW_QUERY_MS = 35;
143387
143568
  var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
143388
143569
  function deriveLabel(sql) {
@@ -143398,15 +143579,15 @@ function resolveSlowMs() {
143398
143579
  }
143399
143580
  function record2(label, ms2, rows, slowMs) {
143400
143581
  if (slowMs > 0 && ms2 >= slowMs) {
143401
- log3.warn(
143582
+ log4.warn(
143402
143583
  `[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
143403
143584
  { event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143404
143585
  "pino"
143405
143586
  );
143406
143587
  return;
143407
143588
  }
143408
- if (log3.pino.isLevelEnabled("debug")) {
143409
- log3.debug(
143589
+ if (log4.pino.isLevelEnabled("debug")) {
143590
+ log4.debug(
143410
143591
  `[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
143411
143592
  { event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143412
143593
  "pino"
@@ -145950,10 +146131,10 @@ function fingerprintOf(ids) {
145950
146131
  return `sha256:${(0, import_crypto11.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
145951
146132
  }
145952
146133
  var CacheIntegrityMonitor = class {
145953
- constructor(cache, wsHub, log13, cacheDir, rescan, runDuringReset) {
146134
+ constructor(cache, wsHub, log15, cacheDir, rescan, runDuringReset) {
145954
146135
  this.cache = cache;
145955
146136
  this.wsHub = wsHub;
145956
- this.log = log13;
146137
+ this.log = log15;
145957
146138
  this.cacheDir = cacheDir;
145958
146139
  this.rescan = rescan;
145959
146140
  this.runDuringReset = runDuringReset;
@@ -146810,166 +146991,100 @@ function deriveProjectChatTitle(input) {
146810
146991
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
146811
146992
  }
146812
146993
 
146813
- // src/services/push/apnsClient.ts
146814
- var import_node_crypto3 = require("crypto");
146815
- var import_node_http2 = require("http2");
146994
+ // src/services/push/expoPushSender.ts
146816
146995
  init_logger();
146817
- var log4 = getLogger("apns");
146818
- var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
146819
- var APNS_MAX_PAYLOAD_BYTES = 4096;
146820
- var JWT_TTL_SECONDS = 3e3;
146821
- var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
146822
- "BadDeviceToken",
146823
- "DeviceTokenNotForTopic",
146824
- "Unregistered",
146825
- "ExpiredToken"
146826
- ]);
146827
- function base64url3(input) {
146828
- return Buffer.from(input).toString("base64url");
146829
- }
146830
- function readApnsCredentialsFromEnv(env = process.env) {
146831
- const key = env.APNS_KEY;
146832
- if (!key || key.trim().length === 0) return null;
146833
- const keyId = env.APNS_KEY_ID?.trim();
146834
- const teamId = env.APNS_TEAM_ID?.trim();
146835
- const bundleId = env.APNS_BUNDLE_ID?.trim();
146836
- if (!keyId || !teamId || !bundleId) return null;
146837
- const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
146838
- return { key, keyId, teamId, bundleId, host };
146839
- }
146840
- function describeMissingApnsCredentials(env = process.env) {
146841
- if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
146842
- return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
146843
- }
146844
- const missing = [
146845
- ["APNS_KEY_ID", env.APNS_KEY_ID],
146846
- ["APNS_TEAM_ID", env.APNS_TEAM_ID],
146847
- ["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
146848
- ].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
146849
- if (missing.length === 0) return null;
146850
- return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
146851
- }
146852
- var ApnsClient = class {
146853
- constructor(creds) {
146854
- this.creds = creds;
146855
- }
146856
- creds;
146857
- session = null;
146858
- cachedJwt = null;
146859
- /**
146860
- * The `apns-topic` for Live Activity pushes.
146861
- *
146862
- * The `.push-type.liveactivity` suffix is mandatory and is why the signing key
146863
- * must be Team Scoped (All Topics) — a key scoped to the bundle id alone
146864
- * cannot sign this topic.
146865
- */
146866
- get topic() {
146867
- return `${this.creds.bundleId}.push-type.liveactivity`;
146868
- }
146996
+ var log5 = getLogger("expo-push");
146997
+ var EXPO_PUSH_ENDPOINT = "https://exp.host/--/api/v2/push/send";
146998
+ var EXPO_PUSH_BATCH_SIZE = 100;
146999
+ var DEAD_TOKEN_ERROR = "DeviceNotRegistered";
147000
+ var ExpoPushSender = class {
146869
147001
  /**
146870
- * Mint or reuse the provider JWT.
146871
- *
146872
- * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
146873
- * token older than an hour, but minting one per request is wasteful and can
146874
- * trip APNs' provider-token-update throttle.
147002
+ * @param accessToken Expo access token, when the project has enhanced
147003
+ * security enabled. Optional on purpose: a self-hoster does not own the Expo
147004
+ * project and cannot obtain one, so requiring it would break the deployment
147005
+ * this transport exists to serve.
146875
147006
  */
146876
- getJwt(now = Date.now()) {
146877
- const nowSeconds = Math.floor(now / 1e3);
146878
- if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
146879
- return this.cachedJwt.token;
146880
- }
146881
- const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
146882
- const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
146883
- const signingInput = `${header}.${payload}`;
146884
- const signature = (0, import_node_crypto3.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
146885
- const token = `${signingInput}.${base64url3(signature)}`;
146886
- this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
146887
- return token;
147007
+ constructor(repo, accessToken) {
147008
+ this.repo = repo;
147009
+ this.accessToken = accessToken;
146888
147010
  }
147011
+ repo;
147012
+ accessToken;
146889
147013
  /**
146890
- * Reuse one HTTP/2 session across sends.
147014
+ * Send one message to every deliverable Expo token.
146891
147015
  *
146892
- * APNs expects a long-lived connection; a fresh TLS handshake per push is slow
146893
- * and Apple treats connection churn as abuse.
147016
+ * Sends are independent: Expo returns a ticket per token in one response, so
147017
+ * a dead device is recorded against its own row and never silences the other
147018
+ * devices in the batch.
146894
147019
  */
146895
- getSession() {
146896
- if (this.session && !this.session.closed && !this.session.destroyed) {
146897
- return this.session;
147020
+ async send(message, now = Date.now()) {
147021
+ const rows = this.repo.listDeliverable();
147022
+ const outcome = { attempted: rows.length, succeeded: 0, retired: 0 };
147023
+ if (rows.length === 0) return outcome;
147024
+ for (let i = 0; i < rows.length; i += EXPO_PUSH_BATCH_SIZE) {
147025
+ const chunk = rows.slice(i, i + EXPO_PUSH_BATCH_SIZE);
147026
+ await this.sendChunk(chunk, message, now, outcome);
146898
147027
  }
146899
- const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
146900
- session.on("error", (err) => {
146901
- log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
146902
- });
146903
- this.session = session;
146904
- return session;
147028
+ return outcome;
146905
147029
  }
146906
- /**
146907
- * Send one push.
146908
- *
146909
- * Resolves with a result rather than rejecting on an APNs rejection: a
146910
- * rejected push is an expected outcome the caller must act on (retire the
146911
- * token), not an exception. Only a genuinely unexpected local failure throws,
146912
- * and the caller logs it.
146913
- */
146914
- async send(args) {
146915
- const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
146916
- if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
146917
- throw new Error(
146918
- `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
146919
- );
146920
- }
146921
- const session = this.getSession();
146922
- const headers = {
146923
- [import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
146924
- [import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
146925
- [import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
146926
- "apns-push-type": "liveactivity",
146927
- "apns-topic": this.topic,
146928
- "apns-priority": String(args.priority ?? 10),
146929
- ...args.expirationSeconds != null && {
146930
- "apns-expiration": String(args.expirationSeconds)
146931
- },
146932
- [import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
146933
- [import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
146934
- };
146935
- return new Promise((resolve4, reject) => {
146936
- const req = session.request(headers);
146937
- req.setTimeout(args.timeoutMs ?? 1e4, () => {
146938
- req.close(import_node_http2.constants.NGHTTP2_CANCEL);
146939
- resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
146940
- });
146941
- let status = 0;
146942
- req.on("response", (resHeaders) => {
146943
- status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
147030
+ async sendChunk(rows, message, now, outcome) {
147031
+ let payload;
147032
+ try {
147033
+ const res = await fetch(EXPO_PUSH_ENDPOINT, {
147034
+ method: "POST",
147035
+ headers: {
147036
+ "content-type": "application/json",
147037
+ accept: "application/json",
147038
+ ...this.accessToken && { authorization: `Bearer ${this.accessToken}` }
147039
+ },
147040
+ body: JSON.stringify(rows.map((row) => ({ to: row.token, ...message })))
146944
147041
  });
146945
- const chunks = [];
146946
- req.on("data", (chunk) => chunks.push(chunk));
146947
- req.on("error", reject);
146948
- req.on("end", () => {
146949
- const raw2 = Buffer.concat(chunks).toString("utf-8");
146950
- let reason;
146951
- if (raw2.length > 0) {
146952
- try {
146953
- reason = JSON.parse(raw2).reason;
146954
- } catch {
146955
- reason = raw2.slice(0, 200);
146956
- }
146957
- }
146958
- resolve4({
146959
- ok: status === 200,
146960
- status,
146961
- reason,
146962
- tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
147042
+ if (!res.ok) {
147043
+ const code = `HTTP_${res.status}`;
147044
+ for (const row of rows) this.repo.recordFailure(row.token, code, now);
147045
+ log5.warn("expo_push.request_rejected", {
147046
+ event: "expo_push.request_rejected",
147047
+ status: res.status,
147048
+ tokens: rows.length
146963
147049
  });
147050
+ return;
147051
+ }
147052
+ payload = await res.json();
147053
+ } catch (err) {
147054
+ for (const row of rows) this.repo.recordFailure(row.token, "SendError", now);
147055
+ log5.error("expo_push.send_failed", {
147056
+ event: "expo_push.send_failed",
147057
+ tokens: rows.length,
147058
+ err: String(err)
147059
+ });
147060
+ return;
147061
+ }
147062
+ const tickets = payload?.data;
147063
+ rows.forEach((row, index) => {
147064
+ const ticket = Array.isArray(tickets) ? tickets[index] : void 0;
147065
+ if (!ticket) {
147066
+ this.repo.recordFailure(row.token, "NoTicket", now);
147067
+ return;
147068
+ }
147069
+ if (ticket.status === "ok") {
147070
+ this.repo.recordSuccess(row.token, now);
147071
+ outcome.succeeded += 1;
147072
+ return;
147073
+ }
147074
+ const code = ticket.details?.error ?? "PushError";
147075
+ this.repo.recordFailure(row.token, code, now);
147076
+ if (code === DEAD_TOKEN_ERROR) {
147077
+ this.repo.revoke(row.token, now);
147078
+ outcome.retired += 1;
147079
+ }
147080
+ log5.warn("expo_push.send_rejected", {
147081
+ event: "expo_push.send_rejected",
147082
+ code,
147083
+ message: ticket.message,
147084
+ retired: code === DEAD_TOKEN_ERROR
146964
147085
  });
146965
- req.end(body);
146966
147086
  });
146967
147087
  }
146968
- /** Close the shared connection. Called on server shutdown. */
146969
- close() {
146970
- this.session?.close();
146971
- this.session = null;
146972
- }
146973
147088
  };
146974
147089
 
146975
147090
  // src/services/push/liveActivityNotifier.ts
@@ -146986,7 +147101,7 @@ function truncateLastOutput(raw2) {
146986
147101
  }
146987
147102
 
146988
147103
  // src/services/push/liveActivityNotifier.ts
146989
- var log5 = getLogger("live-activity");
147104
+ var log6 = getLogger("live-activity");
146990
147105
  function contentStateForSession(args) {
146991
147106
  const status = toLiveActivityStatus(args.session.status);
146992
147107
  if (!status) return null;
@@ -147046,7 +147161,7 @@ var LiveActivityNotifier = class {
147046
147161
  }
147047
147162
  await this.maybeSendName(session);
147048
147163
  } catch (err) {
147049
- log5.error("live_activity.notify_failed", {
147164
+ log6.error("live_activity.notify_failed", {
147050
147165
  event: "live_activity.notify_failed",
147051
147166
  sessionId: session.id,
147052
147167
  status: session.status,
@@ -147068,7 +147183,7 @@ var LiveActivityNotifier = class {
147068
147183
  });
147069
147184
  this.openActivity.set(session.id, { sessionNameSent: session.sessionName != null });
147070
147185
  if (outcome.attempted > 0) {
147071
- log5.info("live_activity.updated", {
147186
+ log6.info("live_activity.updated", {
147072
147187
  event: "live_activity.updated",
147073
147188
  sessionId: session.id,
147074
147189
  status: contentState.status,
@@ -147092,7 +147207,7 @@ var LiveActivityNotifier = class {
147092
147207
  });
147093
147208
  open3.sessionNameSent = true;
147094
147209
  if (outcome.attempted > 0) {
147095
- log5.info("live_activity.updated", {
147210
+ log6.info("live_activity.updated", {
147096
147211
  event: "live_activity.updated",
147097
147212
  sessionId: session.id,
147098
147213
  status: contentState.status,
@@ -147111,7 +147226,7 @@ var LiveActivityNotifier = class {
147111
147226
  if (!contentState) return;
147112
147227
  const outcome = await this.sender.end({ sessionId: session.id, contentState });
147113
147228
  if (outcome.attempted > 0) {
147114
- log5.info("live_activity.ended", {
147229
+ log6.info("live_activity.ended", {
147115
147230
  event: "live_activity.ended",
147116
147231
  sessionId: session.id,
147117
147232
  ...outcome
@@ -147129,7 +147244,7 @@ init_logger();
147129
147244
 
147130
147245
  // src/services/push/liveActivitySender.ts
147131
147246
  init_logger();
147132
- var log6 = getLogger("live-activity");
147247
+ var log7 = getLogger("live-activity");
147133
147248
  var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
147134
147249
  function buildActivityKitPayload(args) {
147135
147250
  return {
@@ -147193,7 +147308,7 @@ var LiveActivitySender = class {
147193
147308
  );
147194
147309
  for (const { row, result, error: error51 } of results) {
147195
147310
  if (error51) {
147196
- log6.error("live_activity.send_failed", {
147311
+ log7.error("live_activity.send_failed", {
147197
147312
  event: "live_activity.send_failed",
147198
147313
  sessionId: args.sessionId,
147199
147314
  activityId: row.activity_id,
@@ -147214,7 +147329,7 @@ var LiveActivitySender = class {
147214
147329
  this.repo.expire(row.token, now);
147215
147330
  outcome.retired += 1;
147216
147331
  }
147217
- log6.warn("live_activity.send_rejected", {
147332
+ log7.warn("live_activity.send_rejected", {
147218
147333
  event: "live_activity.send_rejected",
147219
147334
  sessionId: args.sessionId,
147220
147335
  activityId: row.activity_id,
@@ -147259,7 +147374,7 @@ var LiveActivitySender = class {
147259
147374
  };
147260
147375
 
147261
147376
  // src/services/push/liveActivityRenewal.ts
147262
- var log7 = getLogger("live-activity");
147377
+ var log8 = getLogger("live-activity");
147263
147378
  var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
147264
147379
  var MAX_TIMER_MS = 60 * 60 * 1e3;
147265
147380
  function renewalDueAt(row) {
@@ -147307,7 +147422,7 @@ var LiveActivityRenewalScheduler = class {
147307
147422
  await this.renew(row, now);
147308
147423
  }
147309
147424
  } catch (err) {
147310
- log7.error("live_activity.renewal_sweep_failed", {
147425
+ log8.error("live_activity.renewal_sweep_failed", {
147311
147426
  event: "live_activity.renewal_sweep_failed",
147312
147427
  err: String(err)
147313
147428
  });
@@ -147336,7 +147451,7 @@ var LiveActivityRenewalScheduler = class {
147336
147451
  if (!session || !status) {
147337
147452
  this.deps.repo.claimRenewal(row.token, now);
147338
147453
  this.deps.repo.expire(row.token, now);
147339
- log7.info("live_activity.renewal_skipped", {
147454
+ log8.info("live_activity.renewal_skipped", {
147340
147455
  event: "live_activity.renewal_skipped",
147341
147456
  sessionId: row.session_id,
147342
147457
  activityId: row.activity_id,
@@ -147371,7 +147486,7 @@ var LiveActivityRenewalScheduler = class {
147371
147486
  startedAt,
147372
147487
  now
147373
147488
  });
147374
- log7.info("live_activity.renewed", {
147489
+ log8.info("live_activity.renewed", {
147375
147490
  event: "live_activity.renewed",
147376
147491
  sessionId: session.id,
147377
147492
  activityId: row.activity_id,
@@ -147381,7 +147496,7 @@ var LiveActivityRenewalScheduler = class {
147381
147496
  replacementRequested: started
147382
147497
  });
147383
147498
  } catch (err) {
147384
- log7.error("live_activity.renewal_failed", {
147499
+ log8.error("live_activity.renewal_failed", {
147385
147500
  event: "live_activity.renewal_failed",
147386
147501
  sessionId: session.id,
147387
147502
  activityId: row.activity_id,
@@ -147426,6 +147541,78 @@ var LiveActivityRenewalScheduler = class {
147426
147541
  }
147427
147542
  };
147428
147543
 
147544
+ // src/services/push/waitingInputNotifier.ts
147545
+ init_logger();
147546
+ var log9 = getLogger("expo-push");
147547
+ function waitingInputMessage(session, serverId) {
147548
+ return {
147549
+ title: session.projectName || "Threadbase",
147550
+ body: "Waiting for your input",
147551
+ data: { sessionId: session.id, serverId }
147552
+ };
147553
+ }
147554
+ var WaitingInputNotifier = class {
147555
+ /**
147556
+ * @param isWatched Whether a client is currently subscribed to this session
147557
+ * over WebSocket. Mobile subscribes while the session screen is open and the
147558
+ * socket dies when the app is backgrounded, so this is the available signal
147559
+ * for "the user is already looking" — and a push to someone already reading
147560
+ * the output is pure noise.
147561
+ */
147562
+ constructor(sender, serverId, isWatched) {
147563
+ this.sender = sender;
147564
+ this.serverId = serverId;
147565
+ this.isWatched = isWatched;
147566
+ }
147567
+ sender;
147568
+ serverId;
147569
+ isWatched;
147570
+ /** Sessions with a turn the user started that has not yet been answered. */
147571
+ openTurn = /* @__PURE__ */ new Set();
147572
+ /**
147573
+ * React to a session status change.
147574
+ *
147575
+ * Fire-and-forget by design: a push must never delay or fail a session
147576
+ * transition, so this returns a promise the caller may ignore and every error
147577
+ * is logged rather than propagated.
147578
+ */
147579
+ async onStatusChange(session, previousStatus) {
147580
+ try {
147581
+ if (session.status === "running") {
147582
+ if (previousStatus === "waiting_input") this.openTurn.add(session.id);
147583
+ return;
147584
+ }
147585
+ if (session.status !== "waiting_input") {
147586
+ this.openTurn.delete(session.id);
147587
+ return;
147588
+ }
147589
+ if (!this.openTurn.delete(session.id)) return;
147590
+ if (this.isWatched(session.id)) {
147591
+ log9.debug("expo_push.suppressed_watched", {
147592
+ event: "expo_push.suppressed_watched",
147593
+ sessionId: session.id
147594
+ });
147595
+ return;
147596
+ }
147597
+ const outcome = await this.sender.send(waitingInputMessage(session, this.serverId));
147598
+ if (outcome.attempted > 0) {
147599
+ log9.info("expo_push.waiting_input", {
147600
+ event: "expo_push.waiting_input",
147601
+ sessionId: session.id,
147602
+ ...outcome
147603
+ });
147604
+ }
147605
+ } catch (err) {
147606
+ log9.error("expo_push.notify_failed", {
147607
+ event: "expo_push.notify_failed",
147608
+ sessionId: session.id,
147609
+ status: session.status,
147610
+ err: String(err)
147611
+ });
147612
+ }
147613
+ }
147614
+ };
147615
+
147429
147616
  // src/server.ts
147430
147617
  init_detectPermissionGate();
147431
147618
  init_detectQuestionFromScreen();
@@ -148773,6 +148960,10 @@ var StreamerServer = class {
148773
148960
  apnsClient = null;
148774
148961
  liveActivityNotifier = null;
148775
148962
  liveActivityRenewal = null;
148963
+ // "Your turn" notifications over Expo's relay (#528). Needs no credential of
148964
+ // its own, so unlike the Live Activity path it is on wherever the cache DB
148965
+ // opened — with no registered device it simply sends nothing.
148966
+ waitingInputNotifier = null;
148776
148967
  discoveryCache = null;
148777
148968
  // Single-flight for process discovery. Mobile polls GET /api/sessions and
148778
148969
  // retries on timeout; without this, every concurrent request starts its own
@@ -149072,6 +149263,7 @@ var StreamerServer = class {
149072
149263
  this.wsHub.broadcast({ type: "session_update", session: resp });
149073
149264
  }
149074
149265
  void this.liveActivityNotifier?.onStatusChange(session, previousStatus);
149266
+ void this.waitingInputNotifier?.onStatusChange(session, previousStatus);
149075
149267
  this.sessionStatusBus.emit(`status:${session.id}`, session.status, session);
149076
149268
  }
149077
149269
  });
@@ -149117,6 +149309,7 @@ var StreamerServer = class {
149117
149309
  cache: () => this.cache,
149118
149310
  cacheMonitor: () => this.cacheMonitor,
149119
149311
  pushRepo: () => this.pushRepo,
149312
+ liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
149120
149313
  devicesRepo: () => this.devicesRepo,
149121
149314
  projectsRepo: () => this.projectsRepo,
149122
149315
  conversationsRepo: () => this.conversationsRepo,
@@ -149384,6 +149577,33 @@ var StreamerServer = class {
149384
149577
  topic: `${creds.bundleId}.push-type.liveactivity`
149385
149578
  });
149386
149579
  }
149580
+ /**
149581
+ * Bring up "your turn" notifications over Expo's relay (#528).
149582
+ *
149583
+ * Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM
149584
+ * credentials, so a self-hosted streamer needs no credential of its own. The
149585
+ * access token is optional and only relevant if the Expo project has enhanced
149586
+ * security enabled — requiring one would lock out every self-hoster, since
149587
+ * they do not own the project. It is never logged.
149588
+ */
149589
+ initWaitingInputPush(pushRepo) {
149590
+ const sender = new ExpoPushSender(pushRepo, process.env.THREADBASE_EXPO_ACCESS_TOKEN);
149591
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os13.hostname)();
149592
+ this.waitingInputNotifier = new WaitingInputNotifier(
149593
+ sender,
149594
+ serverId,
149595
+ (id) => this.hasSessionSubscriber(id)
149596
+ );
149597
+ }
149598
+ /** Whether any live socket is subscribed to this session — "someone is looking". */
149599
+ hasSessionSubscriber(sessionId) {
149600
+ const subs = this.sessionSubscribers.get(sessionId);
149601
+ if (!subs) return false;
149602
+ for (const ws2 of subs) {
149603
+ if (ws2.readyState === ws2.OPEN) return true;
149604
+ }
149605
+ return false;
149606
+ }
149387
149607
  /**
149388
149608
  * Classify sessions left behind by previous streamer runs (C1 Phase 3a).
149389
149609
  *
@@ -149986,6 +150206,7 @@ var StreamerServer = class {
149986
150206
  this.pushRepo = new PushRepository(db);
149987
150207
  this.devicesRepo = new DevicesRepository(db);
149988
150208
  this.initLiveActivityPush(this.pushRepo);
150209
+ this.initWaitingInputPush(this.pushRepo);
149989
150210
  this.cacheMonitor = new CacheIntegrityMonitor(
149990
150211
  this.cache,
149991
150212
  this.wsHub,
@@ -156833,7 +157054,7 @@ init_platform2();
156833
157054
  init_logger();
156834
157055
  init_protocol();
156835
157056
  init_socket();
156836
- var log9 = getLogger("prod");
157057
+ var log11 = getLogger("prod");
156837
157058
  function clearSupervisorLogs() {
156838
157059
  let paths;
156839
157060
  try {
@@ -157017,12 +157238,12 @@ function registerProdCommands(program3) {
157017
157238
  prod.command("start").description("Restore prod after a user-held suspension").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
157018
157239
  if (opts.clearLogs === true) clearSupervisorLogs();
157019
157240
  const r = await runProdStart();
157020
- log9.info(r.message, void 0, "console");
157241
+ log11.info(r.message, void 0, "console");
157021
157242
  if (!r.ok) process.exitCode = 1;
157022
157243
  });
157023
157244
  prod.command("stop").description("Unload the launchd agent (prod will not auto-restart)").action(async () => {
157024
157245
  const r = await runProdStop();
157025
- log9.info(r.message, void 0, "console");
157246
+ log11.info(r.message, void 0, "console");
157026
157247
  });
157027
157248
  prod.command("status").description("Report whether prod is supervised, suspended, or down").action(async () => {
157028
157249
  const s3 = await runProdStatus();
@@ -157031,7 +157252,7 @@ function registerProdCommands(program3) {
157031
157252
  `pid: ${s3.agentPid ?? "(none)"}`,
157032
157253
  s3.marker ? `marker: ${s3.marker.userHeld ? "userHeld (intentional stop)" : "dev-suspended"}, devPid=${s3.marker.devPid}, port=${s3.marker.port}, repo=${s3.marker.repoToplevel}` : "marker: none"
157033
157254
  ];
157034
- log9.info(parts.join("\n "), void 0, "console");
157255
+ log11.info(parts.join("\n "), void 0, "console");
157035
157256
  });
157036
157257
  prod.command("restart").description("Stop + restart the supervised streamer (re-reads service definition)").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
157037
157258
  const sup = getSupervisor();
@@ -157040,24 +157261,24 @@ function registerProdCommands(program3) {
157040
157261
  sup.bootoutAgent();
157041
157262
  sup.bootstrapAgent(specPath, { afterBootout: true });
157042
157263
  const what = process.platform === "darwin" ? `agent restarted from ${specPath}` : `task '${TASK_NAME}' restarted`;
157043
- log9.info(what, void 0, "console");
157264
+ log11.info(what, void 0, "console");
157044
157265
  });
157045
157266
  prod.command("doctor").description("Detect stale markers, missing agents, and pty-host health").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
157046
157267
  const r = await runProdDoctor({ fix: opts.fix === true });
157047
157268
  if (r.ptyHost) {
157048
- log9.info(
157269
+ log11.info(
157049
157270
  r.ptyHost.reachable ? `pty-host: reachable, protocol=${r.ptyHost.protocolVersion}, sessions=${r.ptyHost.sessionCount}` : `pty-host: unreachable (${r.ptyHost.error})`,
157050
157271
  void 0,
157051
157272
  "console"
157052
157273
  );
157053
157274
  }
157054
- log9.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
157055
- for (const f2 of r.findings) log9.info(` - ${f2}`, void 0, "console");
157275
+ log11.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
157276
+ for (const f2 of r.findings) log11.info(` - ${f2}`, void 0, "console");
157056
157277
  if (r.repairs.length) {
157057
- log9.info(`repairs:`, void 0, "console");
157058
- for (const fix of r.repairs) log9.info(` - ${fix}`, void 0, "console");
157278
+ log11.info(`repairs:`, void 0, "console");
157279
+ for (const fix of r.repairs) log11.info(` - ${fix}`, void 0, "console");
157059
157280
  } else if (!opts.fix && r.findings.length > 0) {
157060
- log9.info(`(re-run with --fix to apply repairs)`, void 0, "console");
157281
+ log11.info(`(re-run with --fix to apply repairs)`, void 0, "console");
157061
157282
  }
157062
157283
  });
157063
157284
  prod.command("logs").description("Tail the supervised streamer's stdout + stderr log files").option("-n, --lines <count>", "Seed with the last N lines (default 50)", "50").option("--no-follow", "Print last N lines and exit (do not follow)").option("--errors-only", "Tail only stderr", false).option("--clear", "Truncate stdout + stderr logs in place, then exit", false).action(async (opts) => {
@@ -157068,14 +157289,14 @@ function registerProdCommands(program3) {
157068
157289
  errorsOnly: opts.errorsOnly === true,
157069
157290
  clear: opts.clear === true
157070
157291
  });
157071
- if (r.message) log9.info(r.message, void 0, "console");
157292
+ if (r.message) log11.info(r.message, void 0, "console");
157072
157293
  if (!r.ok) process.exitCode = 1;
157073
157294
  });
157074
157295
  program3.addCommand(prod);
157075
157296
  }
157076
157297
 
157077
157298
  // cli/index.ts
157078
- var log12 = getLogger("cli");
157299
+ var log14 = getLogger("cli");
157079
157300
  var program2 = new Command();
157080
157301
  program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
157081
157302
  program2.command("serve").description("Start the streamer server").option("-p, --port <number>", "Port to listen on", "8766").option("--api-key <key>", "API key for authentication").option("--local-no-auth", "Skip auth for localhost requests", false).option("-v, --verbose", "Verbose output", false).option("--log-menubar-requests", "Log /healthz requests from the menubar app", false).option("--browse-root <path>", "Root directory for file browsing").option(
@@ -157117,14 +157338,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157117
157338
  const { checkSqliteAbi: checkSqliteAbi2 } = await Promise.resolve().then(() => (init_check_sqlite_abi(), check_sqlite_abi_exports));
157118
157339
  checkSqliteAbi2();
157119
157340
  } catch (err) {
157120
- log12.error(err instanceof Error ? err.message : String(err), void 0, "console");
157341
+ log14.error(err instanceof Error ? err.message : String(err), void 0, "console");
157121
157342
  process.exit(1);
157122
157343
  }
157123
157344
  if (opts.multiAgentFlow) {
157124
157345
  process.env.MULTI_AGENT_FLOW = "true";
157125
157346
  }
157126
157347
  if (opts.defaultPermissionMode !== void 0 && !isPermissionMode(opts.defaultPermissionMode)) {
157127
- log12.error(
157348
+ log14.error(
157128
157349
  `Invalid --default-permission-mode: ${opts.defaultPermissionMode} (expected one of ${PERMISSION_MODES.join(", ")})`,
157129
157350
  void 0,
157130
157351
  "console"
@@ -157140,7 +157361,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157140
157361
  const value = eq === -1 ? true : entry.slice(eq + 1);
157141
157362
  const def = findFlag(id);
157142
157363
  if (!def) {
157143
- log12.error(
157364
+ log14.error(
157144
157365
  `Invalid --claude-flag id: ${id} (expected one of ${CLAUDE_FLAGS.map((f2) => f2.id).join(", ")})`,
157145
157366
  void 0,
157146
157367
  "console"
@@ -157157,7 +157378,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157157
157378
  claudeFlags = validateFlagValues(raw2);
157158
157379
  for (const id of Object.keys(raw2)) {
157159
157380
  if (!(id in claudeFlags)) {
157160
- log12.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
157381
+ log14.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
157161
157382
  process.exit(1);
157162
157383
  }
157163
157384
  }
@@ -157166,13 +157387,13 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157166
157387
  if (Array.isArray(opts.feature) && opts.feature.length > 0) {
157167
157388
  const parsed = parseFeatureFlagArgs(opts.feature);
157168
157389
  if (parsed.errors.length > 0) {
157169
- for (const error51 of parsed.errors) log12.error(`--feature: ${error51}`, void 0, "console");
157390
+ for (const error51 of parsed.errors) log14.error(`--feature: ${error51}`, void 0, "console");
157170
157391
  process.exit(1);
157171
157392
  }
157172
157393
  featureFlags = parsed.values;
157173
157394
  }
157174
157395
  if (opts.defaultEffort !== void 0 && !isEffortLevel(opts.defaultEffort)) {
157175
- log12.error(
157396
+ log14.error(
157176
157397
  `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${EFFORT_LEVELS.join(", ")})`,
157177
157398
  void 0,
157178
157399
  "console"
@@ -157183,7 +157404,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157183
157404
  if (opts.ptyGracePeriodMs !== void 0) {
157184
157405
  const parsed = Number(opts.ptyGracePeriodMs);
157185
157406
  if (!Number.isInteger(parsed) || parsed < 0) {
157186
- log12.error(
157407
+ log14.error(
157187
157408
  `Invalid --pty-grace-period-ms: ${opts.ptyGracePeriodMs} (expected a non-negative integer; 0 disables auto-hold)`,
157188
157409
  void 0,
157189
157410
  "console"
@@ -157202,14 +157423,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157202
157423
  if (isProdInvocation) {
157203
157424
  const { truncateOversizedLogs: truncateOversizedLogs2 } = await Promise.resolve().then(() => (init_log_cap(), log_cap_exports));
157204
157425
  for (const f2 of truncateOversizedLogs2()) {
157205
- log12.info(`truncated oversized log at boot: ${f2}`, { path: f2, event: "log.truncated" });
157426
+ log14.info(`truncated oversized log at boot: ${f2}`, { path: f2, event: "log.truncated" });
157206
157427
  }
157207
157428
  }
157208
157429
  if (process.platform === "darwin") {
157209
157430
  const { detectConflictingAgents: detectConflictingAgents2, formatConflictMessage: formatConflictMessage2 } = await Promise.resolve().then(() => (init_conflict_check(), conflict_check_exports));
157210
157431
  const conflicts = detectConflictingAgents2();
157211
157432
  if (conflicts.length > 0) {
157212
- log12.warn(formatConflictMessage2(conflicts), void 0, "console");
157433
+ log14.warn(formatConflictMessage2(conflicts), void 0, "console");
157213
157434
  }
157214
157435
  }
157215
157436
  let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
@@ -157224,7 +157445,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157224
157445
  resolvedAutoResumeOnBoot = await interactiveAutoResumePrompt2();
157225
157446
  setAutoResumeOnBoot(resolvedAutoResumeOnBoot);
157226
157447
  } else if (resolvedAutoResumeOnBoot === void 0) {
157227
- log12.info(
157448
+ log14.info(
157228
157449
  "auto_resume_on_boot is not set; interrupted sessions will wait for you to resume them.\nSet `auto_resume_on_boot: true` in ~/.threadbase/server.yaml to resume them automatically.",
157229
157450
  void 0,
157230
157451
  "console"
@@ -157274,15 +157495,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157274
157495
  await server.listen(resolvedPort);
157275
157496
  {
157276
157497
  const v2 = getVersion();
157277
- log12.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
157498
+ log14.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
157278
157499
  }
157279
- log12.info(`Listening on http://localhost:${resolvedPort}`, {
157500
+ log14.info(`Listening on http://localhost:${resolvedPort}`, {
157280
157501
  url: `http://localhost:${resolvedPort}`
157281
157502
  });
157282
- log12.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
157503
+ log14.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
157283
157504
  wsUrl: `ws://localhost:${resolvedPort}/ws`
157284
157505
  });
157285
- log12.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
157506
+ log14.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
157286
157507
  try {
157287
157508
  await printServerBanner({
157288
157509
  port: resolvedPort,
@@ -157292,15 +157513,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157292
157513
  });
157293
157514
  } catch (err) {
157294
157515
  const message = err instanceof Error ? err.message : String(err);
157295
- log12.warn(`(skipped pairing QR: ${message})`, { reason: message });
157296
- log12.info(
157516
+ log14.warn(`(skipped pairing QR: ${message})`, { reason: message });
157517
+ log14.info(
157297
157518
  printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
157298
157519
  void 0,
157299
157520
  "console"
157300
157521
  );
157301
157522
  }
157302
157523
  const shutdown = async () => {
157303
- log12.info("Shutting down...");
157524
+ log14.info("Shutting down...");
157304
157525
  await server.close();
157305
157526
  process.exit(0);
157306
157527
  };
@@ -157308,7 +157529,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157308
157529
  process.on("SIGINT", shutdown);
157309
157530
  process.on("SIGTERM", shutdown);
157310
157531
  process.on("uncaughtException", (err) => {
157311
- log12.error(`uncaught: ${err.message}`, {
157532
+ log14.error(`uncaught: ${err.message}`, {
157312
157533
  error: err.message,
157313
157534
  stack: err.stack,
157314
157535
  event: "process.uncaught"
@@ -157317,7 +157538,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157317
157538
  });
157318
157539
  process.on("unhandledRejection", (reason) => {
157319
157540
  const msg = reason instanceof Error ? reason.message : String(reason);
157320
- log12.error(`unhandled rejection: ${msg}`, {
157541
+ log14.error(`unhandled rejection: ${msg}`, {
157321
157542
  error: msg,
157322
157543
  event: "process.unhandled_rejection"
157323
157544
  });
@@ -157342,10 +157563,10 @@ program2.command("cache").description("Manage the local SQLite conversation cach
157342
157563
  const f2 = dbPath + suffix;
157343
157564
  if (existsSync16(f2)) {
157344
157565
  rmSync6(f2);
157345
- log12.info(`Deleted ${f2}`, { path: f2 }, "console");
157566
+ log14.info(`Deleted ${f2}`, { path: f2 }, "console");
157346
157567
  }
157347
157568
  }
157348
- log12.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
157569
+ log14.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
157349
157570
  })
157350
157571
  );
157351
157572
  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) => {
@@ -157370,7 +157591,7 @@ program2.command("set-key [key]").description("Set the streamer API key in ~/.th
157370
157591
  program2.command("update").description("Check for streamer updates from GitHub Releases and install them").option("--check", "Check only; do not install", false).option("--version <version>", "Pin to a specific release tag").option("--allow-major", "Allow a major-version bump", false).option("--force", "Skip the active-session defer check", false).option("--dry-run", "Print what would be installed without writing to disk", false).option("-p, --port <number>", "Port of the running streamer for active-session check", "8766").action(async (opts) => {
157371
157592
  const cfg = loadUpdateConfig();
157372
157593
  if (!cfg) {
157373
- log12.warn(
157594
+ log14.warn(
157374
157595
  `No update config found at ${DEFAULT_CONFIG_PATH}. Create one with at least 'github_repo: owner/name' to enable updates.`,
157375
157596
  void 0,
157376
157597
  "console"
@@ -157389,11 +157610,11 @@ program2.command("update").description("Check for streamer updates from GitHub R
157389
157610
  appendUpdateLog(
157390
157611
  `[check] current=${result2.current} latest=${result2.latest ?? "none"} status=${result2.reason}`
157391
157612
  );
157392
- log12.info(`Current : ${result2.current}`, void 0, "console");
157393
- log12.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
157394
- log12.info(`Channel : ${cfg.channel}`, void 0, "console");
157395
- log12.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
157396
- log12.info(`Status : ${result2.reason}`, void 0, "console");
157613
+ log14.info(`Current : ${result2.current}`, void 0, "console");
157614
+ log14.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
157615
+ log14.info(`Channel : ${cfg.channel}`, void 0, "console");
157616
+ log14.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
157617
+ log14.info(`Status : ${result2.reason}`, void 0, "console");
157397
157618
  return;
157398
157619
  }
157399
157620
  const port = Number.parseInt(opts.port, 10);
@@ -157409,20 +157630,20 @@ program2.command("update").description("Check for streamer updates from GitHub R
157409
157630
  });
157410
157631
  switch (result.kind) {
157411
157632
  case "no-op":
157412
- log12.info(`Current : ${result.current}`, void 0, "console");
157413
- log12.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
157414
- log12.info(`Status : ${result.reason}`, void 0, "console");
157633
+ log14.info(`Current : ${result.current}`, void 0, "console");
157634
+ log14.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
157635
+ log14.info(`Status : ${result.reason}`, void 0, "console");
157415
157636
  break;
157416
157637
  case "unsupported-install":
157417
- log12.warn(result.reason, void 0, "console");
157638
+ log14.warn(result.reason, void 0, "console");
157418
157639
  process.exitCode = 2;
157419
157640
  break;
157420
157641
  case "deferred":
157421
- log12.warn(`Deferred: ${result.reason}`, void 0, "console");
157642
+ log14.warn(`Deferred: ${result.reason}`, void 0, "console");
157422
157643
  process.exitCode = 2;
157423
157644
  break;
157424
157645
  case "dry-run":
157425
- log12.info(
157646
+ log14.info(
157426
157647
  `Would install ${result.latest} from ${result.tarballUrl}`,
157427
157648
  void 0,
157428
157649
  "console"
@@ -157430,28 +157651,28 @@ program2.command("update").description("Check for streamer updates from GitHub R
157430
157651
  break;
157431
157652
  case "installed":
157432
157653
  if (result.restart.method.startsWith("failed:")) {
157433
- log12.error(
157654
+ log14.error(
157434
157655
  `Installed ${result.installed} on disk, but the running service was not updated. Restart: ${result.restart.method}.`,
157435
157656
  void 0,
157436
157657
  "console"
157437
157658
  );
157438
157659
  process.exitCode = 1;
157439
157660
  } else {
157440
- log12.info(
157661
+ log14.info(
157441
157662
  `Installed ${result.installed} (was ${result.previous}). Restart: ${result.restart.method}.`,
157442
157663
  void 0,
157443
157664
  "console"
157444
157665
  );
157445
157666
  }
157446
157667
  if (result.pruned.length > 0) {
157447
- log12.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
157668
+ log14.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
157448
157669
  }
157449
157670
  break;
157450
157671
  }
157451
157672
  } catch (err) {
157452
157673
  const message = err instanceof Error ? err.message : String(err);
157453
157674
  appendUpdateLog(`[error] ${message}`);
157454
- log12.error(`Update failed: ${message}`, { error: message }, "console");
157675
+ log14.error(`Update failed: ${message}`, { error: message }, "console");
157455
157676
  process.exitCode = 1;
157456
157677
  }
157457
157678
  });
@@ -157481,14 +157702,14 @@ program2.command("pty-host").description(
157481
157702
  onConnection: (transport) => host.accept(transport)
157482
157703
  });
157483
157704
  } catch (err) {
157484
- log12.error(
157705
+ log14.error(
157485
157706
  `pty-host could not listen on ${opts.socket}: ${err instanceof Error ? err.message : String(err)}`,
157486
157707
  void 0,
157487
157708
  "console"
157488
157709
  );
157489
157710
  process.exit(1);
157490
157711
  }
157491
- log12.info(`pty-host listening on ${opts.socket}`, {
157712
+ log14.info(`pty-host listening on ${opts.socket}`, {
157492
157713
  event: "pty_host.listening",
157493
157714
  socket: opts.socket
157494
157715
  });
@@ -157542,7 +157763,7 @@ async function printServerBanner({
157542
157763
  }) {
157543
157764
  const url2 = resolveServerUrl({ publicUrl, port });
157544
157765
  if (!includeQr) {
157545
- log12.info(printUrlBanner({ url: url2 }), void 0, "console");
157766
+ log14.info(printUrlBanner({ url: url2 }), void 0, "console");
157546
157767
  return;
157547
157768
  }
157548
157769
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
@@ -157557,9 +157778,9 @@ async function printServerBanner({
157557
157778
  const expSeconds = Math.floor(expiresAt / 1e3);
157558
157779
  const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
157559
157780
  const qr2 = await generateQr(payload);
157560
- log12.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
157561
- log12.info(`Pair URL: ${payload}`, void 0, "console");
157562
- log12.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
157781
+ log14.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
157782
+ log14.info(`Pair URL: ${payload}`, void 0, "console");
157783
+ log14.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
157563
157784
  }
157564
157785
  /*! Bundled license information:
157565
157786