@threadbase-sh/streamer 1.48.0 → 1.50.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
@@ -1490,6 +1490,10 @@ function isPermissionMode(value) {
1490
1490
  function isDangerousPermissionMode(mode) {
1491
1491
  return DANGEROUS_PERMISSION_MODES.includes(mode);
1492
1492
  }
1493
+ function effectivePermissionMode(flags, fallback) {
1494
+ const candidate = flags?.permissionMode ?? fallback;
1495
+ return isPermissionMode(candidate) ? candidate : void 0;
1496
+ }
1493
1497
  function isEffortLevel(value) {
1494
1498
  return typeof value === "string" && EFFORT_LEVELS.includes(value);
1495
1499
  }
@@ -142369,7 +142373,7 @@ function createLogsRoutes() {
142369
142373
 
142370
142374
  // src/api/routes/misc.routes.ts
142371
142375
  var import_node_child_process2 = require("child_process");
142372
- var import_node_crypto = require("crypto");
142376
+ var import_node_crypto2 = require("crypto");
142373
142377
  var import_os7 = require("os");
142374
142378
 
142375
142379
  // src/db/repositories/push.repository.ts
@@ -142628,6 +142632,170 @@ var PushRepository = class {
142628
142632
 
142629
142633
  // src/api/routes/misc.routes.ts
142630
142634
  init_logger();
142635
+
142636
+ // src/services/push/apnsClient.ts
142637
+ var import_node_crypto = require("crypto");
142638
+ var import_node_http2 = require("http2");
142639
+ init_logger();
142640
+ var log3 = getLogger("apns");
142641
+ var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
142642
+ var APNS_MAX_PAYLOAD_BYTES = 4096;
142643
+ var JWT_TTL_SECONDS = 3e3;
142644
+ var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
142645
+ "BadDeviceToken",
142646
+ "DeviceTokenNotForTopic",
142647
+ "Unregistered",
142648
+ "ExpiredToken"
142649
+ ]);
142650
+ function base64url3(input) {
142651
+ return Buffer.from(input).toString("base64url");
142652
+ }
142653
+ function readApnsCredentialsFromEnv(env = process.env) {
142654
+ const key = env.APNS_KEY;
142655
+ if (!key || key.trim().length === 0) return null;
142656
+ const keyId = env.APNS_KEY_ID?.trim();
142657
+ const teamId = env.APNS_TEAM_ID?.trim();
142658
+ const bundleId = env.APNS_BUNDLE_ID?.trim();
142659
+ if (!keyId || !teamId || !bundleId) return null;
142660
+ const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
142661
+ return { key, keyId, teamId, bundleId, host };
142662
+ }
142663
+ function describeMissingApnsCredentials(env = process.env) {
142664
+ if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
142665
+ 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.";
142666
+ }
142667
+ const missing = [
142668
+ ["APNS_KEY_ID", env.APNS_KEY_ID],
142669
+ ["APNS_TEAM_ID", env.APNS_TEAM_ID],
142670
+ ["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
142671
+ ].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
142672
+ if (missing.length === 0) return null;
142673
+ 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.`;
142674
+ }
142675
+ var ApnsClient = class {
142676
+ constructor(creds) {
142677
+ this.creds = creds;
142678
+ }
142679
+ creds;
142680
+ session = null;
142681
+ cachedJwt = null;
142682
+ /**
142683
+ * The `apns-topic` for Live Activity pushes.
142684
+ *
142685
+ * The `.push-type.liveactivity` suffix is mandatory and is why the signing key
142686
+ * must be Team Scoped (All Topics) — a key scoped to the bundle id alone
142687
+ * cannot sign this topic.
142688
+ */
142689
+ get topic() {
142690
+ return `${this.creds.bundleId}.push-type.liveactivity`;
142691
+ }
142692
+ /**
142693
+ * Mint or reuse the provider JWT.
142694
+ *
142695
+ * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
142696
+ * token older than an hour, but minting one per request is wasteful and can
142697
+ * trip APNs' provider-token-update throttle.
142698
+ */
142699
+ getJwt(now = Date.now()) {
142700
+ const nowSeconds = Math.floor(now / 1e3);
142701
+ if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
142702
+ return this.cachedJwt.token;
142703
+ }
142704
+ const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
142705
+ const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
142706
+ const signingInput = `${header}.${payload}`;
142707
+ const signature = (0, import_node_crypto.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
142708
+ const token = `${signingInput}.${base64url3(signature)}`;
142709
+ this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
142710
+ return token;
142711
+ }
142712
+ /**
142713
+ * Reuse one HTTP/2 session across sends.
142714
+ *
142715
+ * APNs expects a long-lived connection; a fresh TLS handshake per push is slow
142716
+ * and Apple treats connection churn as abuse.
142717
+ */
142718
+ getSession() {
142719
+ if (this.session && !this.session.closed && !this.session.destroyed) {
142720
+ return this.session;
142721
+ }
142722
+ const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
142723
+ session.on("error", (err) => {
142724
+ log3.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
142725
+ });
142726
+ this.session = session;
142727
+ return session;
142728
+ }
142729
+ /**
142730
+ * Send one push.
142731
+ *
142732
+ * Resolves with a result rather than rejecting on an APNs rejection: a
142733
+ * rejected push is an expected outcome the caller must act on (retire the
142734
+ * token), not an exception. Only a genuinely unexpected local failure throws,
142735
+ * and the caller logs it.
142736
+ */
142737
+ async send(args) {
142738
+ const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
142739
+ if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
142740
+ throw new Error(
142741
+ `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
142742
+ );
142743
+ }
142744
+ const session = this.getSession();
142745
+ const headers = {
142746
+ [import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
142747
+ [import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
142748
+ [import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
142749
+ "apns-push-type": "liveactivity",
142750
+ "apns-topic": this.topic,
142751
+ "apns-priority": String(args.priority ?? 10),
142752
+ ...args.expirationSeconds != null && {
142753
+ "apns-expiration": String(args.expirationSeconds)
142754
+ },
142755
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
142756
+ [import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
142757
+ };
142758
+ return new Promise((resolve4, reject) => {
142759
+ const req = session.request(headers);
142760
+ req.setTimeout(args.timeoutMs ?? 1e4, () => {
142761
+ req.close(import_node_http2.constants.NGHTTP2_CANCEL);
142762
+ resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
142763
+ });
142764
+ let status = 0;
142765
+ req.on("response", (resHeaders) => {
142766
+ status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
142767
+ });
142768
+ const chunks = [];
142769
+ req.on("data", (chunk) => chunks.push(chunk));
142770
+ req.on("error", reject);
142771
+ req.on("end", () => {
142772
+ const raw2 = Buffer.concat(chunks).toString("utf-8");
142773
+ let reason;
142774
+ if (raw2.length > 0) {
142775
+ try {
142776
+ reason = JSON.parse(raw2).reason;
142777
+ } catch {
142778
+ reason = raw2.slice(0, 200);
142779
+ }
142780
+ }
142781
+ resolve4({
142782
+ ok: status === 200,
142783
+ status,
142784
+ reason,
142785
+ tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
142786
+ });
142787
+ });
142788
+ req.end(body);
142789
+ });
142790
+ }
142791
+ /** Close the shared connection. Called on server shutdown. */
142792
+ close() {
142793
+ this.session?.close();
142794
+ this.session = null;
142795
+ }
142796
+ };
142797
+
142798
+ // src/api/routes/misc.routes.ts
142631
142799
  function numberOrNull(value) {
142632
142800
  return typeof value === "number" && Number.isFinite(value) ? value : null;
142633
142801
  }
@@ -142657,11 +142825,22 @@ function readRawBody3(req) {
142657
142825
  function verifyWebhookSignature(body, header, secret) {
142658
142826
  if (!header) return false;
142659
142827
  const provided = header.startsWith("sha256=") ? header.slice(7) : header;
142660
- const expected = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
142828
+ const expected = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
142661
142829
  const a = Buffer.from(provided, "utf-8");
142662
142830
  const b2 = Buffer.from(expected, "utf-8");
142663
142831
  if (a.length !== b2.length) return false;
142664
- return (0, import_node_crypto.timingSafeEqual)(a, b2);
142832
+ return (0, import_node_crypto2.timingSafeEqual)(a, b2);
142833
+ }
142834
+ function describePushCapability(liveActivityEnabled, env = process.env) {
142835
+ if (liveActivityEnabled) return { liveActivity: true, notifications: false };
142836
+ return {
142837
+ liveActivity: false,
142838
+ notifications: false,
142839
+ // describeMissingApnsCredentials only explains a *credential* gap and
142840
+ // returns null once the credentials are complete — reachable here, because
142841
+ // an unavailable token store disables the feature with the key still set.
142842
+ liveActivityReason: describeMissingApnsCredentials(env) ?? "APNs credentials are set but the push token store is unavailable, so Live Activity push is disabled."
142843
+ };
142665
142844
  }
142666
142845
  var clientLog = getLogger("client");
142667
142846
  var createMiscRoutes = (deps) => {
@@ -142684,7 +142863,12 @@ var createMiscRoutes = (deps) => {
142684
142863
  featureFlags: true,
142685
142864
  // Same contract: this server serves GET /api/projects/summary, which the
142686
142865
  // Hub's grouped views need before they can draw a tree.
142687
- projectSummary: true
142866
+ projectSummary: true,
142867
+ // Delivery capability, not endpoint support: whether this server can
142868
+ // actually send a push, so mobile can hide an affordance instead of
142869
+ // registering tokens nothing will ever send to. Absent on older servers,
142870
+ // which a client should read as "unknown", not "unavailable".
142871
+ push: describePushCapability(deps.liveActivityPushEnabled())
142688
142872
  });
142689
142873
  });
142690
142874
  app.get("/api/profiles", (c) => c.json([]));
@@ -142745,9 +142929,10 @@ var createMiscRoutes = (deps) => {
142745
142929
  return c.json({ ok: true });
142746
142930
  });
