@threadbase-sh/streamer 1.24.7 → 1.26.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
@@ -1530,6 +1530,15 @@ function loadPublicUrl() {
1530
1530
  }
1531
1531
  return void 0;
1532
1532
  }
1533
+ function loadBrowserCors() {
1534
+ try {
1535
+ const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
1536
+ const match2 = content.match(/browser_cors:\s*(.+)/);
1537
+ if (match2?.[1]) return match2[1].trim();
1538
+ } catch {
1539
+ }
1540
+ return void 0;
1541
+ }
1533
1542
  function loadCacheDir() {
1534
1543
  try {
1535
1544
  const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
@@ -1548,6 +1557,42 @@ function loadTailSize() {
1548
1557
  }
1549
1558
  return void 0;
1550
1559
  }
1560
+ function loadDefaultPermissionMode() {
1561
+ try {
1562
+ const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
1563
+ const match2 = content.match(/default_permission_mode:\s*(\S+)/);
1564
+ const value = match2?.[1]?.trim();
1565
+ if (value === "acceptEdits" || value === "manual") return value;
1566
+ } catch {
1567
+ }
1568
+ return void 0;
1569
+ }
1570
+ function setDefaultPermissionMode(mode) {
1571
+ const file2 = configFile();
1572
+ (0, import_fs.mkdirSync)(configDir(), { recursive: true });
1573
+ let content = "";
1574
+ try {
1575
+ content = (0, import_fs.readFileSync)(file2, "utf-8");
1576
+ } catch (err) {
1577
+ if (err.code !== "ENOENT") throw err;
1578
+ }
1579
+ const line = `default_permission_mode: ${mode}`;
1580
+ let updated;
1581
+ if (/^default_permission_mode:\s*.+$/m.test(content)) {
1582
+ updated = content.replace(/^default_permission_mode:\s*.+$/m, line);
1583
+ } else if (content.length === 0 || content.endsWith("\n")) {
1584
+ updated = `${content}${line}
1585
+ `;
1586
+ } else {
1587
+ updated = `${content}
1588
+ ${line}
1589
+ `;
1590
+ }
1591
+ const tmpFile = `${file2}.tmp`;
1592
+ (0, import_fs.writeFileSync)(tmpFile, updated, { encoding: "utf-8", mode: 384 });
1593
+ (0, import_fs.chmodSync)(tmpFile, 384);
1594
+ (0, import_fs.renameSync)(tmpFile, file2);
1595
+ }
1551
1596
  function validatePublicUrl(raw2) {
1552
1597
  let parsed;
1553
1598
  try {
@@ -123699,6 +123744,68 @@ var init_check_sqlite_abi = __esm({
123699
123744
  }
123700
123745
  });
123701
123746
 
123747
+ // src/lifecycle/prompt.ts
123748
+ var prompt_exports = {};
123749
+ __export(prompt_exports, {
123750
+ interactivePermissionModePrompt: () => interactivePermissionModePrompt,
123751
+ interactivePrompt: () => interactivePrompt
123752
+ });
123753
+ var import_node_process2, import_promises18, interactivePrompt, interactivePermissionModePrompt;
123754
+ var init_prompt = __esm({
123755
+ "src/lifecycle/prompt.ts"() {
123756
+ "use strict";
123757
+ import_node_process2 = require("process");
123758
+ import_promises18 = require("readline/promises");
123759
+ interactivePrompt = async ({ prodPort, suggestedAltPort, prodActive }) => {
123760
+ const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
123761
+ try {
123762
+ if (!prodActive) {
123763
+ import_node_process2.stdout.write(
123764
+ `
123765
+ Port ${prodPort} is already in use by another process.
123766
+ Running dev on port ${suggestedAltPort} instead.
123767
+ `
123768
+ );
123769
+ const rememberAns2 = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
123770
+ return {
123771
+ choice: "use-port",
123772
+ port: suggestedAltPort,
123773
+ remember: rememberAns2 === "y" || rememberAns2 === "yes"
123774
+ };
123775
+ }
123776
+ import_node_process2.stdout.write(
123777
+ `
123778
+ The supervised prod streamer is already holding port ${prodPort}.
123779
+ [r] Stop prod and take port ${prodPort}
123780
+ [p] Run dev on port ${suggestedAltPort} instead
123781
+ `
123782
+ );
123783
+ const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
123784
+ const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
123785
+ const remember = rememberAns === "y" || rememberAns === "yes";
123786
+ if (choiceAns === "r") {
123787
+ return { choice: "replace-prod", remember };
123788
+ }
123789
+ return { choice: "use-port", port: suggestedAltPort, remember };
123790
+ } finally {
123791
+ rl.close();
123792
+ }
123793
+ };
123794
+ interactivePermissionModePrompt = async () => {
123795
+ const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
123796
+ try {
123797
+ import_node_process2.stdout.write(
123798
+ "\nWhich permission mode should spawned Claude Code sessions use?\n [a] acceptEdits \u2014 auto-approve file edits, still prompt for shell commands (default)\n [m] manual \u2014 prompt for every edit and command\n"
123799
+ );
123800
+ const ans = (await rl.question("Choice [a/m] (default a): ")).trim().toLowerCase();
123801
+ return ans === "m" ? "manual" : "acceptEdits";
123802
+ } finally {
123803
+ rl.close();
123804
+ }
123805
+ };
123806
+ }
123807
+ });
123808
+
123702
123809
  // src/lifecycle/prefs.ts
123703
123810
  function readPrefs() {
123704
123811
  const path2 = prefsPath();
@@ -123781,7 +123888,7 @@ async function resolveDevPlan(opts) {
123781
123888
  }
123782
123889
  const prodActive = opts.isProdActive();
123783
123890
  const portTaken = opts.portInUse(opts.requestedPort);
123784
- if (!prodActive && !portTaken) {
123891
+ if (!portTaken) {
123785
123892
  return { kind: "use-port", port: opts.requestedPort };
123786
123893
  }
123787
123894
  if (!opts.forget) {
@@ -123796,7 +123903,12 @@ async function resolveDevPlan(opts) {
123796
123903
  }
123797
123904
  }
123798
123905
  const suggested = await opts.findFreePort(opts.requestedPort + 1);
123799
- const answer = await opts.prompt({ prodPort: opts.requestedPort, suggestedAltPort: suggested });
123906
+ const answer = await opts.prompt({
123907
+ prodPort: opts.requestedPort,
123908
+ suggestedAltPort: suggested,
123909
+ prodActive,
123910
+ portTaken
123911
+ });
123800
123912
  if (answer.remember && opts.repoToplevel) {
123801
123913
  if (answer.choice === "replace-prod") {
123802
123914
  writePrefForRepo(opts.repoToplevel, { choice: "replace-prod" });
@@ -123902,41 +124014,6 @@ var init_dev_takeover = __esm({
123902
124014
  }
123903
124015
  });
123904
124016
 
123905
- // src/lifecycle/prompt.ts
123906
- var prompt_exports = {};
123907
- __export(prompt_exports, {
123908
- interactivePrompt: () => interactivePrompt
123909
- });
123910
- var import_node_process2, import_promises18, interactivePrompt;
123911
- var init_prompt = __esm({
123912
- "src/lifecycle/prompt.ts"() {
123913
- "use strict";
123914
- import_node_process2 = require("process");
123915
- import_promises18 = require("readline/promises");
123916
- interactivePrompt = async ({ prodPort, suggestedAltPort }) => {
123917
- const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
123918
- try {
123919
- import_node_process2.stdout.write(
123920
- `
123921
- Prod streamer is running on port ${prodPort}.
123922
- [r] Stop prod and take port ${prodPort}
123923
- [p] Run dev on port ${suggestedAltPort} instead
123924
- `
123925
- );
123926
- const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
123927
- const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
123928
- const remember = rememberAns === "y" || rememberAns === "yes";
123929
- if (choiceAns === "r") {
123930
- return { choice: "replace-prod", remember };
123931
- }
123932
- return { choice: "use-port", port: suggestedAltPort, remember };
123933
- } finally {
123934
- rl.close();
123935
- }
123936
- };
123937
- }
123938
- });
123939
-
123940
124017
  // src/lifecycle/repo.ts
123941
124018
  var repo_exports = {};
123942
124019
  __export(repo_exports, {
@@ -124017,6 +124094,9 @@ var init_setKey = __esm({
124017
124094
  );
124018
124095
  })();
124019
124096
 
124097
+ // cli/index.ts
124098
+ var import_node_process3 = require("process");
124099
+
124020
124100
  // node_modules/commander/lib/error.js
124021
124101
  var CommanderError = class extends Error {
124022
124102
  /**
@@ -135731,25 +135811,55 @@ var authMiddleware = (deps) => async (c, next) => {
135731
135811
  };
135732
135812
 
135733
135813
  // src/api/middleware/cors.middleware.ts
135734
- var ALLOWED_ORIGINS = /* @__PURE__ */ new Set([
135814
+ var DEFAULT_DEV_ORIGINS = [
135735
135815
  "http://localhost:8081",
135736
135816
  "http://localhost:19006",
135737
135817
  "http://localhost:3000"
135738
- ]);
135739
- var corsMiddleware = () => async (c, next) => {
135740
- const origin = c.req.header("origin");
135741
- const allowedOrigin = origin && ALLOWED_ORIGINS.has(origin) ? origin : null;
135742
- if (allowedOrigin) {
135743
- c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
135744
- c.res.headers.set("Vary", "Origin");
135745
- c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
135746
- c.res.headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
135747
- c.res.headers.set("Access-Control-Expose-Headers", "ETag");
135748
- }
135749
- if (c.req.method === "OPTIONS") {
135750
- return c.newResponse(null, allowedOrigin ? 204 : 403);
135751
- }
135752
- await next();
135818
+ ];
135819
+ function resolveAllowedOrigins(raw2) {
135820
+ if (!raw2) return null;
135821
+ const trimmed = raw2.trim();
135822
+ const lower = trimmed.toLowerCase();
135823
+ if (lower === "0" || lower === "false" || lower === "no" || lower === "off" || trimmed === "") {
135824
+ return null;
135825
+ }
135826
+ const origins = new Set(DEFAULT_DEV_ORIGINS);
135827
+ if (!["1", "true", "yes", "on"].includes(lower)) {
135828
+ for (const o of trimmed.split(",")) {
135829
+ const origin = o.trim();
135830
+ if (origin) origins.add(origin);
135831
+ }
135832
+ }
135833
+ return origins;
135834
+ }
135835
+ var corsMiddleware = (configValue) => {
135836
+ const allowedOrigins = resolveAllowedOrigins(
135837
+ process.env.THREADBASE_ALLOW_BROWSER_CORS ?? configValue
135838
+ );
135839
+ return async (c, next) => {
135840
+ const origin = c.req.header("origin");
135841
+ const allowedOrigin = allowedOrigins && origin && allowedOrigins.has(origin) ? origin : null;
135842
+ if (allowedOrigin) {
135843
+ const raw2 = c.env.outgoing;
135844
+ raw2.setHeader("Access-Control-Allow-Origin", allowedOrigin);
135845
+ raw2.setHeader("Vary", "Origin");
135846
+ raw2.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
135847
+ raw2.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, If-None-Match");
135848
+ raw2.setHeader("Access-Control-Expose-Headers", "ETag");
135849
+ c.res.headers.set("Access-Control-Allow-Origin", allowedOrigin);
135850
+ c.res.headers.set("Vary", "Origin");
135851
+ c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
135852
+ c.res.headers.set(
135853
+ "Access-Control-Allow-Headers",
135854
+ "Authorization, Content-Type, If-None-Match"
135855
+ );
135856
+ c.res.headers.set("Access-Control-Expose-Headers", "ETag");
135857
+ }
135858
+ if (c.req.method === "OPTIONS") {
135859
+ return c.newResponse(null, allowedOrigin ? 204 : 403);
135860
+ }
135861
+ await next();
135862
+ };
135753
135863
  };
135754
135864
 
135755
135865
  // src/api/middleware/error.middleware.ts
@@ -136246,7 +136356,7 @@ var createHonoApp = (deps, upgradeWebSocket) => {
136246
136356
  event: "http.request"
136247
136357
  });
136248
136358
  });
136249
- app.use("*", corsMiddleware());
136359
+ app.use("*", corsMiddleware(deps.browserCors));
136250
136360
  app.use("*", authMiddleware(deps));
136251
136361
  app.onError(errorMiddleware);
136252
136362
  app.route("/healthz", createHealthRoutes());
@@ -136542,11 +136652,11 @@ var ConversationCache = class _ConversationCache {
136542
136652
  INSERT INTO conversation_meta
136543
136653
  (id, file_path, project_path, project_name, title, model, account, branch,
136544
136654
  message_count, last_activity, first_message, last_message, preview, updated_at,
136545
- mtime_ms, file_size, provider)
136655
+ mtime_ms, file_size, provider, scanner_meta_json)
136546
136656
  VALUES
136547
136657
  (@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
136548
136658
  @message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
136549
- @mtime_ms, @file_size, @provider)
136659
+ @mtime_ms, @file_size, @provider, @scanner_meta_json)
136550
136660
  ON CONFLICT(id) DO UPDATE SET
136551
136661
  file_path = excluded.file_path,
136552
136662
  project_path = excluded.project_path,
@@ -136566,7 +136676,8 @@ var ConversationCache = class _ConversationCache {
136566
136676
  updated_at = excluded.updated_at,
136567
136677
  mtime_ms = excluded.mtime_ms,
136568
136678
  file_size = excluded.file_size,
136569
- provider = excluded.provider
136679
+ provider = excluded.provider,
136680
+ scanner_meta_json = excluded.scanner_meta_json
136570
136681
  WHERE conversation_meta.updated_at < excluded.updated_at
136571
136682
  `),
136572
136683
  getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
@@ -136603,6 +136714,27 @@ var ConversationCache = class _ConversationCache {
136603
136714
  allFileStats: db.prepare(
136604
136715
  "SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
136605
136716
  ),
136717
+ allScannerStatCacheRows: db.prepare(
136718
+ "SELECT file_path, mtime_ms, file_size, scanner_meta_json FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL AND scanner_meta_json IS NOT NULL"
136719
+ ),
136720
+ updateScannerCache: db.prepare(
136721
+ "UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
136722
+ ),
136723
+ getFileMetadata: db.prepare(
136724
+ "SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
136725
+ ),
136726
+ upsertFileMetadata: db.prepare(`
136727
+ INSERT INTO conversation_file_metadata
136728
+ (file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
136729
+ VALUES
136730
+ (@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
136731
+ ON CONFLICT(file_path) DO UPDATE SET
136732
+ mtime_ms = excluded.mtime_ms,
136733
+ file_size = excluded.file_size,
136734
+ is_agent = excluded.is_agent,
136735
+ agent_entrypoints_key = excluded.agent_entrypoints_key,
136736
+ updated_at = excluded.updated_at
136737
+ `),
136606
136738
  upsertSessionName: db.prepare(`
136607
136739
  INSERT INTO session_names (session_id, name, updated_at)
136608
136740
  VALUES (?, ?, ?)
@@ -136643,8 +136775,35 @@ var ConversationCache = class _ConversationCache {
136643
136775
  getDatabase() {
136644
136776
  return this.db;
136645
136777
  }
136646
- getAgentEntrypoints() {
136647
- return this.agentEntrypoints;
136778
+ agentEntrypointsKey() {
136779
+ return [...this.agentEntrypoints].sort().join(",");
136780
+ }
136781
+ classifyAgentFile(filePath, mtimeMs, fileSize) {
136782
+ if (this.agentEntrypoints.size === 0) return false;
136783
+ const entrypointsKey = this.agentEntrypointsKey();
136784
+ const cached3 = this.stmts.getFileMetadata.get(filePath);
136785
+ if (cached3 && cached3.mtime_ms === mtimeMs && cached3.file_size === fileSize && cached3.agent_entrypoints_key === entrypointsKey) {
136786
+ return cached3.is_agent === 1;
136787
+ }
136788
+ const isAgent = isAgentFile(filePath, this.agentEntrypoints);
136789
+ this.stmts.upsertFileMetadata.run({
136790
+ file_path: filePath,
136791
+ mtime_ms: mtimeMs,
136792
+ file_size: fileSize,
136793
+ is_agent: isAgent ? 1 : 0,
136794
+ agent_entrypoints_key: entrypointsKey,
136795
+ updated_at: Date.now()
136796
+ });
136797
+ return isAgent;
136798
+ }
136799
+ isAgentFileCached(filePath) {
136800
+ let s3;
136801
+ try {
136802
+ s3 = (0, import_fs16.statSync)(filePath);
136803
+ } catch {
136804
+ return false;
136805
+ }
136806
+ return this.classifyAgentFile(filePath, s3.mtimeMs, s3.size);
136648
136807
  }
136649
136808
  static open(dbPath, tailSize = 10, migrationsDir, options) {
136650
136809
  (0, import_fs16.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
@@ -136820,11 +136979,9 @@ var ConversationCache = class _ConversationCache {
136820
136979
  // in conversation-cache.test.ts).
136821
136980
  upsertFromScannerMeta(metas) {
136822
136981
  const filter = this.filterAgentConversations;
136823
- const entrypoints = this.agentEntrypoints;
136824
136982
  const upsertedIds = [];
136825
136983
  const run2 = this.db.transaction((items) => {
136826
136984
  for (const m2 of items) {
136827
- if (filter && isAgentFile(m2.filePath, entrypoints)) continue;
136828
136985
  const id = m2.sessionId || m2.id.split("/").pop()?.replace(/\.jsonl$/, "") || m2.id;
136829
136986
  const lastActivityMs = m2.timestamp ? new Date(m2.timestamp).getTime() : null;
136830
136987
  let mtimeMs = null;
@@ -136836,6 +136993,10 @@ var ConversationCache = class _ConversationCache {
136836
136993
  } catch {
136837
136994
  }
136838
136995
  const seq = ++this.tailSeq;
136996
+ if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m2.filePath, mtimeMs, fileSize)) {
136997
+ continue;
136998
+ }
136999
+ const scannerMetaJson = JSON.stringify(m2);
136839
137000
  this.stmts.upsertFull.run({
136840
137001
  id,
136841
137002
  file_path: m2.filePath,
@@ -136853,8 +137014,10 @@ var ConversationCache = class _ConversationCache {
136853
137014
  updated_at: seq,
136854
137015
  mtime_ms: mtimeMs,
136855
137016
  file_size: fileSize,
136856
- provider: m2.provider ?? CLAUDE_CODE_PROVIDER2
137017
+ provider: m2.provider ?? CLAUDE_CODE_PROVIDER2,
137018
+ scanner_meta_json: scannerMetaJson
136857
137019
  });
137020
+ this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
136858
137021
  if (this.fileIndexLoaded) this.fileIndex.set(m2.filePath, id);
136859
137022
  upsertedIds.push(id);
136860
137023
  }
@@ -136978,6 +137141,21 @@ var ConversationCache = class _ConversationCache {
136978
137141
  }
136979
137142
  return map2;
136980
137143
  }
137144
+ getScannerStatCache() {
137145
+ const rows = this.stmts.allScannerStatCacheRows.all();
137146
+ const map2 = /* @__PURE__ */ new Map();
137147
+ for (const r of rows) {
137148
+ try {
137149
+ const meta3 = JSON.parse(r.scanner_meta_json);
137150
+ map2.set(r.file_path, {
137151
+ stat: { mtimeMs: r.mtime_ms, size: r.file_size },
137152
+ meta: meta3
137153
+ });
137154
+ } catch {
137155
+ }
137156
+ }
137157
+ return map2;
137158
+ }
136981
137159
  getMetaById(id) {
136982
137160
  const row = this.stmts.getFullById.get(id);
136983
137161
  if (!row) return null;
@@ -138280,6 +138458,37 @@ function detectShellPrompt(lines) {
138280
138458
  return null;
138281
138459
  }
138282
138460
 
138461
+ // src/utils/debounce.ts
138462
+ function debounce(fn, waitMs) {
138463
+ let timer = null;
138464
+ let lastArgs = null;
138465
+ const run2 = () => {
138466
+ timer = null;
138467
+ if (lastArgs) {
138468
+ const args = lastArgs;
138469
+ lastArgs = null;
138470
+ fn(...args);
138471
+ }
138472
+ };
138473
+ const debounced = (...args) => {
138474
+ lastArgs = args;
138475
+ if (timer) clearTimeout(timer);
138476
+ timer = setTimeout(run2, waitMs);
138477
+ };
138478
+ debounced.cancel = () => {
138479
+ if (timer) clearTimeout(timer);
138480
+ timer = null;
138481
+ lastArgs = null;
138482
+ };
138483
+ debounced.flush = () => {
138484
+ if (timer) {
138485
+ clearTimeout(timer);
138486
+ run2();
138487
+ }
138488
+ };
138489
+ return debounced;
138490
+ }
138491
+
138283
138492
  // src/pty-manager.ts
138284
138493
  var OUTPUT_BUFFER_MAX2 = 65536;
138285
138494
  var PTY_COLS2 = 120;
@@ -138287,11 +138496,13 @@ var PTY_ROWS2 = 40;
138287
138496
  var SCREEN_SCROLLBACK2 = 1e3;
138288
138497
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
138289
138498
  var PROMPT_MARKER_FALLBACK_MS = 1e4;
138499
+ var QUIET_DETECT_MS = 500;
138290
138500
  function buildPasteBytes(input) {
138291
138501
  return `\x1B[200~${input}\x1B[201~`;
138292
138502
  }
138293
138503
  var SUBMIT_BYTES2 = "\r";
138294
138504
  var SUBMIT_DELAY_MS = 16;
138505
+ var SUBMIT_MAX_WAIT_MS = 500;
138295
138506
  function digestBytes2(s3) {
138296
138507
  const escaped = s3.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
138297
138508
  if (escaped.length <= 200) return escaped;
@@ -138368,6 +138579,10 @@ var PTYManager = class {
138368
138579
  // to a given input or fell silent. Reset on dispose().
138369
138580
  chunkIndex = /* @__PURE__ */ new Map();
138370
138581
  lastChunkAt = /* @__PURE__ */ new Map();
138582
+ // Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
138583
+ // QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
138584
+ // wait for another chunk that may never arrive (Claude blocked on input).
138585
+ quietCheckers = /* @__PURE__ */ new Map();
138371
138586
  // In-flight start()/startFresh() calls keyed by sessionId. A second
138372
138587
  // concurrent resume for the same session (double-tap, client retry) awaits
138373
138588
  // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
@@ -138383,16 +138598,19 @@ var PTYManager = class {
138383
138598
  }
138384
138599
  // Resume an existing Claude conversation. sessionId is the JSONL UUID.
138385
138600
  //
138386
- // We use `--permission-mode acceptEdits` rather than `--dangerously-skip-permissions`.
138387
- // Both suppress file-edit prompts, but in an interactive (TUI) launch the
138388
- // skip-permissions flag renders a blocking "Bypass Permissions mode" warning
138389
- // menu on every boot that no known ~/.claude.json flag suppressed (as of
138390
- // Claude CLI v2.1.x) the session never reaches a usable prompt, so the
138391
- // mobile app shows an empty/stuck screen. `acceptEdits` auto-approves file edits
138601
+ // options.permissionMode defaults to `acceptEdits` rather than
138602
+ // `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
138603
+ // file-edit prompts, but in an interactive (TUI) launch the skip-permissions
138604
+ // flag renders a blocking "Bypass Permissions mode" warning menu on every
138605
+ // boot that no known ~/.claude.json flag suppressed (as of Claude CLI
138606
+ // v2.1.x) the session never reaches a usable prompt, so the mobile app
138607
+ // shows an empty/stuck screen. `acceptEdits` auto-approves file edits
138392
138608
  // without that warning gate, while still prompting for shell commands.
138609
+ // `manual` (prompt for everything) is the only other mode callers may pass;
138610
+ // `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
138393
138611
  // (The other first-run gates — onboarding/theme, workspace trust,
138394
138612
  // custom-API-key — are cleared by the seeded ~/.claude.json in
138395
- // docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
138613
+ // docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
138396
138614
  async start(sessionId, options) {
138397
138615
  const existing = this.sessions.get(sessionId);
138398
138616
  if (existing) return toPublicSession2(existing);
@@ -138411,7 +138629,7 @@ var PTYManager = class {
138411
138629
  resolveClaudeExe(),
138412
138630
  [
138413
138631
  "--permission-mode",
138414
- "acceptEdits",
138632
+ options.permissionMode ?? "acceptEdits",
138415
138633
  "--settings",
138416
138634
  '{"spinnerTipsEnabled":false}',
138417
138635
  "--resume",
@@ -138460,7 +138678,7 @@ var PTYManager = class {
138460
138678
  const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
138461
138679
  const args = [
138462
138680
  "--permission-mode",
138463
- "acceptEdits",
138681
+ options.permissionMode ?? "acceptEdits",
138464
138682
  "--settings",
138465
138683
  '{"spinnerTipsEnabled":false}',
138466
138684
  "--session-id",
@@ -138554,9 +138772,20 @@ var PTYManager = class {
138554
138772
  session.promptCount++;
138555
138773
  return session.promptCount;
138556
138774
  }
138557
- // Two-step paste-then-submit. Writes the bracketed-paste body, yields the
138558
- // event loop for SUBMIT_DELAY_MS, then writes \r. See buildPasteBytes() for
138559
- // why the split matters.
138775
+ // Two-step paste-then-submit. Writes the bracketed-paste body, then waits
138776
+ // for the PTY to go quiet before writing \r. See buildPasteBytes() for why
138777
+ // the split matters.
138778
+ //
138779
+ // The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
138780
+ // timer (the original fix) still races a TUI that's mid-redraw of its own
138781
+ // output (e.g. re-painting right after posting a question) when the paste
138782
+ // lands — the timer can elapse and fire \r while the TUI is still busy,
138783
+ // and that \r gets absorbed by the redraw instead of submitting (see
138784
+ // 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
138785
+ // but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
138786
+ // steps and only submitting once lastChunkAt hasn't advanced for a full
138787
+ // step gives the TUI as many extra ticks as it needs, capped at
138788
+ // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
138560
138789
  writeSubmit(sessionId, session, input, path2, promptCount) {
138561
138790
  const pasteBytes = buildPasteBytes(input);
138562
138791
  this.log.info(
@@ -138571,12 +138800,21 @@ var PTYManager = class {
138571
138800
  phase: "paste"
138572
138801
  }
138573
138802
  );
138803
+ const pasteAt = Date.now();
138574
138804
  session.process.write(pasteBytes);
138575
- setTimeout(() => {
138805
+ const trySubmit = () => {
138576
138806
  const current = this.sessions.get(sessionId);
138577
138807
  if (!current || current !== session) return;
138808
+ const now = Date.now();
138809
+ const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
138810
+ const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
138811
+ const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
138812
+ if (!quiet && !timedOut) {
138813
+ setTimeout(trySubmit, SUBMIT_DELAY_MS);
138814
+ return;
138815
+ }
138578
138816
  this.log.info(
138579
- `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
138817
+ `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
138580
138818
  {
138581
138819
  event: "pty.input_write",
138582
138820
  sessionId,
@@ -138584,11 +138822,14 @@ var PTYManager = class {
138584
138822
  byteLen: SUBMIT_BYTES2.length,
138585
138823
  digest: "\\r",
138586
138824
  path: path2,
138587
- phase: "submit"
138825
+ phase: "submit",
138826
+ waitedMs: now - pasteAt,
138827
+ timedOut
138588
138828
  }
138589
138829
  );
138590
138830
  current.process.write(SUBMIT_BYTES2);
138591
- }, SUBMIT_DELAY_MS);
138831
+ };
138832
+ setTimeout(trySubmit, SUBMIT_DELAY_MS);
138592
138833
  }
138593
138834
  // Drain any inputs that were sent while the session was still pendingReady,
138594
138835
  // writing them in arrival order now that Claude is at its prompt.
@@ -138637,6 +138878,8 @@ var PTYManager = class {
138637
138878
  this.permissionOpen.delete(sessionId);
138638
138879
  this.lastScreenQuestionKey.delete(sessionId);
138639
138880
  this.shellPromptOpen.delete(sessionId);
138881
+ this.quietCheckers.get(sessionId)?.cancel();
138882
+ this.quietCheckers.delete(sessionId);
138640
138883
  try {
138641
138884
  session.process.kill("SIGINT");
138642
138885
  } catch {
@@ -138696,6 +138939,8 @@ var PTYManager = class {
138696
138939
  this.firstChunkAt.clear();
138697
138940
  this.chunkIndex.clear();
138698
138941
  this.lastChunkAt.clear();
138942
+ for (const quiet of this.quietCheckers.values()) quiet.cancel();
138943
+ this.quietCheckers.clear();
138699
138944
  this.permissionOpen.clear();
138700
138945
  this.lastScreenQuestionKey.clear();
138701
138946
  this.shellPromptOpen.clear();
@@ -138749,6 +138994,12 @@ var PTYManager = class {
138749
138994
  err
138750
138995
  });
138751
138996
  });
138997
+ let quiet = this.quietCheckers.get(sessionId);
138998
+ if (!quiet) {
138999
+ quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
139000
+ this.quietCheckers.set(sessionId, quiet);
139001
+ }
139002
+ quiet();
138752
139003
  }
138753
139004
  // Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
138754
139005
  // menus from the rendered screen, firing the additive callbacks. Async because
@@ -138824,6 +139075,24 @@ var PTYManager = class {
138824
139075
  }
138825
139076
  }
138826
139077
  }
139078
+ // Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
139079
+ // ready/prompt detection handleOutput() runs per-chunk, using the last
139080
+ // rendered output — a session blocked on a prompt (or an unmarked boot
139081
+ // screen) may never produce another chunk to trigger detection otherwise.
139082
+ handleQuiet(sessionId) {
139083
+ const session = this.sessions.get(sessionId);
139084
+ if (session?.status !== "running") return;
139085
+ if (this.pendingReady.has(sessionId)) {
139086
+ this.markReady(sessionId, session, "quiet:timeout");
139087
+ }
139088
+ this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
139089
+ this.log.warn("[pty.prompt_detect] failed", {
139090
+ event: "pty.prompt_detect_failed",
139091
+ sessionId,
139092
+ err
139093
+ });
139094
+ });
139095
+ }
138827
139096
  // Transition a session from "running" to "waiting_input", clear pendingReady,
138828
139097
  // and flush any queued input. Idempotent: callers can invoke at any chunk.
138829
139098
  markReady(sessionId, session, reason) {
@@ -138864,6 +139133,8 @@ var PTYManager = class {
138864
139133
  this.permissionOpen.delete(sessionId);
138865
139134
  this.lastScreenQuestionKey.delete(sessionId);
138866
139135
  this.shellPromptOpen.delete(sessionId);
139136
+ this.quietCheckers.get(sessionId)?.cancel();
139137
+ this.quietCheckers.delete(sessionId);
138867
139138
  }
138868
139139
  };
138869
139140
  function toPublicSession2(s3) {
@@ -141091,7 +141362,7 @@ function pruneAgentConversations(cache) {
141091
141362
  missing += 1;
141092
141363
  continue;
141093
141364
  }
141094
- if (isAgentFile(row.file_path, cache.getAgentEntrypoints())) {
141365
+ if (cache.isAgentFileCached(row.file_path)) {
141095
141366
  cache.deleteByFilePath(row.file_path);
141096
141367
  pruned += 1;
141097
141368
  }
@@ -141511,37 +141782,6 @@ function computeConversationEtag({
141511
141782
  return `"${digest}"`;
141512
141783
  }
141513
141784
 
141514
- // src/utils/debounce.ts
141515
- function debounce(fn, waitMs) {
141516
- let timer = null;
141517
- let lastArgs = null;
141518
- const run2 = () => {
141519
- timer = null;
141520
- if (lastArgs) {
141521
- const args = lastArgs;
141522
- lastArgs = null;
141523
- fn(...args);
141524
- }
141525
- };
141526
- const debounced = (...args) => {
141527
- lastArgs = args;
141528
- if (timer) clearTimeout(timer);
141529
- timer = setTimeout(run2, waitMs);
141530
- };
141531
- debounced.cancel = () => {
141532
- if (timer) clearTimeout(timer);
141533
- timer = null;
141534
- lastArgs = null;
141535
- };
141536
- debounced.flush = () => {
141537
- if (timer) {
141538
- clearTimeout(timer);
141539
- run2();
141540
- }
141541
- };
141542
- return debounced;
141543
- }
141544
-
141545
141785
  // node_modules/date-fns/constants.js
141546
141786
  var daysInYear = 365.2425;
141547
141787
  var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
@@ -141900,6 +142140,7 @@ var WSHub = class {
141900
142140
  var BROWSE_SYSTEM_PROMPT = (browseRoot) => `You are working within the project boundary: ${browseRoot}. Do not read, write, or execute commands that access files or directories outside this boundary.`;
141901
142141
  var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
141902
142142
  var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
142143
+ var START_READY_TIMEOUT_MS = 15e3;
141903
142144
  function parseIncludeAgentsEnv(raw2) {
141904
142145
  if (raw2 === void 0) return false;
141905
142146
  const v3 = raw2.trim().toLowerCase();
@@ -141962,12 +142203,14 @@ var StreamerServer = class {
141962
142203
  disableDb = false;
141963
142204
  browseRoot = null;
141964
142205
  publicUrl = null;
142206
+ browserCors;
141965
142207
  pairTokens = new PairTokenStore();
141966
142208
  exchangeAttempts = /* @__PURE__ */ new Map();
141967
142209
  sessionStartAttempts = /* @__PURE__ */ new Map();
141968
142210
  sessionInputAttempts = /* @__PURE__ */ new Map();
141969
142211
  ptyGracePeriodMs;
141970
142212
  defaultSystemPrompt;
142213
+ defaultPermissionMode;
141971
142214
  // Map of sessionId → grace timer; fires to kill PTY after WS disconnect
141972
142215
  ptyGraceTimers = /* @__PURE__ */ new Map();
141973
142216
  // Map of sessionId → set of subscribed WS clients
@@ -142016,6 +142259,7 @@ var StreamerServer = class {
142016
142259
  this.codexRoots = config2.codexRoots ?? [(0, import_path21.join)((0, import_os10.homedir)(), ".codex", "sessions")];
142017
142260
  this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
142018
142261
  this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
142262
+ this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
142019
142263
  this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path21.join)((0, import_os10.homedir)(), ".threadbase", "cache");
142020
142264
  this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
142021
142265
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
@@ -142045,6 +142289,7 @@ var StreamerServer = class {
142045
142289
  this.log.warn(`Warning: ${result.error}`, { error: result.error });
142046
142290
  }
142047
142291
  }
142292
+ this.browserCors = config2.browserCors ?? loadBrowserCors();
142048
142293
  this.sessionStore = new SessionStore();
142049
142294
  this.wsHub = new WSHub();
142050
142295
  this.fileWatcher = new ConversationWatcher({
@@ -142199,6 +142444,7 @@ var StreamerServer = class {
142199
142444
  rotateApiKey: () => this.rotateApiKey(),
142200
142445
  publicUrl: this.publicUrl,
142201
142446
  browseRoot: this.browseRoot,
142447
+ browserCors: this.browserCors,
142202
142448
  ptyManager: this.ptyManager,
142203
142449
  sessionStore: this.sessionStore,
142204
142450
  wsHub: this.wsHub,
@@ -142445,9 +142691,9 @@ var StreamerServer = class {
142445
142691
  );
142446
142692
  this.scannerPersistenceDisabled = true;
142447
142693
  }
142448
- const warmupScanner = this.newScanner();
142449
- this.allScanners.add(warmupScanner);
142450
142694
  const warmupStatCache = this.buildStatCache(null);
142695
+ const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
142696
+ this.allScanners.add(warmupScanner);
142451
142697
  const shouldEmitProgress = createScanProgressThrottle();
142452
142698
  const scanOpts = {
142453
142699
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
@@ -142887,6 +143133,10 @@ var StreamerServer = class {
142887
143133
  }
142888
143134
  buildStatCache(previousScanner) {
142889
143135
  if (!this.cache) return void 0;
143136
+ if (!previousScanner) {
143137
+ const persisted = this.cache.getScannerStatCache();
143138
+ return persisted.size > 0 ? persisted : void 0;
143139
+ }
142890
143140
  const dbStats = this.cache.getFileStats();
142891
143141
  if (dbStats.size === 0) return void 0;
142892
143142
  const metaByPath = /* @__PURE__ */ new Map();
@@ -142916,9 +143166,9 @@ var StreamerServer = class {
142916
143166
  // this — its per-file refreshFile (in findConversationByUuid) already
142917
143167
  // reconciles the one conversation being requested, so paying a full-tree
142918
143168
  // rescan just because some OTHER file changed is the stall this avoids.
142919
- newScanner() {
143169
+ newScanner(options) {
142920
143170
  return new ConversationScanner(
142921
- this.scannerPersistenceDisabled ? { persistent: false } : void 0
143171
+ options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
142922
143172
  );
142923
143173
  }
142924
143174
  async getScanner(skipStaleRescan = false) {
@@ -142937,7 +143187,7 @@ var StreamerServer = class {
142937
143187
  }
142938
143188
  this.scannerStale = false;
142939
143189
  const statCache = this.buildStatCache(this.scanner);
142940
- this.scanner = this.newScanner();
143190
+ this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
142941
143191
  this.allScanners.add(this.scanner);
142942
143192
  this.scannerReady = this.scanner.scan({
142943
143193
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
@@ -142960,9 +143210,8 @@ var StreamerServer = class {
142960
143210
  this.scannerReady = null;
142961
143211
  return this.getScanner();
142962
143212
  }
142963
- // refresh=1's scan: reuse the WARM persistent scanner (its index.db + cursors
142964
- // survive, so classify() still skips unchanged files) and re-run its scan
142965
- // with fullRescan:true — the escape hatch that bypasses the scanner's
143213
+ // refresh=1's scan: reuse the WARM scanner and re-run its scan with
143214
+ // fullRescan:true the escape hatch that bypasses the scanner's
142966
143215
  // dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
142967
143216
  // the "don't trust the gate, check disk for real" signal. Unlike
142968
143217
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
@@ -143350,7 +143599,8 @@ var StreamerServer = class {
143350
143599
  provider,
143351
143600
  projectPath,
143352
143601
  projectName: body.projectName,
143353
- branch: body.branch
143602
+ branch: body.branch,
143603
+ permissionMode: this.defaultPermissionMode
143354
143604
  });
143355
143605
  this.sessionStore.addManaged(session);
143356
143606
  void this.watchConversationFile(sessionId);
@@ -143674,7 +143924,8 @@ var StreamerServer = class {
143674
143924
  const session = await this.ptyManager.start(convId, {
143675
143925
  projectPath,
143676
143926
  projectName,
143677
- branch
143927
+ branch,
143928
+ permissionMode: this.defaultPermissionMode
143678
143929
  });
143679
143930
  this.sessionStore.addManaged(session);
143680
143931
  void this.watchConversationFile(session.id);
@@ -143744,10 +143995,35 @@ var StreamerServer = class {
143744
143995
  provider,
143745
143996
  projectPath: resolvedPath,
143746
143997
  projectName: body.projectName,
143747
- systemPrompt: systemPromptParts.join("\n")
143998
+ systemPrompt: systemPromptParts.join("\n"),
143999
+ permissionMode: this.defaultPermissionMode
143748
144000
  });
143749
144001
  this.sessionStore.addManaged(session);
143750
- json2(res, 202, { id: session.id, status: "pending" });
144002
+ const readyOrFailed = new Promise((resolve6) => {
144003
+ const handler = (status) => {
144004
+ if (status === "waiting_input" || status === "idle") {
144005
+ this.sessionStatusBus.off(`status:${session.id}`, handler);
144006
+ resolve6(status === "waiting_input" ? "ready" : "failed");
144007
+ }
144008
+ };
144009
+ this.sessionStatusBus.on(`status:${session.id}`, handler);
144010
+ });
144011
+ const timeoutPromise = new Promise(
144012
+ (resolve6) => setTimeout(() => resolve6("timeout"), START_READY_TIMEOUT_MS)
144013
+ );
144014
+ const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
144015
+ const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
144016
+ if (outcome === "ready" && current) {
144017
+ json2(res, 200, { session: current });
144018
+ } else if (outcome === "failed" && current) {
144019
+ json2(res, 502, {
144020
+ id: session.id,
144021
+ status: "idle",
144022
+ error: current.failureReason ?? "Session exited before becoming ready"
144023
+ });
144024
+ } else {
144025
+ json2(res, 202, { id: session.id, status: "pending" });
144026
+ }
143751
144027
  if (provider === CODEX_CLI_PROVIDER2) {
143752
144028
  this.watchForCodexRollout(session.id, resolvedPath);
143753
144029
  } else {
@@ -147937,7 +148213,10 @@ program2.name("threadbase-streamer").description("PTY session management, WebSoc
147937
148213
  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(
147938
148214
  "--public-url <url>",
147939
148215
  "Public URL clients should use to reach this server (https:// required, except localhost). Falls back to THREADBASE_PUBLIC_URL env or public_url: in ~/.threadbase/server.yaml."
147940
- ).option("--no-pair-qr", "Skip the pairing QR on startup", false).option("--replace-prod", "Stop the launchd-supervised prod streamer and bind its port", false).option("--forget", "Clear this repo's remembered dev-vs-prod choice and re-prompt", false).option("--forget-all", "Clear every repo's remembered dev-vs-prod choice", false).option(
148216
+ ).option(
148217
+ "--default-permission-mode <mode>",
148218
+ "Claude Code permission mode for spawned sessions: acceptEdits (auto-approve file edits, default) or manual (prompt for everything). Falls back to default_permission_mode: in ~/.threadbase/server.yaml, or a first-run interactive prompt on a human TTY invocation (skip with THREADBASE_SKIP_PERMISSION_MODE_PROMPT=true)."
148219
+ ).option("--no-pair-qr", "Skip the pairing QR on startup").option("--replace-prod", "Stop the launchd-supervised prod streamer and bind its port", false).option("--forget", "Clear this repo's remembered dev-vs-prod choice and re-prompt", false).option("--forget-all", "Clear every repo's remembered dev-vs-prod choice", false).option(
147941
148220
  "--prod",
147942
148221
  "Run as if invoked by launchd: skip the dev-takeover prompt and signal handlers",
147943
148222
  false
@@ -147956,11 +148235,25 @@ program2.command("serve").description("Start the streamer server").option("-p, -
147956
148235
  if (opts.multiAgentFlow) {
147957
148236
  process.env.MULTI_AGENT_FLOW = "true";
147958
148237
  }
148238
+ if (opts.defaultPermissionMode !== void 0 && opts.defaultPermissionMode !== "acceptEdits" && opts.defaultPermissionMode !== "manual") {
148239
+ log7.error(
148240
+ `Invalid --default-permission-mode: ${opts.defaultPermissionMode}`,
148241
+ void 0,
148242
+ "console"
148243
+ );
148244
+ process.exit(1);
148245
+ }
147959
148246
  const requestedPort = Number.parseInt(opts.port, 10);
147960
148247
  const apiKey = opts.apiKey ?? loadOrCreateApiKey();
147961
148248
  const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
147962
148249
  const isProdInvocation = opts.prod === true || process.ppid === 1;
147963
148250
  if (!isProdInvocation) appendDevSessionMarker();
148251
+ let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
148252
+ if (resolvedDefaultPermissionMode === void 0 && !isProdInvocation && process.env.THREADBASE_SKIP_PERMISSION_MODE_PROMPT !== "true" && loadDefaultPermissionMode() === void 0 && import_node_process3.stdin.isTTY) {
148253
+ const { interactivePermissionModePrompt: interactivePermissionModePrompt2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
148254
+ resolvedDefaultPermissionMode = await interactivePermissionModePrompt2();
148255
+ setDefaultPermissionMode(resolvedDefaultPermissionMode);
148256
+ }
147964
148257
  let resolvedPort = requestedPort;
147965
148258
  if (!isProdInvocation) {
147966
148259
  const { resolveDevPlan: resolveDevPlan2, detectProdActive: detectProdActive2, isPortInUse: isPortInUse2, findFreePort: findFreePort2, takeoverProd: takeoverProd2 } = await Promise.resolve().then(() => (init_dev_takeover(), dev_takeover_exports));
@@ -147992,7 +148285,8 @@ program2.command("serve").description("Start the streamer server").option("-p, -
147992
148285
  verbose: opts.verbose,
147993
148286
  logMenubarRequests: opts.logMenubarRequests,
147994
148287
  browseRoot: opts.browseRoot,
147995
- publicUrl: opts.publicUrl
148288
+ publicUrl: opts.publicUrl,
148289
+ defaultPermissionMode: resolvedDefaultPermissionMode
147996
148290
  });
147997
148291
  await server.listen(resolvedPort);
147998
148292
  {
@@ -148006,13 +148300,21 @@ program2.command("serve").description("Start the streamer server").option("-p, -
148006
148300
  wsUrl: `ws://localhost:${resolvedPort}/ws`
148007
148301
  });
148008
148302
  log7.info(`API key: ${apiKey}`, { apiKeyMasked: `${apiKey.slice(0, 6)}\u2026` });
148009
- if (opts.pairQr !== false) {
148010
- try {
148011
- await printPairQR({ port: resolvedPort, apiKey, publicUrl });
148012
- } catch (err) {
148013
- const message = err instanceof Error ? err.message : String(err);
148014
- log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
148015
- }
148303
+ try {
148304
+ await printServerBanner({
148305
+ port: resolvedPort,
148306
+ apiKey,
148307
+ publicUrl,
148308
+ includeQr: opts.pairQr !== false
148309
+ });
148310
+ } catch (err) {
148311
+ const message = err instanceof Error ? err.message : String(err);
148312
+ log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
148313
+ log7.info(
148314
+ printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
148315
+ void 0,
148316
+ "console"
148317
+ );
148016
148318
  }
148017
148319
  const shutdown = async () => {
148018
148320
  log7.info("Shutting down...");
@@ -148067,7 +148369,7 @@ program2.command("pair").description("Print a pairing QR code (server must alrea
148067
148369
  const port = Number.parseInt(opts.port, 10);
148068
148370
  const apiKey = loadOrCreateApiKey();
148069
148371
  const publicUrl = loadPublicUrl() ?? null;
148070
- await printPairQR({ port, apiKey, publicUrl });
148372
+ await printServerBanner({ port, apiKey, publicUrl, includeQr: true });
148071
148373
  });
148072
148374
  program2.command("set-key [key]").description("Set the streamer API key in ~/.threadbase/server.yaml").action(async (key) => {
148073
148375
  const { runSetKey: runSetKey2 } = await Promise.resolve().then(() => (init_setKey(), setKey_exports));
@@ -148163,11 +148465,49 @@ program2.command("update").description("Check for streamer updates from GitHub R
148163
148465
  });
148164
148466
  registerProdCommands(program2);
148165
148467
  program2.parse();
148166
- async function printPairQR({
148468
+ function generateQr(payload) {
148469
+ return new Promise((resolve6) => {
148470
+ import_qrcode_terminal.default.generate(payload, { small: true }, resolve6);
148471
+ });
148472
+ }
148473
+ function printUrlBanner({
148474
+ url: url2,
148475
+ qr: qr2,
148476
+ expiresAt
148477
+ }) {
148478
+ const contentLines = ["Threadbase Streamer \u2014 server address", "", url2];
148479
+ if (qr2) {
148480
+ contentLines.push("", ...qr2.split("\n").filter((l) => l.length > 0));
148481
+ if (expiresAt !== void 0) {
148482
+ contentLines.push(
148483
+ "",
148484
+ `Scan to pair a mobile client (expires ${new Date(expiresAt).toLocaleTimeString()})`
148485
+ );
148486
+ } else {
148487
+ contentLines.push("", "Scan to pair a mobile client");
148488
+ }
148489
+ }
148490
+ const width = Math.max(...contentLines.map((l) => l.length));
148491
+ const pad = (l) => `\u2551 ${l}${" ".repeat(width - l.length)} \u2551`;
148492
+ const top = `\u2554${"\u2550".repeat(width + 2)}\u2557`;
148493
+ const bottom = `\u255A${"\u2550".repeat(width + 2)}\u255D`;
148494
+ return `
148495
+ ${top}
148496
+ ${contentLines.map(pad).join("\n")}
148497
+ ${bottom}
148498
+ `;
148499
+ }
148500
+ async function printServerBanner({
148167
148501
  port,
148168
148502
  apiKey,
148169
- publicUrl
148503
+ publicUrl,
148504
+ includeQr
148170
148505
  }) {
148506
+ const url2 = resolveServerUrl({ publicUrl, port });
148507
+ if (!includeQr) {
148508
+ log7.info(printUrlBanner({ url: url2 }), void 0, "console");
148509
+ return;
148510
+ }
148171
148511
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
148172
148512
  method: "POST",
148173
148513
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
@@ -148177,19 +148517,12 @@ async function printPairQR({
148177
148517
  throw new Error(`/api/pair/start returned ${res.status}`);
148178
148518
  }
148179
148519
  const { token, expiresAt, expiresInSeconds } = await res.json();
148180
- const url2 = resolveServerUrl({ publicUrl, port });
148181
148520
  const expSeconds = Math.floor(expiresAt / 1e3);
148182
148521
  const payload = `threadbase://pair?url=${encodeURIComponent(url2)}&token=${token}&exp=${expSeconds}`;
148183
- log7.info("Scan to pair a mobile client:\n", void 0, "console");
148184
- import_qrcode_terminal.default.generate(payload, { small: true });
148185
- log7.info(`Server URL : ${url2}`, void 0, "console");
148186
- log7.info(`Pair URL : ${payload}`, void 0, "console");
148187
- log7.info(
148188
- `Expires : ${new Date(expiresAt).toLocaleTimeString()} (${expiresInSeconds}s)
148189
- `,
148190
- void 0,
148191
- "console"
148192
- );
148522
+ const qr2 = await generateQr(payload);
148523
+ log7.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
148524
+ log7.info(`Pair URL: ${payload}`, void 0, "console");
148525
+ log7.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
148193
148526
  }
148194
148527
  /*! Bundled license information:
148195
148528