@threadbase-sh/streamer 1.37.0 → 1.38.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
@@ -4632,11 +4632,11 @@ var require_tools = __commonJS({
4632
4632
  }
4633
4633
  }
4634
4634
  }
4635
- function buildFormatters(level, bindings, log8) {
4635
+ function buildFormatters(level, bindings, log12) {
4636
4636
  return {
4637
4637
  level,
4638
4638
  bindings,
4639
- log: log8
4639
+ log: log12
4640
4640
  };
4641
4641
  }
4642
4642
  function normalizeDestFileDescriptor(destination) {
@@ -5018,11 +5018,11 @@ var require_proto = __commonJS({
5018
5018
  }
5019
5019
  } else instance[serializersSym] = serializers;
5020
5020
  if (options.hasOwnProperty("formatters")) {
5021
- const { level, bindings: chindings, log: log8 } = options.formatters;
5021
+ const { level, bindings: chindings, log: log12 } = options.formatters;
5022
5022
  instance[formattersSym] = buildFormatters(
5023
5023
  level || formatters.level,
5024
5024
  chindings || resetChildingsFormatter,
5025
- log8 || formatters.log
5025
+ log12 || formatters.log
5026
5026
  );
5027
5027
  } else {
5028
5028
  instance[formattersSym] = buildFormatters(
@@ -5932,7 +5932,7 @@ var require_pino = __commonJS({
5932
5932
  } = symbols;
5933
5933
  var { epochTime, nullTime } = time3;
5934
5934
  var { pid } = process;
5935
- var hostname4 = os2.hostname();
5935
+ var hostname5 = os2.hostname();
5936
5936
  var defaultErrorSerializer = stdSerializers.err;
5937
5937
  var defaultOptions2 = {
5938
5938
  level: "info",
@@ -5942,7 +5942,7 @@ var require_pino = __commonJS({
5942
5942
  errorKey: "err",
5943
5943
  nestedKey: null,
5944
5944
  enabled: true,
5945
- base: { pid, hostname: hostname4 },
5945
+ base: { pid, hostname: hostname5 },
5946
5946
  serializers: Object.assign(/* @__PURE__ */ Object.create(null), {
5947
5947
  err: defaultErrorSerializer
5948
5948
  }),
@@ -6133,6 +6133,86 @@ var init_logger = __esm({
6133
6133
  }
6134
6134
  });
6135
6135
 
6136
+ // src/feature-flags.ts
6137
+ function findFeatureFlag(id) {
6138
+ return FEATURE_FLAGS.find((f2) => f2.id === id);
6139
+ }
6140
+ function parseBooleanEnv(raw2) {
6141
+ if (raw2 === void 0) return void 0;
6142
+ const v2 = raw2.trim().toLowerCase();
6143
+ if (v2 === "") return false;
6144
+ return !(v2 === "0" || v2 === "false" || v2 === "no" || v2 === "off");
6145
+ }
6146
+ function validateFeatureFlagValues(raw2) {
6147
+ if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) return {};
6148
+ const out = {};
6149
+ const dropped = [];
6150
+ for (const [id, value] of Object.entries(raw2)) {
6151
+ if (!findFeatureFlag(id) || typeof value !== "boolean") {
6152
+ dropped.push(id);
6153
+ continue;
6154
+ }
6155
+ out[id] = value;
6156
+ }
6157
+ if (dropped.length > 0) {
6158
+ getLogger("feature-flags").warn(
6159
+ `Ignoring unknown or non-boolean feature flags: ${dropped.join(", ")}`,
6160
+ {
6161
+ event: "config.feature_flags_dropped",
6162
+ dropped
6163
+ }
6164
+ );
6165
+ }
6166
+ return out;
6167
+ }
6168
+ function parseFeatureFlagArgs(entries) {
6169
+ const values = {};
6170
+ const errors = [];
6171
+ for (const entry of entries) {
6172
+ const eq = entry.indexOf("=");
6173
+ const id = (eq === -1 ? entry : entry.slice(0, eq)).trim();
6174
+ const rawValue = eq === -1 ? "true" : entry.slice(eq + 1).trim().toLowerCase();
6175
+ if (!findFeatureFlag(id)) {
6176
+ errors.push(
6177
+ `Unknown feature flag "${id}". Known flags: ${FEATURE_FLAGS.map((f2) => f2.id).join(", ")}`
6178
+ );
6179
+ continue;
6180
+ }
6181
+ if (rawValue !== "true" && rawValue !== "false") {
6182
+ errors.push(`Invalid value "${rawValue}" for feature flag "${id}" \u2014 expected true/false`);
6183
+ continue;
6184
+ }
6185
+ values[id] = rawValue === "true";
6186
+ }
6187
+ return { values, errors };
6188
+ }
6189
+ function resolveFeatureFlags(opts) {
6190
+ const env = opts?.env ?? process.env;
6191
+ const out = {};
6192
+ for (const def of FEATURE_FLAGS) {
6193
+ out[def.id] = parseBooleanEnv(env[def.env]) ?? opts?.cli?.[def.id] ?? opts?.yaml?.[def.id] ?? def.default;
6194
+ }
6195
+ return out;
6196
+ }
6197
+ function nonDefaultFeatureFlags(values) {
6198
+ return FEATURE_FLAGS.filter((f2) => values[f2.id] !== f2.default).map((f2) => f2.id);
6199
+ }
6200
+ var FEATURE_FLAGS;
6201
+ var init_feature_flags = __esm({
6202
+ "src/feature-flags.ts"() {
6203
+ "use strict";
6204
+ init_logger();
6205
+ FEATURE_FLAGS = [
6206
+ {
6207
+ id: "codexSystemPrompt",
6208
+ description: "Send the built system prompt to fresh Codex sessions. Off by default: Codex has no --system-prompt flag, so the prompt goes in the positional [PROMPT] argument, which Codex treats as the user's opening turn rather than a system-level instruction.",
6209
+ default: false,
6210
+ env: "THREADBASE_FEATURE_CODEX_SYSTEM_PROMPT"
6211
+ }
6212
+ ];
6213
+ }
6214
+ });
6215
+
6136
6216
  // src/auth.ts
6137
6217
  function configDir() {
6138
6218
  return process.env.THREADBASE_CONFIG_DIR ?? (0, import_path.join)((0, import_os.homedir)(), ".threadbase");
@@ -6297,6 +6377,21 @@ function setClaudeExtraArgs(text) {
6297
6377
  }
6298
6378
  setConfigValue("claude_extra_args", trimmed && trimmed.length > 0 ? trimmed : void 0);
6299
6379
  }
6380
+ function loadFeatureFlags() {
6381
+ try {
6382
+ const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
6383
+ const match2 = content.match(/^feature_flags:\s*(.+)$/m);
6384
+ if (!match2?.[1]) return {};
6385
+ return validateFeatureFlagValues(JSON.parse(match2[1].trim()));
6386
+ } catch (err) {
6387
+ if (err.code !== "ENOENT") {
6388
+ getLogger("auth").warn(`Ignoring unreadable feature_flags in server.yaml: ${String(err)}`, {
6389
+ event: "config.feature_flags_parse_failed"
6390
+ });
6391
+ }
6392
+ return {};
6393
+ }
6394
+ }
6300
6395
  function validatePublicUrl(raw2) {
6301
6396
  let parsed;
6302
6397
  try {
@@ -6354,6 +6449,7 @@ var init_auth = __esm({
6354
6449
  import_os = require("os");
6355
6450
  import_path = require("path");
6356
6451
  init_claude_flags();
6452
+ init_feature_flags();
6357
6453
  init_logger();
6358
6454
  }
6359
6455
  });
@@ -8078,7 +8174,7 @@ var require_merge = __commonJS({
8078
8174
  var require_addPairToJSMap = __commonJS({
8079
8175
  "node_modules/yaml/dist/nodes/addPairToJSMap.js"(exports2) {
8080
8176
  "use strict";
8081
- var log8 = require_log();
8177
+ var log12 = require_log();
8082
8178
  var merge2 = require_merge();
8083
8179
  var stringify = require_stringify();
8084
8180
  var identity = require_identity();
@@ -8127,7 +8223,7 @@ var require_addPairToJSMap = __commonJS({
8127
8223
  let jsonStr = JSON.stringify(strKey);
8128
8224
  if (jsonStr.length > 40)
8129
8225
  jsonStr = jsonStr.substring(0, 36) + '..."';
8130
- log8.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.`);
8226
+ log12.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.`);
8131
8227
  ctx.mapKeyWarned = true;
8132
8228
  }
8133
8229
  return strKey;
@@ -13543,7 +13639,7 @@ var require_public_api = __commonJS({
13543
13639
  var composer = require_composer();
13544
13640
  var Document = require_Document();
13545
13641
  var errors = require_errors();
13546
- var log8 = require_log();
13642
+ var log12 = require_log();
13547
13643
  var identity = require_identity();
13548
13644
  var lineCounter = require_line_counter();
13549
13645
  var parser = require_parser();
@@ -13595,7 +13691,7 @@ var require_public_api = __commonJS({
13595
13691
  const doc = parseDocument(src, options);
13596
13692
  if (!doc)
13597
13693
  return null;
13598
- doc.warnings.forEach((warning) => log8.warn(doc.options.logLevel, warning));
13694
+ doc.warnings.forEach((warning) => log12.warn(doc.options.logLevel, warning));
13599
13695
  if (doc.errors.length > 0) {
13600
13696
  if (doc.options.logLevel !== "silent")
13601
13697
  throw doc.errors[0];
@@ -14354,8 +14450,8 @@ function uint8ArrayToBase64(bytes) {
14354
14450
  }
14355
14451
  return btoa(binaryString);
14356
14452
  }
14357
- function base64urlToUint8Array(base64url3) {
14358
- const base643 = base64url3.replace(/-/g, "+").replace(/_/g, "/");
14453
+ function base64urlToUint8Array(base64url4) {
14454
+ const base643 = base64url4.replace(/-/g, "+").replace(/_/g, "/");
14359
14455
  const padding = "=".repeat((4 - base643.length % 4) % 4);
14360
14456
  return base64ToUint8Array(base643 + padding);
14361
14457
  }
@@ -37111,7 +37207,7 @@ var require_logging = __commonJS({
37111
37207
  _logVerbosity = verbosity;
37112
37208
  };
37113
37209
  exports2.setLoggerVerbosity = setLoggerVerbosity;
37114
- var log8 = (severity, ...args) => {
37210
+ var log12 = (severity, ...args) => {
37115
37211
  let logFunction;
37116
37212
  if (severity >= _logVerbosity) {
37117
37213
  switch (severity) {
@@ -37133,7 +37229,7 @@ var require_logging = __commonJS({
37133
37229
  }
37134
37230
  }
37135
37231
  };
37136
- exports2.log = log8;
37232
+ exports2.log = log12;
37137
37233
  var tracersString = (_d = (_c = process.env.GRPC_NODE_TRACE) !== null && _c !== void 0 ? _c : process.env.GRPC_TRACE) !== null && _d !== void 0 ? _d : "";
37138
37234
  var enabledTracers = /* @__PURE__ */ new Set();
37139
37235
  var disabledTracers = /* @__PURE__ */ new Set();
@@ -38519,8 +38615,8 @@ var require_service_config = __commonJS({
38519
38615
  }
38520
38616
  if (Array.isArray(validatedConfig.clientHostname)) {
38521
38617
  let hostnameMatched = false;
38522
- for (const hostname4 of validatedConfig.clientHostname) {
38523
- if (hostname4 === os2.hostname()) {
38618
+ for (const hostname5 of validatedConfig.clientHostname) {
38619
+ if (hostname5 === os2.hostname()) {
38524
38620
  hostnameMatched = true;
38525
38621
  }
38526
38622
  }
@@ -51089,8 +51185,8 @@ var require_single_subchannel_channel = __commonJS({
51089
51185
  if (splitPath2.length >= 2) {
51090
51186
  serviceName = splitPath2[1];
51091
51187
  }
51092
- const hostname4 = (_b = (_a3 = (0, uri_parser_1.splitHostPort)(this.options.host)) === null || _a3 === void 0 ? void 0 : _a3.host) !== null && _b !== void 0 ? _b : "localhost";
51093
- this.serviceUrl = `https://${hostname4}/${serviceName}`;
51188
+ const hostname5 = (_b = (_a3 = (0, uri_parser_1.splitHostPort)(this.options.host)) === null || _a3 === void 0 ? void 0 : _a3.host) !== null && _b !== void 0 ? _b : "localhost";
51189
+ this.serviceUrl = `https://${hostname5}/${serviceName}`;
51094
51190
  const timeout = (0, deadline_1.getRelativeTimeout)(options.deadline);
51095
51191
  if (timeout !== Infinity) {
51096
51192
  if (timeout <= 0) {
@@ -51737,8 +51833,8 @@ var require_resolver_dns = __commonJS({
51737
51833
  }
51738
51834
  trace("Looking up DNS hostname " + this.dnsHostname);
51739
51835
  this.latestLookupResult = null;
51740
- const hostname4 = this.dnsHostname;
51741
- this.pendingLookupPromise = this.lookup(hostname4);
51836
+ const hostname5 = this.dnsHostname;
51837
+ this.pendingLookupPromise = this.lookup(hostname5);
51742
51838
  this.pendingLookupPromise.then((addressList) => {
51743
51839
  if (this.pendingLookupPromise === null) {
51744
51840
  return;
@@ -51761,7 +51857,7 @@ var require_resolver_dns = __commonJS({
51761
51857
  this.listener((0, call_interface_1.statusOrFromError)(this.defaultResolutionError), {}, this.latestServiceConfigResult, "");
51762
51858
  });
51763
51859
  if (this.isServiceConfigEnabled && this.pendingTxtPromise === null) {
51764
- this.pendingTxtPromise = this.resolveTxt(hostname4);
51860
+ this.pendingTxtPromise = this.resolveTxt(hostname5);
51765
51861
  this.pendingTxtPromise.then((txtRecord) => {
51766
51862
  if (this.pendingTxtPromise === null) {
51767
51863
  return;
@@ -51803,12 +51899,12 @@ var require_resolver_dns = __commonJS({
51803
51899
  this.continueResolving = true;
51804
51900
  }
51805
51901
  }
51806
- async lookup(hostname4) {
51902
+ async lookup(hostname5) {
51807
51903
  if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
51808
51904
  trace("Using alternative DNS resolver.");
51809
51905
  const records = await Promise.allSettled([
51810
- this.alternativeResolver.resolve4(hostname4),
51811
- this.alternativeResolver.resolve6(hostname4)
51906
+ this.alternativeResolver.resolve4(hostname5),
51907
+ this.alternativeResolver.resolve6(hostname5)
51812
51908
  ]);
51813
51909
  if (records.every((result) => result.status === "rejected")) {
51814
51910
  throw new Error(records[0].reason);
@@ -51820,15 +51916,15 @@ var require_resolver_dns = __commonJS({
51820
51916
  port: +this.port
51821
51917
  }));
51822
51918
  }
51823
- const addressList = await dns_1.promises.lookup(hostname4, { all: true });
51919
+ const addressList = await dns_1.promises.lookup(hostname5, { all: true });
51824
51920
  return addressList.map((addr) => ({ host: addr.address, port: +this.port }));
51825
51921
  }
51826
- async resolveTxt(hostname4) {
51922
+ async resolveTxt(hostname5) {
51827
51923
  if (environment_1.GRPC_NODE_USE_ALTERNATIVE_RESOLVER) {
51828
51924
  trace("Using alternative DNS resolver.");
51829
- return this.alternativeResolver.resolveTxt(hostname4);
51925
+ return this.alternativeResolver.resolveTxt(hostname5);
51830
51926
  }
51831
- return dns_1.promises.resolveTxt(hostname4);
51927
+ return dns_1.promises.resolveTxt(hostname5);
51832
51928
  }
51833
51929
  startNextResolutionTimer() {
51834
51930
  var _a3, _b;
@@ -51956,13 +52052,13 @@ var require_http_proxy = __commonJS({
51956
52052
  userCred = proxyUrl.username;
51957
52053
  }
51958
52054
  }
51959
- const hostname4 = proxyUrl.hostname;
52055
+ const hostname5 = proxyUrl.hostname;
51960
52056
  let port = proxyUrl.port;
51961
52057
  if (port === "") {
51962
52058
  port = "80";
51963
52059
  }
51964
52060
  const result = {
51965
- address: `${hostname4}:${port}`
52061
+ address: `${hostname5}:${port}`
51966
52062
  };
51967
52063
  if (userCred) {
51968
52064
  result.creds = userCred;
@@ -53275,8 +53371,8 @@ var require_load_balancing_call = __commonJS({
53275
53371
  if (splitPath2.length >= 2) {
53276
53372
  serviceName = splitPath2[1];
53277
53373
  }
53278
- const hostname4 = (_b = (_a3 = (0, uri_parser_1.splitHostPort)(this.host)) === null || _a3 === void 0 ? void 0 : _a3.host) !== null && _b !== void 0 ? _b : "localhost";
53279
- this.serviceUrl = `https://${hostname4}/${serviceName}`;
53374
+ const hostname5 = (_b = (_a3 = (0, uri_parser_1.splitHostPort)(this.host)) === null || _a3 === void 0 ? void 0 : _a3.host) !== null && _b !== void 0 ? _b : "localhost";
53375
+ this.serviceUrl = `https://${hostname5}/${serviceName}`;
53280
53376
  this.startTime = /* @__PURE__ */ new Date();
53281
53377
  }
53282
53378
  getDeadlineInfo() {
@@ -91085,9 +91181,9 @@ var require_parse_host_uri = __commonJS({
91085
91181
  var ipv4Hostname = "(?:\\d{1,3}(?:\\.\\d{1,3}){3})";
91086
91182
  var ipv6Hostname = "(?:\\[(?<ipv6>[0-9a-fA-F.:]+)\\])";
91087
91183
  var dnsHostname = "(?:[^:/]+)";
91088
- var hostname4 = `(?:${ipv4Hostname}|${ipv6Hostname}|${dnsHostname})`;
91184
+ var hostname5 = `(?:${ipv4Hostname}|${ipv6Hostname}|${dnsHostname})`;
91089
91185
  var port = "(?::(?<port>\\d+))";
91090
- var protoHostPortRegex = new RegExp(`^${scheme}??(?<hostname>${hostname4})${port}?$`);
91186
+ var protoHostPortRegex = new RegExp(`^${scheme}??(?<hostname>${hostname5})${port}?$`);
91091
91187
  function splitProtoHostPort(uri) {
91092
91188
  const match2 = protoHostPortRegex.exec(uri);
91093
91189
  if (!match2?.groups)
@@ -91099,9 +91195,9 @@ var require_parse_host_uri = __commonJS({
91099
91195
  };
91100
91196
  }
91101
91197
  function joinProtoHostPort(components) {
91102
- const { scheme: scheme2, hostname: hostname5, port: port2 } = components;
91198
+ const { scheme: scheme2, hostname: hostname6, port: port2 } = components;
91103
91199
  const schemeText = scheme2 ? `${scheme2}:` : "";
91104
- const hostnameText = hostname5.includes(":") ? `[${hostname5}]` : hostname5;
91200
+ const hostnameText = hostname6.includes(":") ? `[${hostname6}]` : hostname6;
91105
91201
  const portText = port2 !== void 0 ? `:${port2}` : "";
91106
91202
  return `${schemeText}${hostnameText}${portText}`;
91107
91203
  }
@@ -98687,7 +98783,7 @@ var require_scan = __commonJS({
98687
98783
  var require_parse4 = __commonJS({
98688
98784
  "node_modules/picomatch/lib/parse.js"(exports2, module2) {
98689
98785
  "use strict";
98690
- var constants = require_constants5();
98786
+ var constants2 = require_constants5();
98691
98787
  var utils = require_utils2();
98692
98788
  var {
98693
98789
  MAX_LENGTH,
@@ -98695,7 +98791,7 @@ var require_parse4 = __commonJS({
98695
98791
  REGEX_NON_SPECIAL_CHARS,
98696
98792
  REGEX_SPECIAL_CHARS_BACKREF,
98697
98793
  REPLACEMENTS
98698
- } = constants;
98794
+ } = constants2;
98699
98795
  var expandRange = (args, options) => {
98700
98796
  if (typeof options.expandRange === "function") {
98701
98797
  return options.expandRange(...args, options);
@@ -98901,7 +98997,7 @@ var require_parse4 = __commonJS({
98901
98997
  if (options.maxExtglobRecursion === false) {
98902
98998
  return { risky: false };
98903
98999
  }
98904
- const max = typeof options.maxExtglobRecursion === "number" ? options.maxExtglobRecursion : constants.DEFAULT_MAX_EXTGLOB_RECURSION;
99000
+ const max = typeof options.maxExtglobRecursion === "number" ? options.maxExtglobRecursion : constants2.DEFAULT_MAX_EXTGLOB_RECURSION;
98905
99001
  const branches = splitTopLevel(body).map((branch) => branch.trim());
98906
99002
  if (branches.length > 1) {
98907
99003
  if (branches.some((branch) => branch === "") || branches.some((branch) => /^[*?]+$/.test(branch)) || hasRepeatedCharPrefixOverlap(branches)) {
@@ -98934,8 +99030,8 @@ var require_parse4 = __commonJS({
98934
99030
  const tokens = [bos];
98935
99031
  const capture = opts.capture ? "" : "?:";
98936
99032
  const win32 = utils.isWindows(options);
98937
- const PLATFORM_CHARS = constants.globChars(win32);
98938
- const EXTGLOB_CHARS = constants.extglobChars(PLATFORM_CHARS);
99033
+ const PLATFORM_CHARS = constants2.globChars(win32);
99034
+ const EXTGLOB_CHARS = constants2.extglobChars(PLATFORM_CHARS);
98939
99035
  const {
98940
99036
  DOT_LITERAL,
98941
99037
  PLUS_LITERAL,
@@ -99634,7 +99730,7 @@ var require_parse4 = __commonJS({
99634
99730
  NO_DOTS_SLASH,
99635
99731
  STAR,
99636
99732
  START_ANCHOR
99637
- } = constants.globChars(win32);
99733
+ } = constants2.globChars(win32);
99638
99734
  const nodot = opts.dot ? NO_DOTS : NO_DOT;
99639
99735
  const slashDot = opts.dot ? NO_DOTS_SLASH : NO_DOT;
99640
99736
  const capture = opts.capture ? "" : "?:";
@@ -99693,7 +99789,7 @@ var require_picomatch = __commonJS({
99693
99789
  var scan = require_scan();
99694
99790
  var parse3 = require_parse4();
99695
99791
  var utils = require_utils2();
99696
- var constants = require_constants5();
99792
+ var constants2 = require_constants5();
99697
99793
  var isObject2 = (val) => val && typeof val === "object" && !Array.isArray(val);
99698
99794
  var picomatch = (glob, options, returnState = false) => {
99699
99795
  if (Array.isArray(glob)) {
@@ -99821,7 +99917,7 @@ var require_picomatch = __commonJS({
99821
99917
  return /$^/;
99822
99918
  }
99823
99919
  };
99824
- picomatch.constants = constants;
99920
+ picomatch.constants = constants2;
99825
99921
  module2.exports = picomatch;
99826
99922
  }
99827
99923
  });
@@ -104039,11 +104135,11 @@ var require_pg_connection_string = __commonJS({
104039
104135
  config2.client_encoding = result.searchParams.get("encoding");
104040
104136
  return config2;
104041
104137
  }
104042
- const hostname4 = dummyHost ? "" : result.hostname;
104138
+ const hostname5 = dummyHost ? "" : result.hostname;
104043
104139
  if (!config2.host) {
104044
- config2.host = decodeURIComponent(hostname4);
104045
- } else if (hostname4 && /^%2f/i.test(hostname4)) {
104046
- result.pathname = hostname4 + result.pathname;
104140
+ config2.host = decodeURIComponent(hostname5);
104141
+ } else if (hostname5 && /^%2f/i.test(hostname5)) {
104142
+ result.pathname = hostname5 + result.pathname;
104047
104143
  }
104048
104144
  if (!config2.port) {
104049
104145
  config2.port = result.port;
@@ -119963,7 +120059,7 @@ var require_crc = __commonJS({
119963
120059
  var require_parser3 = __commonJS({
119964
120060
  "node_modules/pngjs/lib/parser.js"(exports2, module2) {
119965
120061
  "use strict";
119966
- var constants = require_constants7();
120062
+ var constants2 = require_constants7();
119967
120063
  var CrcCalculator = require_crc();
119968
120064
  var Parser = module2.exports = function(options, dependencies) {
119969
120065
  this._options = options;
@@ -119974,12 +120070,12 @@ var require_parser3 = __commonJS({
119974
120070
  this._palette = [];
119975
120071
  this._colorType = 0;
119976
120072
  this._chunks = {};
119977
- this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
119978
- this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
119979
- this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
119980
- this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
119981
- this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
119982
- this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);
120073
+ this._chunks[constants2.TYPE_IHDR] = this._handleIHDR.bind(this);
120074
+ this._chunks[constants2.TYPE_IEND] = this._handleIEND.bind(this);
120075
+ this._chunks[constants2.TYPE_IDAT] = this._handleIDAT.bind(this);
120076
+ this._chunks[constants2.TYPE_PLTE] = this._handlePLTE.bind(this);
120077
+ this._chunks[constants2.TYPE_tRNS] = this._handleTRNS.bind(this);
120078
+ this._chunks[constants2.TYPE_gAMA] = this._handleGAMA.bind(this);
119983
120079
  this.read = dependencies.read;
119984
120080
  this.error = dependencies.error;
119985
120081
  this.metadata = dependencies.metadata;
@@ -119994,10 +120090,10 @@ var require_parser3 = __commonJS({
119994
120090
  };
119995
120091
  };
119996
120092
  Parser.prototype.start = function() {
119997
- this.read(constants.PNG_SIGNATURE.length, this._parseSignature.bind(this));
120093
+ this.read(constants2.PNG_SIGNATURE.length, this._parseSignature.bind(this));
119998
120094
  };
119999
120095
  Parser.prototype._parseSignature = function(data) {
120000
- let signature = constants.PNG_SIGNATURE;
120096
+ let signature = constants2.PNG_SIGNATURE;
120001
120097
  for (let i = 0; i < signature.length; i++) {
120002
120098
  if (data[i] !== signature[i]) {
120003
120099
  this.error(new Error("Invalid file signature"));
@@ -120014,7 +120110,7 @@ var require_parser3 = __commonJS({
120014
120110
  name += String.fromCharCode(data[i]);
120015
120111
  }
120016
120112
  let ancillary = Boolean(data[4] & 32);
120017
- if (!this._hasIHDR && type !== constants.TYPE_IHDR) {
120113
+ if (!this._hasIHDR && type !== constants2.TYPE_IHDR) {
120018
120114
  this.error(new Error("Expected IHDR on beggining"));
120019
120115
  return;
120020
120116
  }
@@ -120062,7 +120158,7 @@ var require_parser3 = __commonJS({
120062
120158
  this.error(new Error("Unsupported bit depth " + depth));
120063
120159
  return;
120064
120160
  }
120065
- if (!(colorType in constants.COLORTYPE_TO_BPP_MAP)) {
120161
+ if (!(colorType in constants2.COLORTYPE_TO_BPP_MAP)) {
120066
120162
  this.error(new Error("Unsupported color type"));
120067
120163
  return;
120068
120164
  }
@@ -120079,16 +120175,16 @@ var require_parser3 = __commonJS({
120079
120175
  return;
120080
120176
  }
120081
120177
  this._colorType = colorType;
120082
- let bpp = constants.COLORTYPE_TO_BPP_MAP[this._colorType];
120178
+ let bpp = constants2.COLORTYPE_TO_BPP_MAP[this._colorType];
120083
120179
  this._hasIHDR = true;
120084
120180
  this.metadata({
120085
120181
  width,
120086
120182
  height,
120087
120183
  depth,
120088
120184
  interlace: Boolean(interlace),
120089
- palette: Boolean(colorType & constants.COLORTYPE_PALETTE),
120090
- color: Boolean(colorType & constants.COLORTYPE_COLOR),
120091
- alpha: Boolean(colorType & constants.COLORTYPE_ALPHA),
120185
+ palette: Boolean(colorType & constants2.COLORTYPE_PALETTE),
120186
+ color: Boolean(colorType & constants2.COLORTYPE_COLOR),
120187
+ alpha: Boolean(colorType & constants2.COLORTYPE_ALPHA),
120092
120188
  bpp,
120093
120189
  colorType
120094
120190
  });
@@ -120112,7 +120208,7 @@ var require_parser3 = __commonJS({
120112
120208
  };
120113
120209
  Parser.prototype._parseTRNS = function(data) {
120114
120210
  this._crc.write(data);
120115
- if (this._colorType === constants.COLORTYPE_PALETTE_COLOR) {
120211
+ if (this._colorType === constants2.COLORTYPE_PALETTE_COLOR) {
120116
120212
  if (this._palette.length === 0) {
120117
120213
  this.error(new Error("Transparency chunk must be after palette"));
120118
120214
  return;
@@ -120126,10 +120222,10 @@ var require_parser3 = __commonJS({
120126
120222
  }
120127
120223
  this.palette(this._palette);
120128
120224
  }
120129
- if (this._colorType === constants.COLORTYPE_GRAYSCALE) {
120225
+ if (this._colorType === constants2.COLORTYPE_GRAYSCALE) {
120130
120226
  this.transColor([data.readUInt16BE(0)]);
120131
120227
  }
120132
- if (this._colorType === constants.COLORTYPE_COLOR) {
120228
+ if (this._colorType === constants2.COLORTYPE_COLOR) {
120133
120229
  this.transColor([
120134
120230
  data.readUInt16BE(0),
120135
120231
  data.readUInt16BE(2),
@@ -120143,7 +120239,7 @@ var require_parser3 = __commonJS({
120143
120239
  };
120144
120240
  Parser.prototype._parseGAMA = function(data) {
120145
120241
  this._crc.write(data);
120146
- this.gamma(data.readUInt32BE(0) / constants.GAMMA_DIVISION);
120242
+ this.gamma(data.readUInt32BE(0) / constants2.GAMMA_DIVISION);
120147
120243
  this._handleChunkEnd();
120148
120244
  };
120149
120245
  Parser.prototype._handleIDAT = function(length) {
@@ -120155,7 +120251,7 @@ var require_parser3 = __commonJS({
120155
120251
  };
120156
120252
  Parser.prototype._parseIDAT = function(length, data) {
120157
120253
  this._crc.write(data);
120158
- if (this._colorType === constants.COLORTYPE_PALETTE_COLOR && this._palette.length === 0) {
120254
+ if (this._colorType === constants2.COLORTYPE_PALETTE_COLOR && this._palette.length === 0) {
120159
120255
  throw new Error("Expected palette not found");
120160
120256
  }
120161
120257
  this.inflateData(data);
@@ -120643,9 +120739,9 @@ var require_parser_async = __commonJS({
120643
120739
  var require_bitpacker = __commonJS({
120644
120740
  "node_modules/pngjs/lib/bitpacker.js"(exports2, module2) {
120645
120741
  "use strict";
120646
- var constants = require_constants7();
120742
+ var constants2 = require_constants7();
120647
120743
  module2.exports = function(dataIn, width, height, options) {
120648
- let outHasAlpha = [constants.COLORTYPE_COLOR_ALPHA, constants.COLORTYPE_ALPHA].indexOf(
120744
+ let outHasAlpha = [constants2.COLORTYPE_COLOR_ALPHA, constants2.COLORTYPE_ALPHA].indexOf(
120649
120745
  options.colorType
120650
120746
  ) !== -1;
120651
120747
  if (options.colorType === options.inputColorType) {
@@ -120665,11 +120761,11 @@ var require_bitpacker = __commonJS({
120665
120761
  }
120666
120762
  let data = options.bitDepth !== 16 ? dataIn : new Uint16Array(dataIn.buffer);
120667
120763
  let maxValue = 255;
120668
- let inBpp = constants.COLORTYPE_TO_BPP_MAP[options.inputColorType];
120764
+ let inBpp = constants2.COLORTYPE_TO_BPP_MAP[options.inputColorType];
120669
120765
  if (inBpp === 4 && !options.inputHasAlpha) {
120670
120766
  inBpp = 3;
120671
120767
  }
120672
- let outBpp = constants.COLORTYPE_TO_BPP_MAP[options.colorType];
120768
+ let outBpp = constants2.COLORTYPE_TO_BPP_MAP[options.colorType];
120673
120769
  if (options.bitDepth === 16) {
120674
120770
  maxValue = 65535;
120675
120771
  outBpp *= 2;
@@ -120693,24 +120789,24 @@ var require_bitpacker = __commonJS({
120693
120789
  let blue;
120694
120790
  let alpha = maxValue;
120695
120791
  switch (options.inputColorType) {
120696
- case constants.COLORTYPE_COLOR_ALPHA:
120792
+ case constants2.COLORTYPE_COLOR_ALPHA:
120697
120793
  alpha = data[inIndex + 3];
120698
120794
  red = data[inIndex];
120699
120795
  green = data[inIndex + 1];
120700
120796
  blue = data[inIndex + 2];
120701
120797
  break;
120702
- case constants.COLORTYPE_COLOR:
120798
+ case constants2.COLORTYPE_COLOR:
120703
120799
  red = data[inIndex];
120704
120800
  green = data[inIndex + 1];
120705
120801
  blue = data[inIndex + 2];
120706
120802
  break;
120707
- case constants.COLORTYPE_ALPHA:
120803
+ case constants2.COLORTYPE_ALPHA:
120708
120804
  alpha = data[inIndex + 1];
120709
120805
  red = data[inIndex];
120710
120806
  green = red;
120711
120807
  blue = red;
120712
120808
  break;
120713
- case constants.COLORTYPE_GRAYSCALE:
120809
+ case constants2.COLORTYPE_GRAYSCALE:
120714
120810
  red = data[inIndex];
120715
120811
  green = red;
120716
120812
  blue = red;
@@ -120743,8 +120839,8 @@ var require_bitpacker = __commonJS({
120743
120839
  for (let x = 0; x < width; x++) {
120744
120840
  let rgba = getRGBA(data, inIndex);
120745
120841
  switch (options.colorType) {
120746
- case constants.COLORTYPE_COLOR_ALPHA:
120747
- case constants.COLORTYPE_COLOR:
120842
+ case constants2.COLORTYPE_COLOR_ALPHA:
120843
+ case constants2.COLORTYPE_COLOR:
120748
120844
  if (options.bitDepth === 8) {
120749
120845
  outData[outIndex] = rgba.red;
120750
120846
  outData[outIndex + 1] = rgba.green;
@@ -120761,8 +120857,8 @@ var require_bitpacker = __commonJS({
120761
120857
  }
120762
120858
  }
120763
120859
  break;
120764
- case constants.COLORTYPE_ALPHA:
120765
- case constants.COLORTYPE_GRAYSCALE: {
120860
+ case constants2.COLORTYPE_ALPHA:
120861
+ case constants2.COLORTYPE_GRAYSCALE: {
120766
120862
  let grayscale = (rgba.red + rgba.green + rgba.blue) / 3;
120767
120863
  if (options.bitDepth === 8) {
120768
120864
  outData[outIndex] = grayscale;
@@ -120935,7 +121031,7 @@ var require_filter_pack = __commonJS({
120935
121031
  var require_packer = __commonJS({
120936
121032
  "node_modules/pngjs/lib/packer.js"(exports2, module2) {
120937
121033
  "use strict";
120938
- var constants = require_constants7();
121034
+ var constants2 = require_constants7();
120939
121035
  var CrcStream = require_crc();
120940
121036
  var bitPacker = require_bitpacker();
120941
121037
  var filter = require_filter_pack();
@@ -120948,23 +121044,23 @@ var require_packer = __commonJS({
120948
121044
  options.inputHasAlpha = options.inputHasAlpha != null ? options.inputHasAlpha : true;
120949
121045
  options.deflateFactory = options.deflateFactory || zlib.createDeflate;
120950
121046
  options.bitDepth = options.bitDepth || 8;
120951
- options.colorType = typeof options.colorType === "number" ? options.colorType : constants.COLORTYPE_COLOR_ALPHA;
120952
- options.inputColorType = typeof options.inputColorType === "number" ? options.inputColorType : constants.COLORTYPE_COLOR_ALPHA;
121047
+ options.colorType = typeof options.colorType === "number" ? options.colorType : constants2.COLORTYPE_COLOR_ALPHA;
121048
+ options.inputColorType = typeof options.inputColorType === "number" ? options.inputColorType : constants2.COLORTYPE_COLOR_ALPHA;
120953
121049
  if ([
120954
- constants.COLORTYPE_GRAYSCALE,
120955
- constants.COLORTYPE_COLOR,
120956
- constants.COLORTYPE_COLOR_ALPHA,
120957
- constants.COLORTYPE_ALPHA
121050
+ constants2.COLORTYPE_GRAYSCALE,
121051
+ constants2.COLORTYPE_COLOR,
121052
+ constants2.COLORTYPE_COLOR_ALPHA,
121053
+ constants2.COLORTYPE_ALPHA
120958
121054
  ].indexOf(options.colorType) === -1) {
120959
121055
  throw new Error(
120960
121056
  "option color type:" + options.colorType + " is not supported at present"
120961
121057
  );
120962
121058
  }
120963
121059
  if ([
120964
- constants.COLORTYPE_GRAYSCALE,
120965
- constants.COLORTYPE_COLOR,
120966
- constants.COLORTYPE_COLOR_ALPHA,
120967
- constants.COLORTYPE_ALPHA
121060
+ constants2.COLORTYPE_GRAYSCALE,
121061
+ constants2.COLORTYPE_COLOR,
121062
+ constants2.COLORTYPE_COLOR_ALPHA,
121063
+ constants2.COLORTYPE_ALPHA
120968
121064
  ].indexOf(options.inputColorType) === -1) {
120969
121065
  throw new Error(
120970
121066
  "option input color type:" + options.inputColorType + " is not supported at present"
@@ -120988,7 +121084,7 @@ var require_packer = __commonJS({
120988
121084
  };
120989
121085
  Packer.prototype.filterData = function(data, width, height) {
120990
121086
  let packedData = bitPacker(data, width, height, this._options);
120991
- let bpp = constants.COLORTYPE_TO_BPP_MAP[this._options.colorType];
121087
+ let bpp = constants2.COLORTYPE_TO_BPP_MAP[this._options.colorType];
120992
121088
  let filteredData = filter(packedData, width, height, this._options, bpp);
120993
121089
  return filteredData;
120994
121090
  };
@@ -121008,8 +121104,8 @@ var require_packer = __commonJS({
121008
121104
  };
121009
121105
  Packer.prototype.packGAMA = function(gamma) {
121010
121106
  let buf = Buffer.alloc(4);
121011
- buf.writeUInt32BE(Math.floor(gamma * constants.GAMMA_DIVISION), 0);
121012
- return this._packChunk(constants.TYPE_gAMA, buf);
121107
+ buf.writeUInt32BE(Math.floor(gamma * constants2.GAMMA_DIVISION), 0);
121108
+ return this._packChunk(constants2.TYPE_gAMA, buf);
121013
121109
  };
121014
121110
  Packer.prototype.packIHDR = function(width, height) {
121015
121111
  let buf = Buffer.alloc(13);
@@ -121020,13 +121116,13 @@ var require_packer = __commonJS({
121020
121116
  buf[10] = 0;
121021
121117
  buf[11] = 0;
121022
121118
  buf[12] = 0;
121023
- return this._packChunk(constants.TYPE_IHDR, buf);
121119
+ return this._packChunk(constants2.TYPE_IHDR, buf);
121024
121120
  };
121025
121121
  Packer.prototype.packIDAT = function(data) {
121026
- return this._packChunk(constants.TYPE_IDAT, data);
121122
+ return this._packChunk(constants2.TYPE_IDAT, data);
121027
121123
  };
121028
121124
  Packer.prototype.packIEND = function() {
121029
- return this._packChunk(constants.TYPE_IEND, null);
121125
+ return this._packChunk(constants2.TYPE_IEND, null);
121030
121126
  };
121031
121127
  }
121032
121128
  });
@@ -121037,7 +121133,7 @@ var require_packer_async = __commonJS({
121037
121133
  "use strict";
121038
121134
  var util = require("util");
121039
121135
  var Stream = require("stream");
121040
- var constants = require_constants7();
121136
+ var constants2 = require_constants7();
121041
121137
  var Packer = require_packer();
121042
121138
  var PackerAsync = module2.exports = function(opt) {
121043
121139
  Stream.call(this);
@@ -121048,7 +121144,7 @@ var require_packer_async = __commonJS({
121048
121144
  };
121049
121145
  util.inherits(PackerAsync, Stream);
121050
121146
  PackerAsync.prototype.pack = function(data, width, height, gamma) {
121051
- this.emit("data", Buffer.from(constants.PNG_SIGNATURE));
121147
+ this.emit("data", Buffer.from(constants2.PNG_SIGNATURE));
121052
121148
  this.emit("data", this._packer.packIHDR(width, height));
121053
121149
  if (gamma) {
121054
121150
  this.emit("data", this._packer.packGAMA(gamma));
@@ -121376,7 +121472,7 @@ var require_packer_sync = __commonJS({
121376
121472
  if (!zlib.deflateSync) {
121377
121473
  hasSyncZlib = false;
121378
121474
  }
121379
- var constants = require_constants7();
121475
+ var constants2 = require_constants7();
121380
121476
  var Packer = require_packer();
121381
121477
  module2.exports = function(metaData, opt) {
121382
121478
  if (!hasSyncZlib) {
@@ -121387,7 +121483,7 @@ var require_packer_sync = __commonJS({
121387
121483
  let options = opt || {};
121388
121484
  let packer = new Packer(options);
121389
121485
  let chunks = [];
121390
- chunks.push(Buffer.from(constants.PNG_SIGNATURE));
121486
+ chunks.push(Buffer.from(constants2.PNG_SIGNATURE));
121391
121487
  chunks.push(packer.packIHDR(metaData.width, metaData.height));
121392
121488
  if (metaData.gamma) {
121393
121489
  chunks.push(packer.packGAMA(metaData.gamma));
@@ -122490,10 +122586,10 @@ var require_truncate = __commonJS({
122490
122586
  "node_modules/semver/functions/truncate.js"(exports2, module2) {
122491
122587
  "use strict";
122492
122588
  var parse3 = require_parse5();
122493
- var constants = require_constants8();
122589
+ var constants2 = require_constants8();
122494
122590
  var SemVer = require_semver();
122495
122591
  var truncate = (version2, truncation, options) => {
122496
- if (!constants.RELEASE_TYPES.includes(truncation)) {
122592
+ if (!constants2.RELEASE_TYPES.includes(truncation)) {
122497
122593
  return null;
122498
122594
  }
122499
122595
  const clonedVersion = cloneInputVersion(version2, options);
@@ -123542,7 +123638,7 @@ var require_semver2 = __commonJS({
123542
123638
  "node_modules/semver/index.js"(exports2, module2) {
123543
123639
  "use strict";
123544
123640
  var internalRe = require_re();
123545
- var constants = require_constants8();
123641
+ var constants2 = require_constants8();
123546
123642
  var SemVer = require_semver();
123547
123643
  var identifiers = require_identifiers();
123548
123644
  var parse3 = require_parse5();
@@ -123626,8 +123722,8 @@ var require_semver2 = __commonJS({
123626
123722
  re: internalRe.re,
123627
123723
  src: internalRe.src,
123628
123724
  tokens: internalRe.t,
123629
- SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION,
123630
- RELEASE_TYPES: constants.RELEASE_TYPES,
123725
+ SEMVER_SPEC_VERSION: constants2.SEMVER_SPEC_VERSION,
123726
+ RELEASE_TYPES: constants2.RELEASE_TYPES,
123631
123727
  compareIdentifiers: identifiers.compareIdentifiers,
123632
123728
  rcompareIdentifiers: identifiers.rcompareIdentifiers
123633
123729
  };
@@ -123850,7 +123946,7 @@ function readMarker() {
123850
123946
  const parsed = JSON.parse(raw2);
123851
123947
  return MarkerSchema.parse(parsed);
123852
123948
  } catch (err) {
123853
- log3.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
123949
+ log7.warn(`marker at ${markerPath()} is malformed; treating as absent`, {
123854
123950
  error: err instanceof Error ? err.message : String(err)
123855
123951
  });
123856
123952
  return null;
@@ -123867,7 +123963,7 @@ function writeMarker(marker) {
123867
123963
  function clearMarker() {
123868
123964
  if ((0, import_node_fs21.existsSync)(markerPath())) (0, import_node_fs21.rmSync)(markerPath());
123869
123965
  }
123870
- var import_node_fs21, import_node_path25, log3;
123966
+ var import_node_fs21, import_node_path25, log7;
123871
123967
  var init_marker = __esm({
123872
123968
  "src/lifecycle/marker.ts"() {
123873
123969
  "use strict";
@@ -123876,7 +123972,7 @@ var init_marker = __esm({
123876
123972
  init_logger();
123877
123973
  init_constants();
123878
123974
  init_marker_schema();
123879
- log3 = getLogger("lifecycle.marker");
123975
+ log7 = getLogger("lifecycle.marker");
123880
123976
  }
123881
123977
  });
123882
123978
 
@@ -124077,7 +124173,7 @@ function readPrefs() {
124077
124173
  try {
124078
124174
  return PrefsSchema.parse(JSON.parse((0, import_node_fs23.readFileSync)(path2, "utf8")));
124079
124175
  } catch (err) {
124080
- log5.warn(`prefs at ${path2} are malformed; treating as empty`, {
124176
+ log9.warn(`prefs at ${path2} are malformed; treating as empty`, {
124081
124177
  error: err instanceof Error ? err.message : String(err)
124082
124178
  });
124083
124179
  return { repos: {} };
@@ -124112,7 +124208,7 @@ function forgetAll() {
124112
124208
  const path2 = prefsPath();
124113
124209
  if ((0, import_node_fs23.existsSync)(path2)) (0, import_node_fs23.rmSync)(path2);
124114
124210
  }
124115
- var import_node_fs23, import_node_path26, log5, PrefsSchema;
124211
+ var import_node_fs23, import_node_path26, log9, PrefsSchema;
124116
124212
  var init_prefs = __esm({
124117
124213
  "src/lifecycle/prefs.ts"() {
124118
124214
  "use strict";
@@ -124121,7 +124217,7 @@ var init_prefs = __esm({
124121
124217
  init_zod();
124122
124218
  init_logger();
124123
124219
  init_constants();
124124
- log5 = getLogger("lifecycle.prefs");
124220
+ log9 = getLogger("lifecycle.prefs");
124125
124221
  PrefsSchema = external_exports.object({
124126
124222
  repos: external_exports.record(
124127
124223
  external_exports.string(),
@@ -124224,7 +124320,7 @@ function takeoverProd(opts) {
124224
124320
  `prod is already suspended by dev pid ${existing.devPid} (since ${existing.suspendedAt}). Stop that dev session first, or run 'tb-streamer prod doctor'.`
124225
124321
  );
124226
124322
  }
124227
- log6.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
124323
+ log10.info(`stale marker found (pid ${existing.devPid} is gone) \u2014 clearing and proceeding`);
124228
124324
  }
124229
124325
  getSupervisor().bootoutAgent();
124230
124326
  writeMarker({
@@ -124239,7 +124335,7 @@ function takeoverProd(opts) {
124239
124335
  const m2 = readMarker();
124240
124336
  if (m2 && m2.devPid === process.pid) {
124241
124337
  writeMarker({ ...m2, userHeld: true });
124242
- log6.info(
124338
+ log10.info(
124243
124339
  `prod is suspended (userHeld). Run 'tb-streamer prod start' to restore the supervised instance.`
124244
124340
  );
124245
124341
  }
@@ -124258,14 +124354,14 @@ function takeoverProd(opts) {
124258
124354
  });
124259
124355
  process.on("uncaughtException", (err) => {
124260
124356
  flipUserHeld();
124261
- log6.error(`uncaught: ${err.message}`);
124357
+ log10.error(`uncaught: ${err.message}`);
124262
124358
  process.exit(1);
124263
124359
  });
124264
124360
  process.on("exit", () => {
124265
124361
  flipUserHeld();
124266
124362
  });
124267
124363
  }
124268
- var import_node_net, log6;
124364
+ var import_node_net, log10;
124269
124365
  var init_dev_takeover = __esm({
124270
124366
  "src/lifecycle/dev-takeover.ts"() {
124271
124367
  "use strict";
@@ -124274,7 +124370,7 @@ var init_dev_takeover = __esm({
124274
124370
  init_marker();
124275
124371
  init_platform();
124276
124372
  init_prefs();
124277
- log6 = getLogger("lifecycle.dev-takeover");
124373
+ log10 = getLogger("lifecycle.dev-takeover");
124278
124374
  }
124279
124375
  });
124280
124376
 
@@ -127787,6 +127883,9 @@ function appendDevSessionMarker() {
127787
127883
  }
127788
127884
  }
127789
127885
 
127886
+ // cli/index.ts
127887
+ init_feature_flags();
127888
+
127790
127889
  // src/lan-url.ts
127791
127890
  var import_os2 = require("os");
127792
127891
  function resolveServerUrl({ publicUrl, port }) {
@@ -132113,7 +132212,7 @@ var REF_PREFIX = "ref: refs/heads/";
132113
132212
  var MAX_DEPTH = 6;
132114
132213
  function readGitBranch(projectPath) {
132115
132214
  if (!projectPath) return null;
132116
- const log8 = getLogger2();
132215
+ const log12 = getLogger2();
132117
132216
  let dir = projectPath;
132118
132217
  let depth = 0;
132119
132218
  while (depth < MAX_DEPTH) {
@@ -132122,11 +132221,11 @@ function readGitBranch(projectPath) {
132122
132221
  const content = (0, import_fs2.readFileSync)(headPath, "utf-8").trim();
132123
132222
  if (content.startsWith(REF_PREFIX)) {
132124
132223
  const branch = content.slice(REF_PREFIX.length);
132125
- log8.trace({ projectPath, dir, branch }, "git: branch resolved");
132224
+ log12.trace({ projectPath, dir, branch }, "git: branch resolved");
132126
132225
  return branch;
132127
132226
  }
132128
132227
  if (content.length >= 7) {
132129
- log8.trace({ projectPath, dir }, "git: detached HEAD");
132228
+ log12.trace({ projectPath, dir }, "git: detached HEAD");
132130
132229
  return "(detached)";
132131
132230
  }
132132
132231
  return null;
@@ -132137,7 +132236,7 @@ function readGitBranch(projectPath) {
132137
132236
  dir = parent;
132138
132237
  depth++;
132139
132238
  }
132140
- log8.trace({ projectPath }, "git: no .git found within depth");
132239
+ log12.trace({ projectPath }, "git: no .git found within depth");
132141
132240
  return null;
132142
132241
  }
132143
132242
  function generateMatches(meta3, query) {
@@ -132430,8 +132529,8 @@ function cleanSystemTags(text) {
132430
132529
  return text.replace(SYSTEM_TAG_RE, "").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
132431
132530
  }
132432
132531
  async function parseMeta(filePath, account, tier) {
132433
- const log8 = getLogger2();
132434
- log8.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
132532
+ const log12 = getLogger2();
132533
+ log12.trace({ filePath, account, tier: tier.name }, "parseMeta: start");
132435
132534
  const state = initialReducerState();
132436
132535
  const fileStream = (0, import_fs3.createReadStream)(filePath);
132437
132536
  const rl = (0, import_readline.createInterface)({ input: fileStream, crlfDelay: Infinity });
@@ -132448,22 +132547,22 @@ async function parseMeta(filePath, account, tier) {
132448
132547
  reduceLine(state, entry, tier);
132449
132548
  }
132450
132549
  } catch (err) {
132451
- log8.warn({ filePath, err }, "parseMeta: read failed");
132550
+ log12.warn({ filePath, err }, "parseMeta: read failed");
132452
132551
  return null;
132453
132552
  }
132454
132553
  if (state.badJsonLines > 0) {
132455
- log8.warn(
132554
+ log12.warn(
132456
132555
  { filePath, badJsonLines: state.badJsonLines },
132457
132556
  "parseMeta: skipped malformed JSON lines"
132458
132557
  );
132459
132558
  }
132460
132559
  const meta3 = finalizeMeta(state, filePath, account, tier);
132461
- if (!meta3) log8.trace({ filePath }, "parseMeta: no messages");
132560
+ if (!meta3) log12.trace({ filePath }, "parseMeta: no messages");
132462
132561
  return meta3;
132463
132562
  }
132464
132563
  async function parseConversation(filePath, account) {
132465
- const log8 = getLogger2();
132466
- log8.trace({ filePath, account }, "parseConversation: start");
132564
+ const log12 = getLogger2();
132565
+ log12.trace({ filePath, account }, "parseConversation: start");
132467
132566
  const messages = [];
132468
132567
  let badJsonLines = 0;
132469
132568
  const textParts = [];
@@ -132496,17 +132595,17 @@ async function parseConversation(filePath, account) {
132496
132595
  }
132497
132596
  }
132498
132597
  } catch (err) {
132499
- log8.warn({ filePath, err }, "parseConversation: read failed");
132598
+ log12.warn({ filePath, err }, "parseConversation: read failed");
132500
132599
  return null;
132501
132600
  }
132502
132601
  if (badJsonLines > 0) {
132503
- log8.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
132602
+ log12.warn({ filePath, badJsonLines }, "parseConversation: skipped malformed JSON lines");
132504
132603
  }
132505
132604
  if (messages.length === 0) {
132506
- log8.trace({ filePath }, "parseConversation: no messages");
132605
+ log12.trace({ filePath }, "parseConversation: no messages");
132507
132606
  return null;
132508
132607
  }
132509
- log8.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
132608
+ log12.debug({ filePath, messageCount: messages.length }, "parseConversation: complete");
132510
132609
  applyTeamInfo(messages, state);
132511
132610
  return {
132512
132611
  id: filePath,
@@ -132771,15 +132870,15 @@ async function detectDefaultProfile() {
132771
132870
  };
132772
132871
  }
132773
132872
  async function loadProfiles(configPath) {
132774
- const log8 = getLogger2();
132873
+ const log12 = getLogger2();
132775
132874
  try {
132776
132875
  const resolved = resolveConfigDir(configPath);
132777
132876
  const data = await (0, import_promises4.readFile)((0, import_path5.join)(resolved, PROFILES_FILE), "utf-8");
132778
132877
  const profiles = JSON.parse(data);
132779
- log8.debug({ configPath, count: profiles.length }, "profiles: loaded");
132878
+ log12.debug({ configPath, count: profiles.length }, "profiles: loaded");
132780
132879
  return profiles;
132781
132880
  } catch (err) {
132782
- log8.debug({ configPath, err }, "profiles: load failed, using default");
132881
+ log12.debug({ configPath, err }, "profiles: load failed, using default");
132783
132882
  const defaultProfile = await detectDefaultProfile();
132784
132883
  return [defaultProfile];
132785
132884
  }
@@ -132790,7 +132889,7 @@ function canonicalPath(p2) {
132790
132889
  var CodexCliProvider = class {
132791
132890
  name = CODEX_CLI_PROVIDER;
132792
132891
  async discover(roots) {
132793
- const log8 = getLogger2();
132892
+ const log12 = getLogger2();
132794
132893
  const results = [];
132795
132894
  for (const root of roots) {
132796
132895
  let paths;
@@ -132802,7 +132901,7 @@ var CodexCliProvider = class {
132802
132901
  unique: true
132803
132902
  });
132804
132903
  } catch (err) {
132805
- log8.warn({ root, err }, "codex discovery: glob failed");
132904
+ log12.warn({ root, err }, "codex discovery: glob failed");
132806
132905
  continue;
132807
132906
  }
132808
132907
  for (const filePath of paths) {
@@ -132810,7 +132909,7 @@ var CodexCliProvider = class {
132810
132909
  const s3 = await (0, import_promises5.stat)(filePath);
132811
132910
  if (s3.size > 0) results.push({ filePath: canonicalPath(filePath), account: "codex" });
132812
132911
  } catch (err) {
132813
- log8.warn({ filePath, err }, "codex discovery: stat failed");
132912
+ log12.warn({ filePath, err }, "codex discovery: stat failed");
132814
132913
  }
132815
132914
  }
132816
132915
  }
@@ -132954,7 +133053,7 @@ function getShortProjectName3(fullPath) {
132954
133053
  return fullPath.split("/").filter(Boolean).slice(-3).join("/");
132955
133054
  }
132956
133055
  async function parseCodexConversation(filePath, account) {
132957
- const log8 = getLogger2();
133056
+ const log12 = getLogger2();
132958
133057
  const messages = [];
132959
133058
  const textParts = [];
132960
133059
  let sessionId = "";
@@ -132990,7 +133089,7 @@ async function parseCodexConversation(filePath, account) {
132990
133089
  if (role === "user") lastUserText = text;
132991
133090
  }
132992
133091
  } catch (err) {
132993
- log8.warn({ filePath, err }, "parseCodexConversation: read failed");
133092
+ log12.warn({ filePath, err }, "parseCodexConversation: read failed");
132994
133093
  return null;
132995
133094
  }
132996
133095
  if (messages.length === 0) return null;
@@ -133012,7 +133111,7 @@ async function parseCodexConversation(filePath, account) {
133012
133111
  var EXCLUDED_SEGMENTS = ["/memory/", "/tool-results/"];
133013
133112
  var STAT_CONCURRENCY = 32;
133014
133113
  async function discoverJsonlFiles(dirs, onProgress) {
133015
- const log8 = getLogger2();
133114
+ const log12 = getLogger2();
133016
133115
  const results = [];
133017
133116
  for (const { projectsDir, account } of dirs) {
133018
133117
  let filePaths;
@@ -133023,7 +133122,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
133023
133122
  dot: false
133024
133123
  });
133025
133124
  } catch (err) {
133026
- log8.warn({ projectsDir, account, err }, "discovery: glob failed");
133125
+ log12.warn({ projectsDir, account, err }, "discovery: glob failed");
133027
133126
  continue;
133028
133127
  }
133029
133128
  const filtered = filePaths.filter((fp) => !EXCLUDED_SEGMENTS.some((seg) => fp.includes(seg)));
@@ -133038,7 +133137,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
133038
133137
  const s3 = await (0, import_promises6.stat)(filePath);
133039
133138
  return { filePath, size: s3.size };
133040
133139
  } catch (err) {
133041
- log8.warn({ filePath, err }, "discovery: stat failed");
133140
+ log12.warn({ filePath, err }, "discovery: stat failed");
133042
133141
  return { filePath, size: -1 };
133043
133142
  }
133044
133143
  })
@@ -133054,7 +133153,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
133054
133153
  }
133055
133154
  }
133056
133155
  }
133057
- log8.debug(
133156
+ log12.debug(
133058
133157
  {
133059
133158
  projectsDir,
133060
133159
  account,
@@ -133068,7 +133167,7 @@ async function discoverJsonlFiles(dirs, onProgress) {
133068
133167
  );
133069
133168
  onProgress?.(results.length);
133070
133169
  }
133071
- log8.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
133170
+ log12.debug({ totalFiles: results.length, dirs: dirs.length }, "discovery: complete");
133072
133171
  return results;
133073
133172
  }
133074
133173
  var ThreadbaseProvider = class {
@@ -133378,7 +133477,7 @@ function classify(filePath, existing) {
133378
133477
  return { change: "reindex", stat: stat42 };
133379
133478
  }
133380
133479
  async function parseMetaWithProvider(provider, filePath, account, tier) {
133381
- const log8 = getLogger2();
133480
+ const log12 = getLogger2();
133382
133481
  const acc = provider.createEmptyAccumulator();
133383
133482
  const rl = (0, import_readline3.createInterface)({
133384
133483
  input: (0, import_fs11.createReadStream)(filePath),
@@ -133396,11 +133495,11 @@ async function parseMetaWithProvider(provider, filePath, account, tier) {
133396
133495
  try {
133397
133496
  provider.reduceEntry(acc, entry, tier);
133398
133497
  } catch (err) {
133399
- log8.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
133498
+ log12.warn({ filePath, provider: provider.name, err }, "provider reduce threw; line skipped");
133400
133499
  }
133401
133500
  }
133402
133501
  } catch (err) {
133403
- log8.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
133502
+ log12.warn({ filePath, provider: provider.name, err }, "provider parse: read failed");
133404
133503
  return null;
133405
133504
  }
133406
133505
  return provider.finalize(acc, filePath, account, tier);
@@ -133574,8 +133673,8 @@ CREATE INDEX IF NOT EXISTS idx_scanned_dirs_parent_root ON scanned_dirs(parent_r
133574
133673
  function runMigrations(db) {
133575
133674
  const current = db.pragma("user_version", { simple: true });
133576
133675
  if (current >= SCHEMA_VERSION) return;
133577
- const log8 = getLogger2();
133578
- log8.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
133676
+ const log12 = getLogger2();
133677
+ log12.info({ from: current, to: SCHEMA_VERSION }, "migrations: applying");
133579
133678
  if (current >= 1 && current < 2 && tableExists(db, "conversations")) {
133580
133679
  for (const [col, ddl] of [
133581
133680
  [
@@ -133617,7 +133716,7 @@ function openDatabase(dbPath) {
133617
133716
  }
133618
133717
  var FULL_RECONCILE_EVERY_N_SCANS = 20;
133619
133718
  async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
133620
- const log8 = getLogger2();
133719
+ const log12 = getLogger2();
133621
133720
  if (options.fullRescan) {
133622
133721
  return discoverJsonlFiles(dirs);
133623
133722
  }
@@ -133659,7 +133758,7 @@ async function discoverJsonlFilesGated(dirs, files, scannedDirs, options = {}) {
133659
133758
  }
133660
133759
  }
133661
133760
  }
133662
- log8.debug(
133761
+ log12.debug(
133663
133762
  { totalFiles: results.length, dirs: dirs.length },
133664
133763
  "dir-watermark: gated discovery complete"
133665
133764
  );
@@ -134165,7 +134264,7 @@ var PersistentEngine = class {
134165
134264
  // changed since the last index, and upsert their metadata. Returns the number
134166
134265
  // of files seen on disk this pass (the scan "scanned" count).
134167
134266
  async indexAll(activeProfiles, options) {
134168
- const log8 = getLogger2();
134267
+ const log12 = getLogger2();
134169
134268
  const tier = resolveTier(options.tier ?? "standard", options.tiers);
134170
134269
  const enabled = options.providers ?? [CLAUDE_CODE_PROVIDER];
134171
134270
  const discovered = [];
@@ -134229,7 +134328,7 @@ var PersistentEngine = class {
134229
134328
  for (const path2 of this.files.activePathsByAccounts([...coveredAccounts])) {
134230
134329
  if (!seen.has(path2)) this.markDeleted(path2);
134231
134330
  }
134232
- log8.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
134331
+ log12.info({ scanned, indexed: this.conversations.count() }, "persistent: indexAll complete");
134233
134332
  return { scanned };
134234
134333
  }
134235
134334
  // (Re)index a single file. Classifies the change vs. the persisted cursor:
@@ -134240,7 +134339,7 @@ var PersistentEngine = class {
134240
134339
  // alongside the meta so callers (refreshFile) can keep, extend, or evict
134241
134340
  // their own per-file caches without re-stat'ing the file (racy) themselves.
134242
134341
  async indexFile(rawFilePath, account, tierName, customTiers, resolveGitBranch, force = false, provider) {
134243
- const log8 = getLogger2();
134342
+ const log12 = getLogger2();
134244
134343
  const tier = resolveTier(tierName, customTiers);
134245
134344
  const filePath = canonicalPath(rawFilePath);
134246
134345
  const existing = this.files.getByPath(filePath);
@@ -134271,7 +134370,7 @@ var PersistentEngine = class {
134271
134370
  try {
134272
134371
  result = await tailReduce(filePath, startOffset, startLine, state, tier);
134273
134372
  } catch (err) {
134274
- log8.warn({ filePath, err }, "persistent: tail read failed");
134373
+ log12.warn({ filePath, err }, "persistent: tail read failed");
134275
134374
  return { meta: null, change };
134276
134375
  }
134277
134376
  const meta3 = finalizeMeta(state, filePath, account, tier);
@@ -134314,7 +134413,7 @@ var PersistentEngine = class {
134314
134413
  )
134315
134414
  );
134316
134415
  }
134317
- log8.debug(
134416
+ log12.debug(
134318
134417
  { filePath, change, bytesRead: result.newOffset - startOffset, msgs: meta3.messageCount },
134319
134418
  "persistent: indexed file"
134320
134419
  );
@@ -134326,7 +134425,7 @@ var PersistentEngine = class {
134326
134425
  // so the next pass classifies an unchanged file as "unchanged" and skips it;
134327
134426
  // any change reparses from 0 again. No reducer_state is persisted.
134328
134427
  async indexFileWithProvider(provider, filePath, account, tier, stat42, resolveGitBranch) {
134329
- const log8 = getLogger2();
134428
+ const log12 = getLogger2();
134330
134429
  const meta3 = await parseMetaWithProvider(provider, filePath, account, tier);
134331
134430
  if (!meta3) {
134332
134431
  this.markDeleted(filePath);
@@ -134354,7 +134453,7 @@ var PersistentEngine = class {
134354
134453
  });
134355
134454
  });
134356
134455
  upsert();
134357
- log8.debug(
134456
+ log12.debug(
134358
134457
  { filePath, provider: provider.name, msgs: meta3.messageCount },
134359
134458
  "persistent: indexed provider file"
134360
134459
  );
@@ -134468,7 +134567,7 @@ var FileWatcher = class {
134468
134567
  // Resolves once every underlying chokidar watcher has finished its initial
134469
134568
  // scan, so the caller knows subsequent FS changes will be observed.
134470
134569
  async start() {
134471
- const log8 = getLogger2();
134570
+ const log12 = getLogger2();
134472
134571
  const ready = [];
134473
134572
  for (const profile of this.profiles) {
134474
134573
  const dir = getProjectsDir(profile);
@@ -134480,7 +134579,7 @@ var FileWatcher = class {
134480
134579
  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"));
134481
134580
  ready.push(new Promise((resolve4) => watcher.once("ready", () => resolve4())));
134482
134581
  this.watchers.push(watcher);
134483
- log8.debug({ dir, account: profile.id }, "watcher: watching");
134582
+ log12.debug({ dir, account: profile.id }, "watcher: watching");
134484
134583
  }
134485
134584
  await Promise.all(ready);
134486
134585
  }
@@ -134527,14 +134626,14 @@ var IndexQueue = class {
134527
134626
  async drain() {
134528
134627
  if (this.running) return;
134529
134628
  this.running = true;
134530
- const log8 = getLogger2();
134629
+ const log12 = getLogger2();
134531
134630
  while (this.pending.size > 0) {
134532
134631
  const [path2, job] = this.pending.entries().next().value;
134533
134632
  this.pending.delete(path2);
134534
134633
  try {
134535
134634
  await this.process(job);
134536
134635
  } catch (err) {
134537
- log8.warn({ filePath: job.filePath, err }, "index-queue: job failed");
134636
+ log12.warn({ filePath: job.filePath, err }, "index-queue: job failed");
134538
134637
  }
134539
134638
  }
134540
134639
  this.running = false;
@@ -134643,13 +134742,13 @@ var ConversationScanner = class {
134643
134742
  // active metas and run the identical filter/sort/view/paginate pipeline as
134644
134743
  // the in-memory path — guaranteeing an identical ScanResult shape.
134645
134744
  async scanPersistent(activeProfiles, options) {
134646
- const log8 = getLogger2();
134745
+ const log12 = getLogger2();
134647
134746
  const startedAt = Date.now();
134648
134747
  const engine = this.engine();
134649
134748
  const { scanned } = await engine.indexAll(activeProfiles, options);
134650
134749
  const allMetas = engine.allActive();
134651
134750
  const { conversations, total } = this.finalize(allMetas, options);
134652
- log8.info(
134751
+ log12.info(
134653
134752
  { scanned, kept: allMetas.length, filteredTotal: total, elapsedMs: Date.now() - startedAt },
134654
134753
  "scan: complete (persistent)"
134655
134754
  );
@@ -134676,10 +134775,10 @@ var ConversationScanner = class {
134676
134775
  return { conversations, total };
134677
134776
  }
134678
134777
  async scanInMemory(activeProfiles, options) {
134679
- const log8 = getLogger2();
134778
+ const log12 = getLogger2();
134680
134779
  const startedAt = Date.now();
134681
134780
  const tier = this.lastTier;
134682
- log8.info(
134781
+ log12.info(
134683
134782
  {
134684
134783
  activeProfiles: activeProfiles.length,
134685
134784
  tier: tier.name,
@@ -134733,7 +134832,7 @@ var ConversationScanner = class {
134733
134832
  return meta3;
134734
134833
  } catch (err) {
134735
134834
  parseFailures++;
134736
- log8.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
134835
+ log12.warn({ filePath, account, provider: provider.name, err }, "scan: parse threw");
134737
134836
  return null;
134738
134837
  }
134739
134838
  })
@@ -134753,12 +134852,12 @@ var ConversationScanner = class {
134753
134852
  options.onBatch?.(batchMetas);
134754
134853
  }
134755
134854
  scanned += batch.length;
134756
- log8.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
134855
+ log12.debug({ scanned, totalFiles, batchKept: batchMetas.length }, "scan: batch complete");
134757
134856
  options.onProgress?.(scanned, totalFiles);
134758
134857
  }
134759
134858
  const { conversations, total } = this.finalize(allMetas, options);
134760
134859
  const elapsedMs = Date.now() - startedAt;
134761
- log8.info(
134860
+ log12.info(
134762
134861
  {
134763
134862
  totalFiles,
134764
134863
  scanned,
@@ -134772,13 +134871,13 @@ var ConversationScanner = class {
134772
134871
  return { conversations, total, scanned };
134773
134872
  }
134774
134873
  async search(query, options = {}) {
134775
- const log8 = getLogger2();
134776
- log8.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
134874
+ const log12 = getLogger2();
134875
+ log12.debug({ query, indexSize: this.indexer.getDocumentCount() }, "search: start");
134777
134876
  let results;
134778
134877
  if (this.persistent) {
134779
134878
  const engine = this.engine();
134780
134879
  if (engine.conversations.count() === 0) {
134781
- log8.debug("search: persistent index empty, triggering scan");
134880
+ log12.debug("search: persistent index empty, triggering scan");
134782
134881
  const profiles = await this.resolveProfiles(options.profiles);
134783
134882
  const activeProfiles = profiles.filter((p2) => p2.enabled && p2.scanHistory !== false);
134784
134883
  await engine.indexAll(activeProfiles, { ...options, limit: void 0, offset: void 0 });
@@ -134791,7 +134890,7 @@ var ConversationScanner = class {
134791
134890
  }));
134792
134891
  } else {
134793
134892
  if (this.indexer.getDocumentCount() === 0) {
134794
- log8.debug("search: index empty, triggering scan");
134893
+ log12.debug("search: index empty, triggering scan");
134795
134894
  await this.scan({ ...options, limit: void 0, offset: void 0 });
134796
134895
  }
134797
134896
  results = this.indexer.search(query, {
@@ -134834,23 +134933,23 @@ var ConversationScanner = class {
134834
134933
  const limit = options.limit ?? 50;
134835
134934
  const offset = options.offset ?? 0;
134836
134935
  const sliced = results.slice(offset, offset + limit);
134837
- log8.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
134936
+ log12.debug({ query, matched: results.length, returned: sliced.length }, "search: complete");
134838
134937
  return sliced;
134839
134938
  }
134840
134939
  async getConversation(id, _options) {
134841
- const log8 = getLogger2();
134940
+ const log12 = getLogger2();
134842
134941
  const cid = canonicalPath(id);
134843
134942
  const cached3 = this.conversationLRU.get(cid);
134844
134943
  if (cached3) {
134845
- log8.debug({ id }, "getConversation: cache hit");
134944
+ log12.debug({ id }, "getConversation: cache hit");
134846
134945
  return cached3.conversation;
134847
134946
  }
134848
134947
  const meta3 = this.persistent ? this.engine().getByIdOrSession(cid) : this.metadataCache.get(cid) ?? this.resolveSessionId(cid);
134849
134948
  if (!meta3) {
134850
- log8.debug({ id }, "getConversation: not found in metadata");
134949
+ log12.debug({ id }, "getConversation: not found in metadata");
134851
134950
  return null;
134852
134951
  }
134853
- log8.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
134952
+ log12.debug({ id, filePath: meta3.filePath }, "getConversation: cache miss, parsing");
134854
134953
  try {
134855
134954
  if (this.persistent && meta3.provider !== CODEX_CLI_PROVIDER) {
134856
134955
  const parsed = await parseConversationResumable(meta3.filePath, meta3.account);
@@ -134863,7 +134962,7 @@ var ConversationScanner = class {
134863
134962
  }
134864
134963
  return conversation;
134865
134964
  } catch (err) {
134866
- log8.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
134965
+ log12.warn({ id, filePath: meta3.filePath, err }, "getConversation: parse failed");
134867
134966
  return null;
134868
134967
  }
134869
134968
  }
@@ -134949,7 +135048,7 @@ var ConversationScanner = class {
134949
135048
  return refresh;
134950
135049
  }
134951
135050
  async doRefreshFile(filePath, account) {
134952
- const log8 = getLogger2();
135051
+ const log12 = getLogger2();
134953
135052
  if (this.persistent) {
134954
135053
  const engine = this.engine();
134955
135054
  const previous2 = engine.getByIdOrSession(filePath);
@@ -134976,7 +135075,7 @@ var ConversationScanner = class {
134976
135075
  } else if (change === "appended") {
134977
135076
  await this.extendCachedConversations(cacheKeys, filePath, meta22.account);
134978
135077
  }
134979
- log8.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
135078
+ log12.debug({ filePath, change, kept: !!meta22 }, "refreshFile: updated persistent index");
134980
135079
  return meta22;
134981
135080
  }
134982
135081
  const previous = this.metadataCache.get(filePath);
@@ -134985,7 +135084,7 @@ var ConversationScanner = class {
134985
135084
  try {
134986
135085
  meta3 = await parseMeta(filePath, resolvedAccount, this.lastTier);
134987
135086
  } catch (err) {
134988
- log8.warn({ filePath, err }, "refreshFile: parseMeta threw");
135087
+ log12.warn({ filePath, err }, "refreshFile: parseMeta threw");
134989
135088
  meta3 = null;
134990
135089
  }
134991
135090
  const evict = (m2) => {
@@ -135001,7 +135100,7 @@ var ConversationScanner = class {
135001
135100
  this.removeFromSessionIndex(previous);
135002
135101
  this.indexer.removeDocument(previous.id);
135003
135102
  }
135004
- log8.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
135103
+ log12.debug({ filePath }, "refreshFile: dropped (no parseable messages)");
135005
135104
  return null;
135006
135105
  }
135007
135106
  meta3.gitBranch = readGitBranch(meta3.projectPath);
@@ -135014,7 +135113,7 @@ var ConversationScanner = class {
135014
135113
  } else {
135015
135114
  this.indexer.addDocument(meta3);
135016
135115
  }
135017
- log8.debug(
135116
+ log12.debug(
135018
135117
  { filePath, messageCount: meta3.messageCount },
135019
135118
  "refreshFile: updated in-memory indexes"
135020
135119
  );
@@ -138222,6 +138321,7 @@ function readRawBody2(req) {
138222
138321
  var createConfigRoutes = (deps) => {
138223
138322
  const app = new Hono2();
138224
138323
  app.get("/claude-flags", (c) => c.json(deps.claudeFlagsConfig()));
138324
+ app.get("/feature-flags", (c) => c.json(deps.featureFlagsConfig()));
138225
138325
  app.put("/claude-flags", async (c) => {
138226
138326
  if (deps.localNoAuth) {
138227
138327
  return c.json({ error: "claude flag changes are disabled while localNoAuth is active" }, 403);
@@ -138475,7 +138575,266 @@ function createLogsRoutes() {
138475
138575
  var import_node_child_process2 = require("child_process");
138476
138576
  var import_node_crypto = require("crypto");
138477
138577
  var import_os5 = require("os");
138578
+
138579
+ // src/db/repositories/push.repository.ts
138580
+ var FAILURE_STREAK_LIMIT = 5;
138581
+ var PUSH_TOKEN_KINDS = ["expo", "liveactivity_start", "liveactivity_update"];
138582
+ var DEFAULT_PUSH_TOKEN_KIND = "expo";
138583
+ function isPushTokenKind(value) {
138584
+ return typeof value === "string" && PUSH_TOKEN_KINDS.includes(value);
138585
+ }
138586
+ function tokenState(row, now = Date.now()) {
138587
+ if (row.revoked_at != null) return "revoked";
138588
+ if (row.expires_at != null && row.expires_at <= now) return "expired";
138589
+ if (row.failure_streak >= FAILURE_STREAK_LIMIT) return "dead";
138590
+ if (row.failure_streak > 0) return "failing";
138591
+ if (row.last_success_at == null) return "never-delivered";
138592
+ return "healthy";
138593
+ }
138594
+ function toHealth(row, now = Date.now()) {
138595
+ return {
138596
+ platform: row.platform,
138597
+ deviceId: row.device_id,
138598
+ registeredAt: row.registered_at,
138599
+ lastSuccessAt: row.last_success_at,
138600
+ lastFailureAt: row.last_failure_at,
138601
+ lastFailureCode: row.last_failure_code,
138602
+ failureStreak: row.failure_streak,
138603
+ revokedAt: row.revoked_at,
138604
+ state: tokenState(row, now),
138605
+ kind: row.kind,
138606
+ activityId: row.activity_id,
138607
+ sessionId: row.session_id,
138608
+ expiresAt: row.expires_at
138609
+ };
138610
+ }
138611
+ var PushRepository = class {
138612
+ upsertStmt;
138613
+ getStmt;
138614
+ listActiveStmt;
138615
+ listAllStmt;
138616
+ successStmt;
138617
+ failureStmt;
138618
+ revokeStmt;
138619
+ claimEventStmt;
138620
+ markDeliveredStmt;
138621
+ listByKindSessionStmt;
138622
+ listByKindStmt;
138623
+ listRenewableStmt;
138624
+ claimRenewalStmt;
138625
+ expireStmt;
138626
+ expireSessionActivitiesStmt;
138627
+ constructor(db) {
138628
+ this.upsertStmt = db.prepare(`
138629
+ INSERT INTO push_tokens (
138630
+ token, platform, device_id, registered_at,
138631
+ kind, activity_id, session_id, expires_at, stale_date, started_at
138632
+ )
138633
+ VALUES (
138634
+ @token, @platform, @device_id, @registered_at,
138635
+ @kind, @activity_id, @session_id, @expires_at, @stale_date, @started_at
138636
+ )
138637
+ ON CONFLICT(token) DO UPDATE SET
138638
+ platform = excluded.platform,
138639
+ device_id = COALESCE(excluded.device_id, push_tokens.device_id),
138640
+ registered_at = excluded.registered_at,
138641
+ kind = excluded.kind,
138642
+ activity_id = COALESCE(excluded.activity_id, push_tokens.activity_id),
138643
+ session_id = COALESCE(excluded.session_id, push_tokens.session_id),
138644
+ expires_at = excluded.expires_at,
138645
+ stale_date = excluded.stale_date,
138646
+ -- Preserve the ORIGINAL start across a re-registration. iOS renders its
138647
+ -- own ticking timer from started_at, so overwriting it with a fresh
138648
+ -- value visibly resets the user's elapsed time to zero.
138649
+ started_at = COALESCE(push_tokens.started_at, excluded.started_at),
138650
+ -- A fresh registration clears prior failure state and any revocation:
138651
+ -- the client is telling us this token is live again. renewed_at clears
138652
+ -- too \u2014 this is a new activity generation, so it is renewable again.
138653
+ failure_streak = 0,
138654
+ last_failure_at = NULL,
138655
+ last_failure_code = NULL,
138656
+ revoked_at = NULL,
138657
+ renewed_at = NULL
138658
+ `);
138659
+ this.getStmt = db.prepare("SELECT * FROM push_tokens WHERE token = ?");
138660
+ this.listActiveStmt = db.prepare(`
138661
+ SELECT * FROM push_tokens
138662
+ WHERE revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}
138663
+ AND kind = 'expo'
138664
+ ORDER BY registered_at ASC
138665
+ `);
138666
+ this.listAllStmt = db.prepare("SELECT * FROM push_tokens ORDER BY registered_at ASC");
138667
+ this.successStmt = db.prepare(`
138668
+ UPDATE push_tokens
138669
+ SET last_success_at = @at, failure_streak = 0,
138670
+ last_failure_code = NULL
138671
+ WHERE token = @token
138672
+ `);
138673
+ this.failureStmt = db.prepare(`
138674
+ UPDATE push_tokens
138675
+ SET last_failure_at = @at, last_failure_code = @code,
138676
+ failure_streak = failure_streak + 1
138677
+ WHERE token = @token
138678
+ `);
138679
+ this.revokeStmt = db.prepare("UPDATE push_tokens SET revoked_at = ? WHERE token = ?");
138680
+ this.listByKindSessionStmt = db.prepare(`
138681
+ SELECT * FROM push_tokens
138682
+ WHERE kind = @kind AND session_id = @session_id
138683
+ AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}
138684
+ AND (expires_at IS NULL OR expires_at > @now)
138685
+ ORDER BY registered_at ASC
138686
+ `);
138687
+ this.listByKindStmt = db.prepare(`
138688
+ SELECT * FROM push_tokens
138689
+ WHERE kind = @kind
138690
+ AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}
138691
+ AND (expires_at IS NULL OR expires_at > @now)
138692
+ ORDER BY registered_at ASC
138693
+ `);
138694
+ this.listRenewableStmt = db.prepare(`
138695
+ SELECT * FROM push_tokens
138696
+ WHERE kind = 'liveactivity_update'
138697
+ AND stale_date IS NOT NULL AND renewed_at IS NULL
138698
+ AND revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}
138699
+ ORDER BY stale_date ASC
138700
+ `);
138701
+ this.claimRenewalStmt = db.prepare(`
138702
+ UPDATE push_tokens SET renewed_at = @at
138703
+ WHERE token = @token AND renewed_at IS NULL
138704
+ `);
138705
+ this.expireStmt = db.prepare("UPDATE push_tokens SET expires_at = ? WHERE token = ?");
138706
+ this.expireSessionActivitiesStmt = db.prepare(`
138707
+ UPDATE push_tokens SET expires_at = @at
138708
+ WHERE session_id = @session_id AND kind = 'liveactivity_update'
138709
+ AND (expires_at IS NULL OR expires_at > @at)
138710
+ `);
138711
+ this.claimEventStmt = db.prepare(`
138712
+ INSERT OR IGNORE INTO push_events (event_id, session_id, created_at)
138713
+ VALUES (@event_id, @session_id, @created_at)
138714
+ `);
138715
+ this.markDeliveredStmt = db.prepare(
138716
+ "UPDATE push_events SET delivered_at = ? WHERE event_id = ?"
138717
+ );
138718
+ }
138719
+ /**
138720
+ * Register or refresh a token.
138721
+ *
138722
+ * `kind` defaults to Expo so a released client posting `{ token, platform }`
138723
+ * keeps working — tb-mobile cannot be force-updated, and every client
138724
+ * predating Live Activities is registering an Expo relay token.
138725
+ *
138726
+ * Several rows per device is normal and intended: a device runs one activity
138727
+ * per live session, each with its own update token. The token itself is the
138728
+ * primary key, so distinct activities never collide.
138729
+ */
138730
+ register(args) {
138731
+ this.upsertStmt.run({
138732
+ token: args.token,
138733
+ platform: args.platform,
138734
+ device_id: args.deviceId ?? null,
138735
+ registered_at: args.now ?? Date.now(),
138736
+ kind: args.kind ?? DEFAULT_PUSH_TOKEN_KIND,
138737
+ activity_id: args.activityId ?? null,
138738
+ session_id: args.sessionId ?? null,
138739
+ expires_at: args.expiresAt ?? null,
138740
+ stale_date: args.staleDate ?? null,
138741
+ started_at: args.startedAt ?? null
138742
+ });
138743
+ }
138744
+ get(token) {
138745
+ return this.getStmt.get(token) ?? null;
138746
+ }
138747
+ /**
138748
+ * Expo tokens eligible for delivery — not revoked, not past the failure limit.
138749
+ *
138750
+ * Deliberately Expo-only. ActivityKit tokens go over direct APNs with a
138751
+ * different topic and are rejected by Expo's relay, so the ordinary
138752
+ * notification fan-out must not see them.
138753
+ */
138754
+ listDeliverable() {
138755
+ return this.listActiveStmt.all();
138756
+ }
138757
+ /** Live-activity tokens for one session, eligible for delivery. */
138758
+ listForSession(kind, sessionId, now = Date.now()) {
138759
+ return this.listByKindSessionStmt.all({
138760
+ kind,
138761
+ session_id: sessionId,
138762
+ now
138763
+ });
138764
+ }
138765
+ /**
138766
+ * Every deliverable token of one kind.
138767
+ *
138768
+ * Used for push-to-start, which is app-wide rather than session-scoped: the
138769
+ * activity does not exist yet, so there is no per-activity token to look up.
138770
+ */
138771
+ listByKind(kind, now = Date.now()) {
138772
+ return this.listByKindStmt.all({ kind, now });
138773
+ }
138774
+ /** Unrenewed activities with a renewal deadline, soonest first. */
138775
+ listRenewable() {
138776
+ return this.listRenewableStmt.all();
138777
+ }
138778
+ /**
138779
+ * Claim a row for renewal.
138780
+ *
138781
+ * Returns true exactly once per row. A restart re-arms timers from the
138782
+ * persisted deadline, so the same renewal can be attempted twice; the loser
138783
+ * gets false and must not send. Doing this as a conditional UPDATE rather
138784
+ * than read-then-write avoids the race where both attempts observe
138785
+ * "not yet renewed".
138786
+ */
138787
+ claimRenewal(token, now = Date.now()) {
138788
+ return this.claimRenewalStmt.run({ token, at: now }).changes > 0;
138789
+ }
138790
+ /** Mark one token expired, so it stops being a delivery target. */
138791
+ expire(token, now = Date.now()) {
138792
+ this.expireStmt.run(now, token);
138793
+ }
138794
+ /**
138795
+ * Expire every live activity for a session.
138796
+ *
138797
+ * Called when the session ends. Without this, a per-activity token outlives
138798
+ * its session and a later renewal sweep would resurrect an activity for a
138799
+ * session that is already gone.
138800
+ */
138801
+ expireSessionActivities(sessionId, now = Date.now()) {
138802
+ this.expireSessionActivitiesStmt.run({ session_id: sessionId, at: now });
138803
+ }
138804
+ /** Every token, including dead and revoked ones, for the health report. */
138805
+ listHealth(now = Date.now()) {
138806
+ return this.listAllStmt.all().map((r) => toHealth(r, now));
138807
+ }
138808
+ recordSuccess(token, now = Date.now()) {
138809
+ this.successStmt.run({ token, at: now });
138810
+ }
138811
+ recordFailure(token, code, now = Date.now()) {
138812
+ this.failureStmt.run({ token, at: now, code });
138813
+ }
138814
+ revoke(token, now = Date.now()) {
138815
+ return this.revokeStmt.run(now, token).changes > 0;
138816
+ }
138817
+ /**
138818
+ * Claim an event id for delivery.
138819
+ *
138820
+ * Returns true exactly once per event id. A retry, a reconnect
138821
+ * reconciliation, or two triggers firing for the same underlying event all
138822
+ * get false and must not notify — the user should never be told twice about
138823
+ * one thing.
138824
+ */
138825
+ claimEvent(eventId, sessionId, now = Date.now()) {
138826
+ return this.claimEventStmt.run({ event_id: eventId, session_id: sessionId, created_at: now }).changes > 0;
138827
+ }
138828
+ markDelivered(eventId, now = Date.now()) {
138829
+ this.markDeliveredStmt.run(now, eventId);
138830
+ }
138831
+ };
138832
+
138833
+ // src/api/routes/misc.routes.ts
138478
138834
  init_logger();
138835
+ function numberOrNull(value) {
138836
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
138837
+ }
138479
138838
  function readJsonBody(req) {
138480
138839
  return new Promise((resolve4, reject) => {
138481
138840
  const chunks = [];
@@ -138522,7 +138881,11 @@ var createMiscRoutes = (deps) => {
138522
138881
  // Capability flag: this server serves /api/config/claude-flags. Additive —
138523
138882
  // older clients ignore it, and clients talking to an older server see it
138524
138883
  // absent and hide the UI rather than 404ing.
138525
- claudeFlags: true
138884
+ claudeFlags: true,
138885
+ // Same contract: this server serves GET /api/config/feature-flags. Lives
138886
+ // here rather than behind /api/config (admin-only) so a read-only client
138887
+ // still learns the server supports flags even if it can't read values.
138888
+ featureFlags: true
138526
138889
  });
138527
138890
  });
138528
138891
  app.get("/api/profiles", (c) => c.json([]));
@@ -138549,6 +138912,22 @@ var createMiscRoutes = (deps) => {
138549
138912
  if (platform3 !== "ios" && platform3 !== "android") {
138550
138913
  return c.json({ error: "platform must be 'ios' or 'android'" }, 400);
138551
138914
  }
138915
+ const kind = body?.kind === void 0 ? DEFAULT_PUSH_TOKEN_KIND : body.kind;
138916
+ if (!isPushTokenKind(kind)) {
138917
+ return c.json(
138918
+ { error: `kind must be one of ${PUSH_TOKEN_KINDS.join(", ")}`, code: "INVALID_KIND" },
138919
+ 400
138920
+ );
138921
+ }
138922
+ if (kind === "liveactivity_update" && typeof body?.activityId !== "string") {
138923
+ return c.json(
138924
+ {
138925
+ error: "activityId is required for kind 'liveactivity_update'",
138926
+ code: "MISSING_ACTIVITY"
138927
+ },
138928
+ 400
138929
+ );
138930
+ }
138552
138931
  const repo = deps.pushRepo();
138553
138932
  if (!repo) {
138554
138933
  return c.json({ error: "Push registration is unavailable", code: "STORE_UNAVAILABLE" }, 503);
@@ -138556,7 +138935,13 @@ var createMiscRoutes = (deps) => {
138556
138935
  repo.register({
138557
138936
  token,
138558
138937
  platform: platform3,
138559
- deviceId: typeof body?.deviceId === "string" ? body.deviceId : null
138938
+ deviceId: typeof body?.deviceId === "string" ? body.deviceId : null,
138939
+ kind,
138940
+ activityId: typeof body?.activityId === "string" ? body.activityId : null,
138941
+ sessionId: typeof body?.sessionId === "string" ? body.sessionId : null,
138942
+ expiresAt: numberOrNull(body?.expiresAt),
138943
+ staleDate: numberOrNull(body?.staleDate),
138944
+ startedAt: numberOrNull(body?.startedAt)
138560
138945
  });
138561
138946
  return c.json({ ok: true });
138562
138947
  });
@@ -141004,125 +141389,6 @@ function deriveNameFromPath(path2) {
141004
141389
  return parts.length > 0 ? parts[parts.length - 1] : null;
141005
141390
  }
141006
141391
 
141007
- // src/db/repositories/push.repository.ts
141008
- var FAILURE_STREAK_LIMIT = 5;
141009
- function tokenState(row) {
141010
- if (row.revoked_at != null) return "revoked";
141011
- if (row.failure_streak >= FAILURE_STREAK_LIMIT) return "dead";
141012
- if (row.failure_streak > 0) return "failing";
141013
- if (row.last_success_at == null) return "never-delivered";
141014
- return "healthy";
141015
- }
141016
- function toHealth(row) {
141017
- return {
141018
- platform: row.platform,
141019
- deviceId: row.device_id,
141020
- registeredAt: row.registered_at,
141021
- lastSuccessAt: row.last_success_at,
141022
- lastFailureAt: row.last_failure_at,
141023
- lastFailureCode: row.last_failure_code,
141024
- failureStreak: row.failure_streak,
141025
- revokedAt: row.revoked_at,
141026
- state: tokenState(row)
141027
- };
141028
- }
141029
- var PushRepository = class {
141030
- upsertStmt;
141031
- getStmt;
141032
- listActiveStmt;
141033
- listAllStmt;
141034
- successStmt;
141035
- failureStmt;
141036
- revokeStmt;
141037
- claimEventStmt;
141038
- markDeliveredStmt;
141039
- constructor(db) {
141040
- this.upsertStmt = db.prepare(`
141041
- INSERT INTO push_tokens (token, platform, device_id, registered_at)
141042
- VALUES (@token, @platform, @device_id, @registered_at)
141043
- ON CONFLICT(token) DO UPDATE SET
141044
- platform = excluded.platform,
141045
- device_id = COALESCE(excluded.device_id, push_tokens.device_id),
141046
- registered_at = excluded.registered_at,
141047
- -- A fresh registration clears prior failure state and any revocation:
141048
- -- the client is telling us this token is live again.
141049
- failure_streak = 0,
141050
- last_failure_at = NULL,
141051
- last_failure_code = NULL,
141052
- revoked_at = NULL
141053
- `);
141054
- this.getStmt = db.prepare("SELECT * FROM push_tokens WHERE token = ?");
141055
- this.listActiveStmt = db.prepare(`
141056
- SELECT * FROM push_tokens
141057
- WHERE revoked_at IS NULL AND failure_streak < ${FAILURE_STREAK_LIMIT}
141058
- ORDER BY registered_at ASC
141059
- `);
141060
- this.listAllStmt = db.prepare("SELECT * FROM push_tokens ORDER BY registered_at ASC");
141061
- this.successStmt = db.prepare(`
141062
- UPDATE push_tokens
141063
- SET last_success_at = @at, failure_streak = 0,
141064
- last_failure_code = NULL
141065
- WHERE token = @token
141066
- `);
141067
- this.failureStmt = db.prepare(`
141068
- UPDATE push_tokens
141069
- SET last_failure_at = @at, last_failure_code = @code,
141070
- failure_streak = failure_streak + 1
141071
- WHERE token = @token
141072
- `);
141073
- this.revokeStmt = db.prepare("UPDATE push_tokens SET revoked_at = ? WHERE token = ?");
141074
- this.claimEventStmt = db.prepare(`
141075
- INSERT OR IGNORE INTO push_events (event_id, session_id, created_at)
141076
- VALUES (@event_id, @session_id, @created_at)
141077
- `);
141078
- this.markDeliveredStmt = db.prepare(
141079
- "UPDATE push_events SET delivered_at = ? WHERE event_id = ?"
141080
- );
141081
- }
141082
- register(args) {
141083
- this.upsertStmt.run({
141084
- token: args.token,
141085
- platform: args.platform,
141086
- device_id: args.deviceId ?? null,
141087
- registered_at: args.now ?? Date.now()
141088
- });
141089
- }
141090
- get(token) {
141091
- return this.getStmt.get(token) ?? null;
141092
- }
141093
- /** Tokens eligible for delivery — not revoked, not past the failure limit. */
141094
- listDeliverable() {
141095
- return this.listActiveStmt.all();
141096
- }
141097
- /** Every token, including dead and revoked ones, for the health report. */
141098
- listHealth() {
141099
- return this.listAllStmt.all().map(toHealth);
141100
- }
141101
- recordSuccess(token, now = Date.now()) {
141102
- this.successStmt.run({ token, at: now });
141103
- }
141104
- recordFailure(token, code, now = Date.now()) {
141105
- this.failureStmt.run({ token, at: now, code });
141106
- }
141107
- revoke(token, now = Date.now()) {
141108
- return this.revokeStmt.run(now, token).changes > 0;
141109
- }
141110
- /**
141111
- * Claim an event id for delivery.
141112
- *
141113
- * Returns true exactly once per event id. A retry, a reconnect
141114
- * reconciliation, or two triggers firing for the same underlying event all
141115
- * get false and must not notify — the user should never be told twice about
141116
- * one thing.
141117
- */
141118
- claimEvent(eventId, sessionId, now = Date.now()) {
141119
- return this.claimEventStmt.run({ event_id: eventId, session_id: sessionId, created_at: now }).changes > 0;
141120
- }
141121
- markDelivered(eventId, now = Date.now()) {
141122
- this.markDeliveredStmt.run(now, eventId);
141123
- }
141124
- };
141125
-
141126
141392
  // src/db/repositories/sessions.repository.ts
141127
141393
  var SessionsRepository = class {
141128
141394
  constructor(store) {
@@ -141156,6 +141422,9 @@ async function recordUpload(pool2, instanceId, row) {
141156
141422
  );
141157
141423
  }
141158
141424
 
141425
+ // src/server.ts
141426
+ init_feature_flags();
141427
+
141159
141428
  // src/handlers/handleListProjects.ts
141160
141429
  var import_fs18 = require("fs");
141161
141430
  var import_os7 = require("os");
@@ -143429,10 +143698,10 @@ function fingerprintOf(ids) {
143429
143698
  return `sha256:${(0, import_crypto11.createHash)("sha256").update(sorted.join("\n")).digest("hex")}`;
143430
143699
  }
143431
143700
  var CacheIntegrityMonitor = class {
143432
- constructor(cache, wsHub, log8, cacheDir, rescan, runDuringReset) {
143701
+ constructor(cache, wsHub, log12, cacheDir, rescan, runDuringReset) {
143433
143702
  this.cache = cache;
143434
143703
  this.wsHub = wsHub;
143435
- this.log = log8;
143704
+ this.log = log12;
143436
143705
  this.cacheDir = cacheDir;
143437
143706
  this.rescan = rescan;
143438
143707
  this.runDuringReset = runDuringReset;
@@ -144264,6 +144533,585 @@ function deriveProjectChatTitle(input) {
144264
144533
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
144265
144534
  }
144266
144535
 
144536
+ // src/services/push/apnsClient.ts
144537
+ var import_node_crypto3 = require("crypto");
144538
+ var import_node_http2 = require("http2");
144539
+ init_logger();
144540
+ var log3 = getLogger("apns");
144541
+ var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
144542
+ var APNS_MAX_PAYLOAD_BYTES = 4096;
144543
+ var JWT_TTL_SECONDS = 3e3;
144544
+ var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
144545
+ "BadDeviceToken",
144546
+ "DeviceTokenNotForTopic",
144547
+ "Unregistered",
144548
+ "ExpiredToken"
144549
+ ]);
144550
+ function base64url3(input) {
144551
+ return Buffer.from(input).toString("base64url");
144552
+ }
144553
+ function readApnsCredentialsFromEnv(env = process.env) {
144554
+ const key = env.APNS_KEY;
144555
+ if (!key || key.trim().length === 0) return null;
144556
+ const keyId = env.APNS_KEY_ID?.trim();
144557
+ const teamId = env.APNS_TEAM_ID?.trim();
144558
+ const bundleId = env.APNS_BUNDLE_ID?.trim();
144559
+ if (!keyId || !teamId || !bundleId) return null;
144560
+ const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
144561
+ return { key, keyId, teamId, bundleId, host };
144562
+ }
144563
+ function describeMissingApnsCredentials(env = process.env) {
144564
+ if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
144565
+ 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.";
144566
+ }
144567
+ const missing = [
144568
+ ["APNS_KEY_ID", env.APNS_KEY_ID],
144569
+ ["APNS_TEAM_ID", env.APNS_TEAM_ID],
144570
+ ["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
144571
+ ].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
144572
+ if (missing.length === 0) return null;
144573
+ 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.`;
144574
+ }
144575
+ var ApnsClient = class {
144576
+ constructor(creds) {
144577
+ this.creds = creds;
144578
+ }
144579
+ creds;
144580
+ session = null;
144581
+ cachedJwt = null;
144582
+ /**
144583
+ * The `apns-topic` for Live Activity pushes.
144584
+ *
144585
+ * The `.push-type.liveactivity` suffix is mandatory and is why the signing key
144586
+ * must be Team Scoped (All Topics) — a key scoped to the bundle id alone
144587
+ * cannot sign this topic.
144588
+ */
144589
+ get topic() {
144590
+ return `${this.creds.bundleId}.push-type.liveactivity`;
144591
+ }
144592
+ /**
144593
+ * Mint or reuse the provider JWT.
144594
+ *
144595
+ * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
144596
+ * token older than an hour, but minting one per request is wasteful and can
144597
+ * trip APNs' provider-token-update throttle.
144598
+ */
144599
+ getJwt(now = Date.now()) {
144600
+ const nowSeconds = Math.floor(now / 1e3);
144601
+ if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
144602
+ return this.cachedJwt.token;
144603
+ }
144604
+ const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
144605
+ const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
144606
+ const signingInput = `${header}.${payload}`;
144607
+ const signature = (0, import_node_crypto3.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
144608
+ const token = `${signingInput}.${base64url3(signature)}`;
144609
+ this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
144610
+ return token;
144611
+ }
144612
+ /**
144613
+ * Reuse one HTTP/2 session across sends.
144614
+ *
144615
+ * APNs expects a long-lived connection; a fresh TLS handshake per push is slow
144616
+ * and Apple treats connection churn as abuse.
144617
+ */
144618
+ getSession() {
144619
+ if (this.session && !this.session.closed && !this.session.destroyed) {
144620
+ return this.session;
144621
+ }
144622
+ const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
144623
+ session.on("error", (err) => {
144624
+ log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
144625
+ });
144626
+ this.session = session;
144627
+ return session;
144628
+ }
144629
+ /**
144630
+ * Send one push.
144631
+ *
144632
+ * Resolves with a result rather than rejecting on an APNs rejection: a
144633
+ * rejected push is an expected outcome the caller must act on (retire the
144634
+ * token), not an exception. Only a genuinely unexpected local failure throws,
144635
+ * and the caller logs it.
144636
+ */
144637
+ async send(args) {
144638
+ const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
144639
+ if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
144640
+ throw new Error(
144641
+ `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
144642
+ );
144643
+ }
144644
+ const session = this.getSession();
144645
+ const headers = {
144646
+ [import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
144647
+ [import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
144648
+ [import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
144649
+ "apns-push-type": "liveactivity",
144650
+ "apns-topic": this.topic,
144651
+ "apns-priority": String(args.priority ?? 10),
144652
+ ...args.expirationSeconds != null && {
144653
+ "apns-expiration": String(args.expirationSeconds)
144654
+ },
144655
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
144656
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
144657
+ };
144658
+ return new Promise((resolve4, reject) => {
144659
+ const req = session.request(headers);
144660
+ req.setTimeout(args.timeoutMs ?? 1e4, () => {
144661
+ req.close(import_node_http2.constants.NGHTTP2_CANCEL);
144662
+ resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
144663
+ });
144664
+ let status = 0;
144665
+ req.on("response", (resHeaders) => {
144666
+ status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
144667
+ });
144668
+ const chunks = [];
144669
+ req.on("data", (chunk) => chunks.push(chunk));
144670
+ req.on("error", reject);
144671
+ req.on("end", () => {
144672
+ const raw2 = Buffer.concat(chunks).toString("utf-8");
144673
+ let reason;
144674
+ if (raw2.length > 0) {
144675
+ try {
144676
+ reason = JSON.parse(raw2).reason;
144677
+ } catch {
144678
+ reason = raw2.slice(0, 200);
144679
+ }
144680
+ }
144681
+ resolve4({
144682
+ ok: status === 200,
144683
+ status,
144684
+ reason,
144685
+ tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
144686
+ });
144687
+ });
144688
+ req.end(body);
144689
+ });
144690
+ }
144691
+ /** Close the shared connection. Called on server shutdown. */
144692
+ close() {
144693
+ this.session?.close();
144694
+ this.session = null;
144695
+ }
144696
+ };
144697
+
144698
+ // src/services/push/liveActivityNotifier.ts
144699
+ init_logger();
144700
+
144701
+ // src/services/push/liveActivityContentState.ts
144702
+ var LAST_OUTPUT_MAX_LENGTH = 90;
144703
+ function toLiveActivityStatus(status) {
144704
+ return status === "running" || status === "waiting_input" ? status : null;
144705
+ }
144706
+ function truncateLastOutput(raw2) {
144707
+ const oneLine = raw2.replace(/\s+/g, " ").trim();
144708
+ return oneLine.length <= LAST_OUTPUT_MAX_LENGTH ? oneLine : oneLine.slice(0, LAST_OUTPUT_MAX_LENGTH);
144709
+ }
144710
+
144711
+ // src/services/push/liveActivityNotifier.ts
144712
+ var log4 = getLogger("live-activity");
144713
+ function contentStateForSession(args) {
144714
+ const status = toLiveActivityStatus(args.session.status);
144715
+ if (!status) return null;
144716
+ return {
144717
+ sessionId: args.session.id,
144718
+ serverId: args.serverId,
144719
+ projectName: args.session.projectName,
144720
+ status,
144721
+ startedAt: args.startedAtOverride ?? args.session.startedAt.getTime(),
144722
+ lastOutput: truncateLastOutput(args.session.lastOutput ?? ""),
144723
+ ...args.serverLabel != null && { serverLabel: args.serverLabel }
144724
+ };
144725
+ }
144726
+ var LiveActivityNotifier = class {
144727
+ constructor(sender, serverId, serverLabel) {
144728
+ this.sender = sender;
144729
+ this.serverId = serverId;
144730
+ this.serverLabel = serverLabel;
144731
+ }
144732
+ sender;
144733
+ serverId;
144734
+ serverLabel;
144735
+ /**
144736
+ * Last status pushed per session.
144737
+ *
144738
+ * Live Activity pushes are rate-limited by iOS and the surface only renders
144739
+ * `running` vs `waiting_input`, so re-pushing an unchanged status is pure
144740
+ * budget spend for no visible change. This is what makes the notifier
144741
+ * edge-triggered rather than level-triggered.
144742
+ */
144743
+ lastPushed = /* @__PURE__ */ new Map();
144744
+ /**
144745
+ * React to a session status change.
144746
+ *
144747
+ * Fire-and-forget by design: a push must never delay or fail a session
144748
+ * transition, so this returns a promise the caller may ignore and every error
144749
+ * is logged rather than propagated.
144750
+ */
144751
+ async onStatusChange(session) {
144752
+ const status = toLiveActivityStatus(session.status);
144753
+ try {
144754
+ if (!status) {
144755
+ await this.endFor(session);
144756
+ return;
144757
+ }
144758
+ if (this.lastPushed.get(session.id) === status) return;
144759
+ const contentState = contentStateForSession({
144760
+ session,
144761
+ serverId: this.serverId,
144762
+ serverLabel: this.serverLabel
144763
+ });
144764
+ if (!contentState) return;
144765
+ const outcome = await this.sender.send({
144766
+ sessionId: session.id,
144767
+ event: "update",
144768
+ contentState
144769
+ });
144770
+ this.lastPushed.set(session.id, status);
144771
+ if (outcome.attempted > 0) {
144772
+ log4.info("live_activity.updated", {
144773
+ event: "live_activity.updated",
144774
+ sessionId: session.id,
144775
+ status,
144776
+ ...outcome
144777
+ });
144778
+ }
144779
+ } catch (err) {
144780
+ log4.error("live_activity.notify_failed", {
144781
+ event: "live_activity.notify_failed",
144782
+ sessionId: session.id,
144783
+ status: session.status,
144784
+ err: String(err)
144785
+ });
144786
+ }
144787
+ }
144788
+ async endFor(session) {
144789
+ const lastStatus = this.lastPushed.get(session.id);
144790
+ this.lastPushed.delete(session.id);
144791
+ const contentState = contentStateForSession({
144792
+ session: {
144793
+ ...session,
144794
+ status: lastStatus === "waiting_input" ? "waiting_input" : "running"
144795
+ },
144796
+ serverId: this.serverId,
144797
+ serverLabel: this.serverLabel
144798
+ });
144799
+ if (!contentState) return;
144800
+ const outcome = await this.sender.end({ sessionId: session.id, contentState });
144801
+ if (outcome.attempted > 0) {
144802
+ log4.info("live_activity.ended", {
144803
+ event: "live_activity.ended",
144804
+ sessionId: session.id,
144805
+ ...outcome
144806
+ });
144807
+ }
144808
+ }
144809
+ /** Drop cached state for a session, so a resume re-pushes its first status. */
144810
+ forget(sessionId) {
144811
+ this.lastPushed.delete(sessionId);
144812
+ }
144813
+ };
144814
+
144815
+ // src/services/push/liveActivityRenewal.ts
144816
+ init_logger();
144817
+
144818
+ // src/services/push/liveActivitySender.ts
144819
+ init_logger();
144820
+ var log5 = getLogger("live-activity");
144821
+ var ACTIVITY_MAX_LIFETIME_MS = 8 * 60 * 60 * 1e3;
144822
+ function buildActivityKitPayload(args) {
144823
+ return {
144824
+ aps: {
144825
+ timestamp: Math.floor(args.now / 1e3),
144826
+ event: args.event,
144827
+ "content-state": args.contentState,
144828
+ ...args.staleDate != null && { "stale-date": Math.floor(args.staleDate / 1e3) },
144829
+ ...args.dismissalDate != null && {
144830
+ "dismissal-date": Math.floor(args.dismissalDate / 1e3)
144831
+ }
144832
+ }
144833
+ };
144834
+ }
144835
+ var LiveActivitySender = class {
144836
+ constructor(apns, repo) {
144837
+ this.apns = apns;
144838
+ this.repo = repo;
144839
+ }
144840
+ apns;
144841
+ repo;
144842
+ /**
144843
+ * Push to every live activity of a session.
144844
+ *
144845
+ * Sends are independent: one rejected token must not stop the others, because
144846
+ * a single dead device would otherwise silence every other device watching the
144847
+ * same session.
144848
+ */
144849
+ async send(args) {
144850
+ const now = args.now ?? Date.now();
144851
+ return this.sendToTokens({
144852
+ tokens: this.repo.listForSession("liveactivity_update", args.sessionId, now),
144853
+ sessionId: args.sessionId,
144854
+ event: args.event,
144855
+ contentState: args.contentState,
144856
+ now,
144857
+ priority: args.priority
144858
+ });
144859
+ }
144860
+ /**
144861
+ * Push to an explicit token list.
144862
+ *
144863
+ * Renewal needs this: a replacement activity does not exist yet, so it is
144864
+ * started via the app-wide push-to-start token rather than any per-session
144865
+ * lookup. Shares one fan-out body with `send()` so failure handling cannot
144866
+ * drift between the two paths.
144867
+ */
144868
+ async sendToTokens(args) {
144869
+ const now = args.now ?? Date.now();
144870
+ const tokens = args.tokens;
144871
+ const outcome = {
144872
+ attempted: tokens.length,
144873
+ succeeded: 0,
144874
+ retired: 0
144875
+ };
144876
+ if (tokens.length === 0) return outcome;
144877
+ const results = await Promise.all(
144878
+ tokens.map(
144879
+ (row) => this.sendToToken(row, args.event, args.contentState, now, args.priority, args.staleDate)
144880
+ )
144881
+ );
144882
+ for (const { row, result, error: error51 } of results) {
144883
+ if (error51) {
144884
+ log5.error("live_activity.send_failed", {
144885
+ event: "live_activity.send_failed",
144886
+ sessionId: args.sessionId,
144887
+ activityId: row.activity_id,
144888
+ apnsEvent: args.event,
144889
+ err: String(error51)
144890
+ });
144891
+ this.repo.recordFailure(row.token, "SendError", now);
144892
+ continue;
144893
+ }
144894
+ if (!result) continue;
144895
+ if (result.ok) {
144896
+ this.repo.recordSuccess(row.token, now);
144897
+ outcome.succeeded += 1;
144898
+ continue;
144899
+ }
144900
+ this.repo.recordFailure(row.token, result.reason ?? `HTTP_${result.status}`, now);
144901
+ if (result.tokenDead) {
144902
+ this.repo.expire(row.token, now);
144903
+ outcome.retired += 1;
144904
+ }
144905
+ log5.warn("live_activity.send_rejected", {
144906
+ event: "live_activity.send_rejected",
144907
+ sessionId: args.sessionId,
144908
+ activityId: row.activity_id,
144909
+ apnsEvent: args.event,
144910
+ status: result.status,
144911
+ reason: result.reason,
144912
+ tokenDead: result.tokenDead
144913
+ });
144914
+ }
144915
+ return outcome;
144916
+ }
144917
+ /**
144918
+ * End every live activity for a session and stop tracking them.
144919
+ *
144920
+ * Expiring locally is what stops the renewal sweep from later resurrecting an
144921
+ * activity for a session that has already finished.
144922
+ */
144923
+ async end(args) {
144924
+ const now = args.now ?? Date.now();
144925
+ const outcome = await this.send({
144926
+ sessionId: args.sessionId,
144927
+ event: "end",
144928
+ contentState: args.contentState,
144929
+ now
144930
+ });
144931
+ this.repo.expireSessionActivities(args.sessionId, now);
144932
+ return outcome;
144933
+ }
144934
+ async sendToToken(row, event, contentState, now, priority, staleDateOverride) {
144935
+ const staleDate = event === "update" ? staleDateOverride ?? row.stale_date ?? contentState.startedAt + ACTIVITY_MAX_LIFETIME_MS : null;
144936
+ try {
144937
+ const result = await this.apns.send({
144938
+ deviceToken: row.token,
144939
+ payload: buildActivityKitPayload({ event, contentState, now, staleDate }),
144940
+ priority
144941
+ });
144942
+ return { row, result };
144943
+ } catch (error51) {
144944
+ return { row, error: error51 };
144945
+ }
144946
+ }
144947
+ };
144948
+
144949
+ // src/services/push/liveActivityRenewal.ts
144950
+ var log6 = getLogger("live-activity");
144951
+ var RENEWAL_LEAD_MS = 30 * 60 * 1e3;
144952
+ var MAX_TIMER_MS = 60 * 60 * 1e3;
144953
+ function renewalDueAt(row) {
144954
+ return row.stale_date == null ? null : row.stale_date - RENEWAL_LEAD_MS;
144955
+ }
144956
+ var LiveActivityRenewalScheduler = class {
144957
+ constructor(deps) {
144958
+ this.deps = deps;
144959
+ this.now = deps.now ?? (() => Date.now());
144960
+ }
144961
+ deps;
144962
+ timer = null;
144963
+ stopped = false;
144964
+ now;
144965
+ /**
144966
+ * Arm the scheduler from persisted state.
144967
+ *
144968
+ * Called on boot, which is what makes a renewal survive a restart: the
144969
+ * deadlines were never in memory to begin with.
144970
+ */
144971
+ start() {
144972
+ this.stopped = false;
144973
+ void this.tick();
144974
+ }
144975
+ stop() {
144976
+ this.stopped = true;
144977
+ if (this.timer) {
144978
+ clearTimeout(this.timer);
144979
+ this.timer = null;
144980
+ }
144981
+ }
144982
+ /**
144983
+ * Renew everything due, then sleep until the next deadline.
144984
+ *
144985
+ * Re-reads from the DB every tick rather than caching a schedule in memory, so
144986
+ * an activity registered after boot is picked up without re-arming anything.
144987
+ */
144988
+ async tick() {
144989
+ if (this.stopped) return;
144990
+ const now = this.now();
144991
+ try {
144992
+ for (const row of this.deps.repo.listRenewable()) {
144993
+ const dueAt = renewalDueAt(row);
144994
+ if (dueAt == null || dueAt > now) continue;
144995
+ await this.renew(row, now);
144996
+ }
144997
+ } catch (err) {
144998
+ log6.error("live_activity.renewal_sweep_failed", {
144999
+ event: "live_activity.renewal_sweep_failed",
145000
+ err: String(err)
145001
+ });
145002
+ }
145003
+ this.scheduleNext();
145004
+ }
145005
+ scheduleNext() {
145006
+ if (this.stopped) return;
145007
+ const now = this.now();
145008
+ const pending = this.deps.repo.listRenewable().map(renewalDueAt).filter((d) => d != null);
145009
+ const nextDue = pending.length > 0 ? Math.min(...pending) : now + MAX_TIMER_MS;
145010
+ const delay = Math.min(Math.max(nextDue - now, 0), MAX_TIMER_MS);
145011
+ this.timer = setTimeout(() => void this.tick(), delay);
145012
+ this.timer.unref?.();
145013
+ }
145014
+ /**
145015
+ * Renew one activity.
145016
+ *
145017
+ * Claims first: `claimRenewal()` succeeds exactly once per row, so a timer
145018
+ * re-armed after a restart mid-window cannot send a second time.
145019
+ */
145020
+ async renew(row, now) {
145021
+ if (!row.session_id) return;
145022
+ const session = this.deps.sessionStore.getManaged(row.session_id);
145023
+ const status = session ? toLiveActivityStatus(session.status) : null;
145024
+ if (!session || !status) {
145025
+ this.deps.repo.claimRenewal(row.token, now);
145026
+ this.deps.repo.expire(row.token, now);
145027
+ log6.info("live_activity.renewal_skipped", {
145028
+ event: "live_activity.renewal_skipped",
145029
+ sessionId: row.session_id,
145030
+ activityId: row.activity_id,
145031
+ reason: session ? `status_${session.status}` : "session_gone"
145032
+ });
145033
+ return;
145034
+ }
145035
+ if (!this.deps.repo.claimRenewal(row.token, now)) {
145036
+ return;
145037
+ }
145038
+ const startedAt = row.started_at ?? session.startedAt.getTime();
145039
+ const contentState = {
145040
+ sessionId: session.id,
145041
+ serverId: this.deps.serverId,
145042
+ projectName: session.projectName,
145043
+ status,
145044
+ startedAt,
145045
+ lastOutput: session.lastOutput ?? "",
145046
+ ...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
145047
+ };
145048
+ try {
145049
+ await this.deps.sender.send({
145050
+ sessionId: session.id,
145051
+ event: "end",
145052
+ contentState: { ...contentState, lastOutput: truncateLastOutput(contentState.lastOutput) },
145053
+ now
145054
+ });
145055
+ this.deps.repo.expire(row.token, now);
145056
+ const started = await this.startReplacement({
145057
+ sessionId: session.id,
145058
+ startedAt,
145059
+ now
145060
+ });
145061
+ log6.info("live_activity.renewed", {
145062
+ event: "live_activity.renewed",
145063
+ sessionId: session.id,
145064
+ activityId: row.activity_id,
145065
+ // Logged because a regression here is invisible on the server and only
145066
+ // shows up as a reset timer on someone's Lock Screen.
145067
+ startedAt,
145068
+ replacementRequested: started
145069
+ });
145070
+ } catch (err) {
145071
+ log6.error("live_activity.renewal_failed", {
145072
+ event: "live_activity.renewal_failed",
145073
+ sessionId: session.id,
145074
+ activityId: row.activity_id,
145075
+ err: String(err)
145076
+ });
145077
+ }
145078
+ }
145079
+ /**
145080
+ * Ask the device to start a replacement activity.
145081
+ *
145082
+ * Uses the app-wide push-to-start token, because the replacement does not
145083
+ * exist yet and therefore has no per-activity token. Returns false when the
145084
+ * device never registered one, which is not an error: the app simply cannot be
145085
+ * asked to start an activity remotely, and the next foreground WS update
145086
+ * recreates it.
145087
+ */
145088
+ async startReplacement(args) {
145089
+ const starters = this.deps.repo.listByKind("liveactivity_start", args.now);
145090
+ if (starters.length === 0) return false;
145091
+ const session = this.deps.sessionStore.getManaged(args.sessionId);
145092
+ const status = session ? toLiveActivityStatus(session.status) : null;
145093
+ if (!session || !status) return false;
145094
+ await this.deps.sender.sendToTokens({
145095
+ tokens: starters,
145096
+ event: "update",
145097
+ sessionId: args.sessionId,
145098
+ contentState: {
145099
+ sessionId: session.id,
145100
+ serverId: this.deps.serverId,
145101
+ projectName: session.projectName,
145102
+ status,
145103
+ // Carried through unchanged — the whole point of the renewal.
145104
+ startedAt: args.startedAt,
145105
+ lastOutput: truncateLastOutput(session.lastOutput ?? ""),
145106
+ ...this.deps.serverLabel != null && { serverLabel: this.deps.serverLabel }
145107
+ },
145108
+ now: args.now,
145109
+ staleDate: args.startedAt + ACTIVITY_MAX_LIFETIME_MS
145110
+ });
145111
+ return true;
145112
+ }
145113
+ };
145114
+
144267
145115
  // src/services/questions/parseStatusLine.ts
144268
145116
  var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
144269
145117
  var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
@@ -144972,13 +145820,13 @@ function hashPrefix(text) {
144972
145820
  }
144973
145821
 
144974
145822
  // src/utils/conversationEtag.ts
144975
- var import_node_crypto3 = require("crypto");
145823
+ var import_node_crypto4 = require("crypto");
144976
145824
  function computeConversationEtag({
144977
145825
  filePath,
144978
145826
  messageCount,
144979
145827
  timestamp: timestamp2
144980
145828
  }) {
144981
- const digest = (0, import_node_crypto3.createHash)("sha1").update(`${filePath}:${messageCount}:${timestamp2}`).digest("hex").slice(0, 16);
145829
+ const digest = (0, import_node_crypto4.createHash)("sha1").update(`${filePath}:${messageCount}:${timestamp2}`).digest("hex").slice(0, 16);
144982
145830
  return `"${digest}"`;
144983
145831
  }
144984
145832
 
@@ -145241,6 +146089,12 @@ var StreamerServer = class {
145241
146089
  sessionInputAttempts = /* @__PURE__ */ new Map();
145242
146090
  ptyGracePeriodMs;
145243
146091
  defaultSystemPrompt;
146092
+ // Resolved once at boot; see src/feature-flags.ts. Total map — every registry
146093
+ // id is present, so indexing it never yields undefined.
146094
+ featureFlags;
146095
+ // Derived from featureFlags.codexSystemPrompt. Kept as its own field so the
146096
+ // read site in startFresh() is unchanged.
146097
+ codexSystemPromptEnabled;
145244
146098
  defaultPermissionMode;
145245
146099
  defaultModel;
145246
146100
  defaultEffort;
@@ -145296,6 +146150,12 @@ var StreamerServer = class {
145296
146150
  // Paired-device registry (C5). Null when the cache DB failed to open — auth
145297
146151
  // then falls back to the shared API key alone, which is the pre-C5 behaviour.
145298
146152
  devicesRepo = null;
146153
+ // Live Activity push (Feature 12). Null when APNS_KEY is unset — the ordinary
146154
+ // case on a dev machine and in CI, where the feature is simply off. Missing an
146155
+ // optional push credential must never stop the server from booting.
146156
+ apnsClient = null;
146157
+ liveActivityNotifier = null;
146158
+ liveActivityRenewal = null;
145299
146159
  discoveryCache = null;
145300
146160
  cacheDir;
145301
146161
  tailSize;
@@ -145331,6 +146191,11 @@ var StreamerServer = class {
145331
146191
  this.codexRoots = config2.codexRoots ?? [(0, import_path29.join)((0, import_os12.homedir)(), ".codex", "sessions")];
145332
146192
  this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
145333
146193
  this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
146194
+ this.featureFlags = resolveFeatureFlags({ cli: config2.featureFlags, yaml: loadFeatureFlags() });
146195
+ if (config2.codexSystemPromptEnabled !== void 0) {
146196
+ this.featureFlags.codexSystemPrompt = config2.codexSystemPromptEnabled;
146197
+ }
146198
+ this.codexSystemPromptEnabled = this.featureFlags.codexSystemPrompt;
145334
146199
  this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
145335
146200
  this.defaultModel = config2.defaultModel ?? "sonnet";
145336
146201
  this.defaultEffort = config2.defaultEffort ?? "low";
@@ -145346,6 +146211,13 @@ var StreamerServer = class {
145346
146211
  }, this.directoryDebounceMs);
145347
146212
  this.includeAgents = parseIncludeAgentsEnv(process.env.THREADBASE_INCLUDE_AGENTS);
145348
146213
  this.agentEntrypoints = parseAgentEntrypointsEnv(process.env.THREADBASE_AGENT_ENTRYPOINTS);
146214
+ const enabledFlags = nonDefaultFeatureFlags(this.featureFlags);
146215
+ if (enabledFlags.length > 0) {
146216
+ this.log.info(`Feature flags active: ${enabledFlags.join(", ")}`, {
146217
+ event: "config.feature_flags_active",
146218
+ flags: enabledFlags
146219
+ });
146220
+ }
145349
146221
  const rawRoot = process.env.THREADBASE_BROWSE_ROOT ?? loadBrowseRoot() ?? config2.browseRoot;
145350
146222
  if (rawRoot) {
145351
146223
  (0, import_promises16.realpath)(rawRoot).then((resolved) => {
@@ -145539,6 +146411,7 @@ var StreamerServer = class {
145539
146411
  if (resp) {
145540
146412
  this.wsHub.broadcast({ type: "session_update", session: resp });
145541
146413
  }
146414
+ void this.liveActivityNotifier?.onStatusChange(session);
145542
146415
  this.sessionStatusBus.emit(`status:${session.id}`, session.status);
145543
146416
  }
145544
146417
  });
@@ -145573,6 +146446,7 @@ var StreamerServer = class {
145573
146446
  logMenubarRequests: this.logMenubarRequests,
145574
146447
  rotateApiKey: () => this.rotateApiKey(),
145575
146448
  claudeFlagsConfig: () => this.getClaudeFlagsConfig(),
146449
+ featureFlagsConfig: () => this.getFeatureFlagsConfig(),
145576
146450
  setClaudeFlagsConfig: (values, extraArgs) => this.setClaudeFlagsConfig(values, extraArgs),
145577
146451
  publicUrl: this.publicUrl,
145578
146452
  browseRoot: this.browseRoot,
@@ -145804,6 +146678,41 @@ var StreamerServer = class {
145804
146678
  }
145805
146679
  this.ptyGraceDeferCounts.delete(sessionId);
145806
146680
  }
146681
+ /**
146682
+ * Bring up Live Activity push, if credentials are present (Feature 12).
146683
+ *
146684
+ * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this
146685
+ * logs once at info and leaves the feature off rather than failing: the server
146686
+ * must not refuse to boot over a missing optional push credential.
146687
+ *
146688
+ * The key is read from the environment as PEM contents and never from a path
146689
+ * on disk; neither it nor any device token is ever logged.
146690
+ */
146691
+ initLiveActivityPush(pushRepo) {
146692
+ const creds = readApnsCredentialsFromEnv();
146693
+ if (!creds) {
146694
+ const why = describeMissingApnsCredentials();
146695
+ if (why) this.log.info(why, { event: "live_activity.disabled" });
146696
+ return;
146697
+ }
146698
+ this.apnsClient = new ApnsClient(creds);
146699
+ const sender = new LiveActivitySender(this.apnsClient, pushRepo);
146700
+ const serverId = process.env.THREADBASE_INSTANCE_ID ?? (0, import_os12.hostname)();
146701
+ this.liveActivityNotifier = new LiveActivityNotifier(sender, serverId, (0, import_os12.hostname)());
146702
+ this.liveActivityRenewal = new LiveActivityRenewalScheduler({
146703
+ repo: pushRepo,
146704
+ sender,
146705
+ sessionStore: this.sessionStore,
146706
+ serverId,
146707
+ serverLabel: (0, import_os12.hostname)()
146708
+ });
146709
+ this.liveActivityRenewal.start();
146710
+ this.log.info("Live Activity push enabled", {
146711
+ event: "live_activity.enabled",
146712
+ host: creds.host,
146713
+ topic: `${creds.bundleId}.push-type.liveactivity`
146714
+ });
146715
+ }
145807
146716
  /**
145808
146717
  * Classify sessions left behind by previous streamer runs (C1 Phase 3a).
145809
146718
  *
@@ -146113,6 +147022,7 @@ var StreamerServer = class {
146113
147022
  this.cacheMetadataRepo = new CacheMetadataRepository(db);
146114
147023
  this.pushRepo = new PushRepository(db);
146115
147024
  this.devicesRepo = new DevicesRepository(db);
147025
+ this.initLiveActivityPush(this.pushRepo);
146116
147026
  this.cacheMonitor = new CacheIntegrityMonitor(
146117
147027
  this.cache,
146118
147028
  this.wsHub,
@@ -146347,6 +147257,8 @@ var StreamerServer = class {
146347
147257
  this.externalTails.clear();
146348
147258
  this.wsHub.dispose();
146349
147259
  this.pairTokens.dispose();
147260
+ this.liveActivityRenewal?.stop();
147261
+ this.apnsClient?.close();
146350
147262
  if (this.dbPool) {
146351
147263
  await this.dbPool.end();
146352
147264
  }
@@ -146418,7 +147330,6 @@ var StreamerServer = class {
146418
147330
  json2(res, 400, { error: message });
146419
147331
  return;
146420
147332
  }
146421
- const { hostname: hostname4 } = require("os");
146422
147333
  const ts2 = (/* @__PURE__ */ new Date()).toISOString();
146423
147334
  this.log.info(`[pair] token exchanged from ${ip} at ${ts2}`, {
146424
147335
  event: "pair.token_exchanged",
@@ -146441,7 +147352,7 @@ var StreamerServer = class {
146441
147352
  nonce: sealed.nonce,
146442
147353
  ephemeralPublicKey: sealed.ephemeralPublicKey,
146443
147354
  publicUrl: this.publicUrl,
146444
- machineName: hostname4(),
147355
+ machineName: (0, import_os12.hostname)(),
146445
147356
  ...device && {
146446
147357
  deviceId: device.deviceId,
146447
147358
  deviceToken: device.deviceToken,
@@ -146463,6 +147374,16 @@ var StreamerServer = class {
146463
147374
  });
146464
147375
  return { newKey, persisted };
146465
147376
  }
147377
+ /**
147378
+ * The registry ships with the values so a client renders the list from one
147379
+ * round-trip, same as getClaudeFlagsConfig().
147380
+ *
147381
+ * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and
147382
+ * the absence of that field is the signal that this endpoint is read-only.
147383
+ */
147384
+ getFeatureFlagsConfig() {
147385
+ return { registry: FEATURE_FLAGS, values: this.featureFlags };
147386
+ }
146466
147387
  getClaudeFlagsConfig() {
146467
147388
  return {
146468
147389
  registry: CLAUDE_FLAGS,
@@ -148175,12 +149096,13 @@ var StreamerServer = class {
148175
149096
  BROWSE_SYSTEM_PROMPT(this.browseRoot),
148176
149097
  typeof clientPrompt === "string" ? clientPrompt : null
148177
149098
  ].filter(Boolean);
149099
+ const includeSystemPrompt = provider !== CODEX_CLI_PROVIDER2 || this.codexSystemPromptEnabled;
148178
149100
  try {
148179
149101
  const session = await this.ptyManager.startFresh({
148180
149102
  provider,
148181
149103
  projectPath: resolvedPath,
148182
149104
  projectName: body.projectName,
148183
- systemPrompt: systemPromptParts.join("\n"),
149105
+ ...includeSystemPrompt && { systemPrompt: systemPromptParts.join("\n") },
148184
149106
  permissionMode: this.defaultPermissionMode,
148185
149107
  claudeFlags: this.claudeFlags,
148186
149108
  claudeExtraArgs: this.claudeExtraArgs,
@@ -148886,7 +149808,7 @@ function isBrewInstall(scriptPath = process.argv[1] ?? "") {
148886
149808
  }
148887
149809
 
148888
149810
  // src/updater/download.ts
148889
- var import_node_crypto4 = require("crypto");
149811
+ var import_node_crypto5 = require("crypto");
148890
149812
  var import_node_fs9 = require("fs");
148891
149813
  var import_node_path10 = require("path");
148892
149814
  var import_promises17 = require("stream/promises");
@@ -148953,7 +149875,7 @@ async function downloadAndVerify(opts) {
148953
149875
  if (!res.ok || !res.body) {
148954
149876
  throw new Error(`Failed to download ${artifact.filename}: ${res.status} ${res.statusText}`);
148955
149877
  }
148956
- const hash2 = (0, import_node_crypto4.createHash)("sha256");
149878
+ const hash2 = (0, import_node_crypto5.createHash)("sha256");
148957
149879
  const out = (0, import_node_fs9.createWriteStream)(targetPath);
148958
149880
  let bytes = 0;
148959
149881
  const measured = new TransformStream({
@@ -149258,7 +150180,7 @@ var import_node_path17 = require("path");
149258
150180
  var import_path32 = __toESM(require("path"), 1);
149259
150181
  var import_node_fs13 = __toESM(require("fs"), 1);
149260
150182
  var import_node_assert = __toESM(require("assert"), 1);
149261
- var import_node_crypto5 = require("crypto");
150183
+ var import_node_crypto6 = require("crypto");
149262
150184
  var import_node_fs14 = __toESM(require("fs"), 1);
149263
150185
  var import_node_path18 = __toESM(require("path"), 1);
149264
150186
  var import_fs33 = __toESM(require("fs"), 1);
@@ -151666,7 +152588,7 @@ var Te = fo === "win32";
151666
152588
  var mo = 1024;
151667
152589
  var uo = (s3, t) => {
151668
152590
  if (!Te) return import_node_fs14.default.unlink(s3, t);
151669
- let e = s3 + ".DELETE." + (0, import_node_crypto5.randomBytes)(16).toString("hex");
152591
+ let e = s3 + ".DELETE." + (0, import_node_crypto6.randomBytes)(16).toString("hex");
151670
152592
  import_node_fs14.default.rename(s3, e, (i) => {
151671
152593
  if (i) return t(i);
151672
152594
  import_node_fs14.default.unlink(e, t);
@@ -151674,7 +152596,7 @@ var uo = (s3, t) => {
151674
152596
  };
151675
152597
  var po = (s3) => {
151676
152598
  if (!Te) return import_node_fs14.default.unlinkSync(s3);
151677
- let t = s3 + ".DELETE." + (0, import_node_crypto5.randomBytes)(16).toString("hex");
152599
+ let t = s3 + ".DELETE." + (0, import_node_crypto6.randomBytes)(16).toString("hex");
151678
152600
  import_node_fs14.default.renameSync(s3, t), import_node_fs14.default.unlinkSync(t);
151679
152601
  };
151680
152602
  var vr = (s3, t, e) => s3 !== void 0 && s3 === s3 >>> 0 ? s3 : t !== void 0 && t === t >>> 0 ? t : e;
@@ -152331,7 +153253,7 @@ init_launchd();
152331
153253
  init_marker();
152332
153254
  init_platform();
152333
153255
  init_logger();
152334
- var log4 = getLogger("prod");
153256
+ var log8 = getLogger("prod");
152335
153257
  function clearSupervisorLogs() {
152336
153258
  let paths;
152337
153259
  try {
@@ -152488,12 +153410,12 @@ function registerProdCommands(program3) {
152488
153410
  prod.command("start").description("Restore prod after a user-held suspension").option("--clear-logs", "Clear existing stdout + stderr logs", false).action(async (opts) => {
152489
153411
  if (opts.clearLogs === true) clearSupervisorLogs();
152490
153412
  const r = await runProdStart();
152491
- log4.info(r.message, void 0, "console");
153413
+ log8.info(r.message, void 0, "console");
152492
153414
  if (!r.ok) process.exitCode = 1;
152493
153415
  });
152494
153416
  prod.command("stop").description("Unload the launchd agent (prod will not auto-restart)").action(async () => {
152495
153417
  const r = await runProdStop();
152496
- log4.info(r.message, void 0, "console");
153418
+ log8.info(r.message, void 0, "console");
152497
153419
  });
152498
153420
  prod.command("status").description("Report whether prod is supervised, suspended, or down").action(async () => {
152499
153421
  const s3 = await runProdStatus();
@@ -152502,7 +153424,7 @@ function registerProdCommands(program3) {
152502
153424
  `pid: ${s3.agentPid ?? "(none)"}`,
152503
153425
  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"
152504
153426
  ];
152505
- log4.info(parts.join("\n "), void 0, "console");
153427
+ log8.info(parts.join("\n "), void 0, "console");
152506
153428
  });
152507
153429
  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) => {
152508
153430
  const sup = getSupervisor();
@@ -152511,17 +153433,17 @@ function registerProdCommands(program3) {
152511
153433
  sup.bootoutAgent();
152512
153434
  sup.bootstrapAgent(specPath, { afterBootout: true });
152513
153435
  const what = process.platform === "darwin" ? `agent restarted from ${specPath}` : `task '${TASK_NAME}' restarted`;
152514
- log4.info(what, void 0, "console");
153436
+ log8.info(what, void 0, "console");
152515
153437
  });
152516
153438
  prod.command("doctor").description("Detect & repair stale markers, missing agent, plist drift").option("--fix", "Apply repairs (default is dry-run)", false).action(async (opts) => {
152517
153439
  const r = await runProdDoctor({ fix: opts.fix === true });
152518
- log4.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
152519
- for (const f2 of r.findings) log4.info(` - ${f2}`, void 0, "console");
153440
+ log8.info(`findings: ${r.findings.length === 0 ? "(none)" : ""}`, void 0, "console");
153441
+ for (const f2 of r.findings) log8.info(` - ${f2}`, void 0, "console");
152520
153442
  if (r.repairs.length) {
152521
- log4.info(`repairs:`, void 0, "console");
152522
- for (const fix of r.repairs) log4.info(` - ${fix}`, void 0, "console");
153443
+ log8.info(`repairs:`, void 0, "console");
153444
+ for (const fix of r.repairs) log8.info(` - ${fix}`, void 0, "console");
152523
153445
  } else if (!opts.fix && r.findings.length > 0) {
152524
- log4.info(`(re-run with --fix to apply repairs)`, void 0, "console");
153446
+ log8.info(`(re-run with --fix to apply repairs)`, void 0, "console");
152525
153447
  }
152526
153448
  });
152527
153449
  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) => {
@@ -152532,14 +153454,14 @@ function registerProdCommands(program3) {
152532
153454
  errorsOnly: opts.errorsOnly === true,
152533
153455
  clear: opts.clear === true
152534
153456
  });
152535
- if (r.message) log4.info(r.message, void 0, "console");
153457
+ if (r.message) log8.info(r.message, void 0, "console");
152536
153458
  if (!r.ok) process.exitCode = 1;
152537
153459
  });
152538
153460
  program3.addCommand(prod);
152539
153461
  }
152540
153462
 
152541
153463
  // cli/index.ts
152542
- var log7 = getLogger("cli");
153464
+ var log11 = getLogger("cli");
152543
153465
  var program2 = new Command();
152544
153466
  program2.name("threadbase-streamer").description("PTY session management, WebSocket streaming, and REST API server for Claude Code").version(getVersion());
152545
153467
  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(
@@ -152561,6 +153483,10 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152561
153483
  "--claude-flag <id=value>",
152562
153484
  "Allowlisted Claude CLI flag applied to every spawned session, e.g. --claude-flag permissionMode=bypassPermissions. Repeatable; repeat the same id to build a list (--claude-flag addDir=/a --claude-flag addDir=/b). Overrides claude_flags: in ~/.threadbase/server.yaml and makes the value non-persistable.",
152563
153485
  (value, previous = []) => [...previous, value]
153486
+ ).option(
153487
+ "--feature <id=bool>",
153488
+ "Enable or disable a server feature flag, e.g. --feature codexSystemPrompt=true. Repeatable. Overridden by the flag's THREADBASE_FEATURE_* env var; overrides feature_flags: in ~/.threadbase/server.yaml.",
153489
+ (value, previous = []) => [...previous, value]
152564
153490
  ).option(
152565
153491
  "--claude-extra-args <args>",
152566
153492
  "Free-text argv appended verbatim to every spawned Claude session, after the allowlisted flags. Unvalidated escape hatch."
@@ -152577,14 +153503,14 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152577
153503
  const { checkSqliteAbi: checkSqliteAbi2 } = await Promise.resolve().then(() => (init_check_sqlite_abi(), check_sqlite_abi_exports));
152578
153504
  checkSqliteAbi2();
152579
153505
  } catch (err) {
152580
- log7.error(err instanceof Error ? err.message : String(err), void 0, "console");
153506
+ log11.error(err instanceof Error ? err.message : String(err), void 0, "console");
152581
153507
  process.exit(1);
152582
153508
  }
152583
153509
  if (opts.multiAgentFlow) {
152584
153510
  process.env.MULTI_AGENT_FLOW = "true";
152585
153511
  }
152586
153512
  if (opts.defaultPermissionMode !== void 0 && !isPermissionMode(opts.defaultPermissionMode)) {
152587
- log7.error(
153513
+ log11.error(
152588
153514
  `Invalid --default-permission-mode: ${opts.defaultPermissionMode} (expected one of ${PERMISSION_MODES.join(", ")})`,
152589
153515
  void 0,
152590
153516
  "console"
@@ -152600,7 +153526,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152600
153526
  const value = eq === -1 ? true : entry.slice(eq + 1);
152601
153527
  const def = findFlag(id);
152602
153528
  if (!def) {
152603
- log7.error(
153529
+ log11.error(
152604
153530
  `Invalid --claude-flag id: ${id} (expected one of ${CLAUDE_FLAGS.map((f2) => f2.id).join(", ")})`,
152605
153531
  void 0,
152606
153532
  "console"
@@ -152617,14 +153543,23 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152617
153543
  claudeFlags = validateFlagValues(raw2);
152618
153544
  for (const id of Object.keys(raw2)) {
152619
153545
  if (!(id in claudeFlags)) {
152620
- log7.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
153546
+ log11.error(`Invalid value for --claude-flag ${id}`, void 0, "console");
152621
153547
  process.exit(1);
152622
153548
  }
152623
153549
  }
152624
153550
  }
153551
+ let featureFlags;
153552
+ if (Array.isArray(opts.feature) && opts.feature.length > 0) {
153553
+ const parsed = parseFeatureFlagArgs(opts.feature);
153554
+ if (parsed.errors.length > 0) {
153555
+ for (const error51 of parsed.errors) log11.error(`--feature: ${error51}`, void 0, "console");
153556
+ process.exit(1);
153557
+ }
153558
+ featureFlags = parsed.values;
153559
+ }
152625
153560
  const validEfforts = ["low", "medium", "high", "xhigh", "max"];
152626
153561
  if (opts.defaultEffort !== void 0 && !validEfforts.includes(opts.defaultEffort)) {
152627
- log7.error(
153562
+ log11.error(
152628
153563
  `Invalid --default-effort: ${opts.defaultEffort} (expected one of ${validEfforts.join(", ")})`,
152629
153564
  void 0,
152630
153565
  "console"
@@ -152635,7 +153570,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152635
153570
  if (opts.ptyGracePeriodMs !== void 0) {
152636
153571
  const parsed = Number(opts.ptyGracePeriodMs);
152637
153572
  if (!Number.isInteger(parsed) || parsed < 0) {
152638
- log7.error(
153573
+ log11.error(
152639
153574
  `Invalid --pty-grace-period-ms: ${opts.ptyGracePeriodMs} (expected a non-negative integer; 0 disables auto-hold)`,
152640
153575
  void 0,
152641
153576
  "console"
@@ -152655,7 +153590,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152655
153590
  const { detectConflictingAgents: detectConflictingAgents2, formatConflictMessage: formatConflictMessage2 } = await Promise.resolve().then(() => (init_conflict_check(), conflict_check_exports));
152656
153591
  const conflicts = detectConflictingAgents2();
152657
153592
  if (conflicts.length > 0) {
152658
- log7.warn(formatConflictMessage2(conflicts), void 0, "console");
153593
+ log11.warn(formatConflictMessage2(conflicts), void 0, "console");
152659
153594
  }
152660
153595
  }
152661
153596
  let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
@@ -152701,20 +153636,21 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152701
153636
  defaultEffort: opts.defaultEffort,
152702
153637
  ptyGracePeriodMs,
152703
153638
  claudeFlags,
153639
+ featureFlags,
152704
153640
  claudeExtraArgs: opts.claudeExtraArgs
152705
153641
  });
152706
153642
  await server.listen(resolvedPort);
152707
153643
  {
152708
153644
  const v2 = getVersion();
152709
- log7.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
153645
+ log11.info(`Threadbase Streamer v${v2}`, { version: v2, port: resolvedPort });
152710
153646
  }
152711
- log7.info(`Listening on http://localhost:${resolvedPort}`, {
153647
+ log11.info(`Listening on http://localhost:${resolvedPort}`, {
152712
153648
  url: `http://localhost:${resolvedPort}`
152713
153649
  });
152714
- log7.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
153650
+ log11.info(`WebSocket at ws://localhost:${resolvedPort}/ws`, {
152715
153651
  wsUrl: `ws://localhost:${resolvedPort}/ws`
152716
153652
  });
152717
- log7.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
153653
+ log11.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
152718
153654
  try {
152719
153655
  await printServerBanner({
152720
153656
  port: resolvedPort,
@@ -152724,15 +153660,15 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152724
153660
  });
152725
153661
  } catch (err) {
152726
153662
  const message = err instanceof Error ? err.message : String(err);
152727
- log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
152728
- log7.info(
153663
+ log11.warn(`(skipped pairing QR: ${message})`, { reason: message });
153664
+ log11.info(
152729
153665
  printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
152730
153666
  void 0,
152731
153667
  "console"
152732
153668
  );
152733
153669
  }
152734
153670
  const shutdown = async () => {
152735
- log7.info("Shutting down...");
153671
+ log11.info("Shutting down...");
152736
153672
  await server.close();
152737
153673
  process.exit(0);
152738
153674
  };
@@ -152740,7 +153676,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152740
153676
  process.on("SIGINT", shutdown);
152741
153677
  process.on("SIGTERM", shutdown);
152742
153678
  process.on("uncaughtException", (err) => {
152743
- log7.error(`uncaught: ${err.message}`, {
153679
+ log11.error(`uncaught: ${err.message}`, {
152744
153680
  error: err.message,
152745
153681
  stack: err.stack,
152746
153682
  event: "process.uncaught"
@@ -152749,7 +153685,7 @@ program2.command("serve").description("Start the streamer server").option("-p, -
152749
153685
  });
152750
153686
  process.on("unhandledRejection", (reason) => {
152751
153687
  const msg = reason instanceof Error ? reason.message : String(reason);
152752
- log7.error(`unhandled rejection: ${msg}`, {
153688
+ log11.error(`unhandled rejection: ${msg}`, {
152753
153689
  error: msg,
152754
153690
  event: "process.unhandled_rejection"
152755
153691
  });
@@ -152774,10 +153710,10 @@ program2.command("cache").description("Manage the local SQLite conversation cach
152774
153710
  const f2 = dbPath + suffix;
152775
153711
  if (existsSync15(f2)) {
152776
153712
  rmSync6(f2);
152777
- log7.info(`Deleted ${f2}`, { path: f2 }, "console");
153713
+ log11.info(`Deleted ${f2}`, { path: f2 }, "console");
152778
153714
  }
152779
153715
  }
152780
- log7.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
153716
+ log11.info("Cache cleared. Restart the server to rebuild.", void 0, "console");
152781
153717
  })
152782
153718
  );
152783
153719
  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) => {
@@ -152802,7 +153738,7 @@ program2.command("set-key [key]").description("Set the streamer API key in ~/.th
152802
153738
  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) => {
152803
153739
  const cfg = loadUpdateConfig();
152804
153740
  if (!cfg) {
152805
- log7.warn(
153741
+ log11.warn(
152806
153742
  `No update config found at ${DEFAULT_CONFIG_PATH}. Create one with at least 'github_repo: owner/name' to enable updates.`,
152807
153743
  void 0,
152808
153744
  "console"
@@ -152821,11 +153757,11 @@ program2.command("update").description("Check for streamer updates from GitHub R
152821
153757
  appendUpdateLog(
152822
153758
  `[check] current=${result2.current} latest=${result2.latest ?? "none"} status=${result2.reason}`
152823
153759
  );
152824
- log7.info(`Current : ${result2.current}`, void 0, "console");
152825
- log7.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
152826
- log7.info(`Channel : ${cfg.channel}`, void 0, "console");
152827
- log7.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
152828
- log7.info(`Status : ${result2.reason}`, void 0, "console");
153760
+ log11.info(`Current : ${result2.current}`, void 0, "console");
153761
+ log11.info(`Latest : ${result2.latest ?? "(none)"}`, void 0, "console");
153762
+ log11.info(`Channel : ${cfg.channel}`, void 0, "console");
153763
+ log11.info(`Diff : ${result2.diff ?? "(none)"}`, void 0, "console");
153764
+ log11.info(`Status : ${result2.reason}`, void 0, "console");
152829
153765
  return;
152830
153766
  }
152831
153767
  const port = Number.parseInt(opts.port, 10);
@@ -152841,20 +153777,20 @@ program2.command("update").description("Check for streamer updates from GitHub R
152841
153777
  });
152842
153778
  switch (result.kind) {
152843
153779
  case "no-op":
152844
- log7.info(`Current : ${result.current}`, void 0, "console");
152845
- log7.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
152846
- log7.info(`Status : ${result.reason}`, void 0, "console");
153780
+ log11.info(`Current : ${result.current}`, void 0, "console");
153781
+ log11.info(`Latest : ${result.latest ?? "(none)"}`, void 0, "console");
153782
+ log11.info(`Status : ${result.reason}`, void 0, "console");
152847
153783
  break;
152848
153784
  case "unsupported-install":
152849
- log7.warn(result.reason, void 0, "console");
153785
+ log11.warn(result.reason, void 0, "console");
152850
153786
  process.exitCode = 2;
152851
153787
  break;
152852
153788
  case "deferred":
152853
- log7.warn(`Deferred: ${result.reason}`, void 0, "console");
153789
+ log11.warn(`Deferred: ${result.reason}`, void 0, "console");
152854
153790
  process.exitCode = 2;
152855
153791
  break;
152856
153792
  case "dry-run":
152857
- log7.info(
153793
+ log11.info(
152858
153794
  `Would install ${result.latest} from ${result.tarballUrl}`,
152859
153795
  void 0,
152860
153796
  "console"
@@ -152862,28 +153798,28 @@ program2.command("update").description("Check for streamer updates from GitHub R
152862
153798
  break;
152863
153799
  case "installed":
152864
153800
  if (result.restart.method.startsWith("failed:")) {
152865
- log7.error(
153801
+ log11.error(
152866
153802
  `Installed ${result.installed} on disk, but the running service was not updated. Restart: ${result.restart.method}.`,
152867
153803
  void 0,
152868
153804
  "console"
152869
153805
  );
152870
153806
  process.exitCode = 1;
152871
153807
  } else {
152872
- log7.info(
153808
+ log11.info(
152873
153809
  `Installed ${result.installed} (was ${result.previous}). Restart: ${result.restart.method}.`,
152874
153810
  void 0,
152875
153811
  "console"
152876
153812
  );
152877
153813
  }
152878
153814
  if (result.pruned.length > 0) {
152879
- log7.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
153815
+ log11.info(`Pruned old releases: ${result.pruned.join(", ")}`, void 0, "console");
152880
153816
  }
152881
153817
  break;
152882
153818
  }
152883
153819
  } catch (err) {
152884
153820
  const message = err instanceof Error ? err.message : String(err);
152885
153821
  appendUpdateLog(`[error] ${message}`);
152886
- log7.error(`Update failed: ${message}`, { error: message }, "console");
153822
+ log11.error(`Update failed: ${message}`, { error: message }, "console");
152887
153823
  process.exitCode = 1;
152888
153824
  }
152889
153825
  });
@@ -152929,7 +153865,7 @@ async function printServerBanner({
152929
153865
  }) {
152930
153866
  const url2 = resolveServerUrl({ publicUrl, port });
152931
153867
  if (!includeQr) {
152932
- log7.info(printUrlBanner({ url: url2 }), void 0, "console");
153868
+ log11.info(printUrlBanner({ url: url2 }), void 0, "console");
152933
153869
  return;
152934
153870
  }
152935
153871
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
@@ -152944,9 +153880,9 @@ async function printServerBanner({
152944
153880
  const expSeconds = Math.floor(expiresAt / 1e3);
152945
153881
  const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
152946
153882
  const qr2 = await generateQr(payload);
152947
- log7.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
152948
- log7.info(`Pair URL: ${payload}`, void 0, "console");
152949
- log7.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
153883
+ log11.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
153884
+ log11.info(`Pair URL: ${payload}`, void 0, "console");
153885
+ log11.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
152950
153886
  }
152951
153887
  /*! Bundled license information:
152952
153888