142747
142931
  app.get("/api/push/health", (c) => {
142932
+ const push = describePushCapability(deps.liveActivityPushEnabled());
142748
142933
  const repo = deps.pushRepo();
142749
- if (!repo) return c.json({ tokens: [], available: false });
142750
- return c.json({ tokens: repo.listHealth(), available: true });
142934
+ if (!repo) return c.json({ tokens: [], available: false, push });
142935
+ return c.json({ tokens: repo.listHealth(), available: true, push });
142751
142936
  });
142752
142937
  app.post("/api/__update", async (c) => {
142753
142938
  const cfg = loadUpdateConfig();
@@ -142815,7 +143000,7 @@ var createPairRoutes = (deps) => {
142815
143000
  };
142816
143001
 
142817
143002
  // src/api/routes/progress.routes.ts
142818
- var import_node_crypto2 = __toESM(require("crypto"), 1);
143003
+ var import_node_crypto3 = __toESM(require("crypto"), 1);
142819
143004
  function readRawBody4(req) {
142820
143005
  return new Promise((resolve4, reject) => {
142821
143006
  const chunks = [];
@@ -142826,10 +143011,10 @@ function readRawBody4(req) {
142826
143011
  }
142827
143012
  function verifySignature(rawBody, signature, secret) {
142828
143013
  if (!signature || signature.length === 0) return false;
142829
- const expected = import_node_crypto2.default.createHmac("sha256", secret).update(rawBody).digest("hex");
143014
+ const expected = import_node_crypto3.default.createHmac("sha256", secret).update(rawBody).digest("hex");
142830
143015
  if (expected.length !== signature.length) return false;
142831
143016
  try {
142832
- return import_node_crypto2.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
143017
+ return import_node_crypto3.default.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
142833
143018
  } catch {
142834
143019
  return false;
142835
143020
  }
@@ -143382,7 +143567,7 @@ var import_promises13 = require("timers/promises");
143382
143567
 
143383
143568
  // src/db/query-timing.ts
143384
143569
  init_logger();
143385
- var log3 = getLogger("db");
143570
+ var log4 = getLogger("db");
143386
143571
  var DEFAULT_SLOW_QUERY_MS = 35;
143387
143572
  var LABEL = /* @__PURE__ */ Symbol("tbQueryLabel");
143388
143573
  function deriveLabel(sql) {
@@ -143398,15 +143583,15 @@ function resolveSlowMs() {
143398
143583
  }
143399
143584
  function record2(label, ms2, rows, slowMs) {
143400
143585
  if (slowMs > 0 && ms2 >= slowMs) {
143401
- log3.warn(
143586
+ log4.warn(
143402
143587
  `[db] slow query ${label} ${ms2.toFixed(1)}ms rows=${rows}`,
143403
143588
  { event: "db.slow_query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143404
143589
  "pino"
143405
143590
  );
143406
143591
  return;
143407
143592
  }
143408
- if (log3.pino.isLevelEnabled("debug")) {
143409
- log3.debug(
143593
+ if (log4.pino.isLevelEnabled("debug")) {
143594
+ log4.debug(
143410
143595
  `[db] ${label} ${ms2.toFixed(2)}ms rows=${rows}`,
143411
143596
  { event: "db.query", stmt: label, ms: Math.round(ms2 * 100) / 100, rows },
143412
143597
  "pino"
@@ -146810,168 +146995,6 @@ function deriveProjectChatTitle(input) {
146810
146995
  return `Untitled \xB7 ${input.id.slice(0, 8)}`;
146811
146996
  }
146812
146997
 
146813
- // src/services/push/apnsClient.ts
146814
- var import_node_crypto3 = require("crypto");
146815
- var import_node_http2 = require("http2");
146816
- init_logger();
146817
- var log4 = getLogger("apns");
146818
- var APNS_HOST_SANDBOX = "api.sandbox.push.apple.com";
146819
- var APNS_MAX_PAYLOAD_BYTES = 4096;
146820
- var JWT_TTL_SECONDS = 3e3;
146821
- var DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
146822
- "BadDeviceToken",
146823
- "DeviceTokenNotForTopic",
146824
- "Unregistered",
146825
- "ExpiredToken"
146826
- ]);
146827
- function base64url3(input) {
146828
- return Buffer.from(input).toString("base64url");
146829
- }
146830
- function readApnsCredentialsFromEnv(env = process.env) {
146831
- const key = env.APNS_KEY;
146832
- if (!key || key.trim().length === 0) return null;
146833
- const keyId = env.APNS_KEY_ID?.trim();
146834
- const teamId = env.APNS_TEAM_ID?.trim();
146835
- const bundleId = env.APNS_BUNDLE_ID?.trim();
146836
- if (!keyId || !teamId || !bundleId) return null;
146837
- const host = env.APNS_HOST ?? APNS_HOST_SANDBOX;
146838
- return { key, keyId, teamId, bundleId, host };
146839
- }
146840
- function describeMissingApnsCredentials(env = process.env) {
146841
- if (!env.APNS_KEY || env.APNS_KEY.trim().length === 0) {
146842
- return "APNS_KEY is not set, so Live Activity push is disabled. Set it to the p8 key contents (not a path) to enable it.";
146843
- }
146844
- const missing = [
146845
- ["APNS_KEY_ID", env.APNS_KEY_ID],
146846
- ["APNS_TEAM_ID", env.APNS_TEAM_ID],
146847
- ["APNS_BUNDLE_ID", env.APNS_BUNDLE_ID]
146848
- ].filter(([, value]) => !value || value.trim().length === 0).map(([name]) => name);
146849
- if (missing.length === 0) return null;
146850
- return `APNS_KEY is set but ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not, so Live Activity push is disabled. Under launchd, APNS_KEY_ID is derived from the AuthKey_<keyId>.p8 filename; the team and bundle ids must be set explicitly.`;
146851
- }
146852
- var ApnsClient = class {
146853
- constructor(creds) {
146854
- this.creds = creds;
146855
- }
146856
- creds;
146857
- session = null;
146858
- cachedJwt = null;
146859
- /**
146860
- * The `apns-topic` for Live Activity pushes.
146861
- *
146862
- * The `.push-type.liveactivity` suffix is mandatory and is why the signing key
146863
- * must be Team Scoped (All Topics) — a key scoped to the bundle id alone
146864
- * cannot sign this topic.
146865
- */
146866
- get topic() {
146867
- return `${this.creds.bundleId}.push-type.liveactivity`;
146868
- }
146869
- /**
146870
- * Mint or reuse the provider JWT.
146871
- *
146872
- * ES256 over the p8 key. Cached until shortly before expiry: Apple rejects a
146873
- * token older than an hour, but minting one per request is wasteful and can
146874
- * trip APNs' provider-token-update throttle.
146875
- */
146876
- getJwt(now = Date.now()) {
146877
- const nowSeconds = Math.floor(now / 1e3);
146878
- if (this.cachedJwt && this.cachedJwt.expiresAt > nowSeconds + 60) {
146879
- return this.cachedJwt.token;
146880
- }
146881
- const header = base64url3(JSON.stringify({ alg: "ES256", kid: this.creds.keyId, typ: "JWT" }));
146882
- const payload = base64url3(JSON.stringify({ iss: this.creds.teamId, iat: nowSeconds }));
146883
- const signingInput = `${header}.${payload}`;
146884
- const signature = (0, import_node_crypto3.createSign)("SHA256").update(signingInput).sign({ key: this.creds.key, dsaEncoding: "ieee-p1363" });
146885
- const token = `${signingInput}.${base64url3(signature)}`;
146886
- this.cachedJwt = { token, expiresAt: nowSeconds + JWT_TTL_SECONDS };
146887
- return token;
146888
- }
146889
- /**
146890
- * Reuse one HTTP/2 session across sends.
146891
- *
146892
- * APNs expects a long-lived connection; a fresh TLS handshake per push is slow
146893
- * and Apple treats connection churn as abuse.
146894
- */
146895
- getSession() {
146896
- if (this.session && !this.session.closed && !this.session.destroyed) {
146897
- return this.session;
146898
- }
146899
- const session = (0, import_node_http2.connect)(`https://${this.creds.host}`);
146900
- session.on("error", (err) => {
146901
- log4.warn("apns.session_error", { event: "apns.session_error", err: String(err) });
146902
- });
146903
- this.session = session;
146904
- return session;
146905
- }
146906
- /**
146907
- * Send one push.
146908
- *
146909
- * Resolves with a result rather than rejecting on an APNs rejection: a
146910
- * rejected push is an expected outcome the caller must act on (retire the
146911
- * token), not an exception. Only a genuinely unexpected local failure throws,
146912
- * and the caller logs it.
146913
- */
146914
- async send(args) {
146915
- const body = Buffer.from(JSON.stringify(args.payload), "utf-8");
146916
- if (body.byteLength > APNS_MAX_PAYLOAD_BYTES) {
146917
- throw new Error(
146918
- `APNs payload is ${body.byteLength} bytes, over the ${APNS_MAX_PAYLOAD_BYTES} byte limit`
146919
- );
146920
- }
146921
- const session = this.getSession();
146922
- const headers = {
146923
- [import_node_http2.constants.HTTP2_HEADER_METHOD]: "POST",
146924
- [import_node_http2.constants.HTTP2_HEADER_PATH]: `/3/device/${args.deviceToken}`,
146925
- [import_node_http2.constants.HTTP2_HEADER_AUTHORIZATION]: `bearer ${this.getJwt()}`,
146926
- "apns-push-type": "liveactivity",
146927
- "apns-topic": this.topic,
146928
- "apns-priority": String(args.priority ?? 10),
146929
- ...args.expirationSeconds != null && {
146930
- "apns-expiration": String(args.expirationSeconds)
146931
- },
146932
- [import_node_http2.constants.HTTP2_HEADER_CONTENT_TYPE]: "application/json",
146933
- [import_node_http2.constants.HTTP2_HEADER_CONTENT_LENGTH]: String(body.byteLength)
146934
- };
146935
- return new Promise((resolve4, reject) => {
146936
- const req = session.request(headers);
146937
- req.setTimeout(args.timeoutMs ?? 1e4, () => {
146938
- req.close(import_node_http2.constants.NGHTTP2_CANCEL);
146939
- resolve4({ ok: false, status: 0, reason: "Timeout", tokenDead: false });
146940
- });
146941
- let status = 0;
146942
- req.on("response", (resHeaders) => {
146943
- status = Number(resHeaders[import_node_http2.constants.HTTP2_HEADER_STATUS] ?? 0);
146944
- });
146945
- const chunks = [];
146946
- req.on("data", (chunk) => chunks.push(chunk));
146947
- req.on("error", reject);
146948
- req.on("end", () => {
146949
- const raw2 = Buffer.concat(chunks).toString("utf-8");
146950
- let reason;
146951
- if (raw2.length > 0) {
146952
- try {
146953
- reason = JSON.parse(raw2).reason;
146954
- } catch {
146955
- reason = raw2.slice(0, 200);
146956
- }
146957
- }
146958
- resolve4({
146959
- ok: status === 200,
146960
- status,
146961
- reason,
146962
- tokenDead: reason != null && DEAD_TOKEN_REASONS.has(reason)
146963
- });
146964
- });
146965
- req.end(body);
146966
- });
146967
- }
146968
- /** Close the shared connection. Called on server shutdown. */
146969
- close() {
146970
- this.session?.close();
146971
- this.session = null;
146972
- }
146973
- };
146974
-
146975
146998
  // src/services/push/expoPushSender.ts
146976
146999
  init_logger();
146977
147000
  var log5 = getLogger("expo-push");
@@ -149290,6 +149313,7 @@ var StreamerServer = class {
149290
149313
  cache: () => this.cache,
149291
149314
  cacheMonitor: () => this.cacheMonitor,
149292
149315
  pushRepo: () => this.pushRepo,
149316
+ liveActivityPushEnabled: () => this.liveActivityNotifier !== null,
149293
149317
  devicesRepo: () => this.devicesRepo,
149294
149318
  projectsRepo: () => this.projectsRepo,
149295
149319
  conversationsRepo: () => this.conversationsRepo,
@@ -157454,6 +157478,17 @@ program2.command("serve").description("Start the streamer server").option("-p, -
157454
157478
  takeoverProd2({ port: plan.port, repoToplevel });
157455
157479
  }
157456
157480
  }
157481
+ const spawnMode = effectivePermissionMode(
157482
+ claudeFlags,
157483
+ resolvedDefaultPermissionMode ?? loadDefaultPermissionMode()
157484
+ );
157485
+ if (spawnMode !== void 0 && isDangerousPermissionMode(spawnMode)) {
157486
+ log14.warn(
157487
+ `[WARN] permission mode is ${spawnMode} \u2014 spawned sessions run without confirmation prompts, so anyone holding the API key can execute arbitrary code on this machine. There is no spend limit.`,
157488
+ void 0,
157489
+ "console"
157490
+ );
157491
+ }
157457
157492
  const server = new StreamerServer({
157458
157493
  port: resolvedPort,
157459
157494
  apiKey,