@threadbase-sh/streamer 1.24.7 → 1.25.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
@@ -1548,6 +1548,42 @@ function loadTailSize() {
1548
1548
  }
1549
1549
  return void 0;
1550
1550
  }
1551
+ function loadDefaultPermissionMode() {
1552
+ try {
1553
+ const content = (0, import_fs.readFileSync)(configFile(), "utf-8");
1554
+ const match2 = content.match(/default_permission_mode:\s*(\S+)/);
1555
+ const value = match2?.[1]?.trim();
1556
+ if (value === "acceptEdits" || value === "manual") return value;
1557
+ } catch {
1558
+ }
1559
+ return void 0;
1560
+ }
1561
+ function setDefaultPermissionMode(mode) {
1562
+ const file2 = configFile();
1563
+ (0, import_fs.mkdirSync)(configDir(), { recursive: true });
1564
+ let content = "";
1565
+ try {
1566
+ content = (0, import_fs.readFileSync)(file2, "utf-8");
1567
+ } catch (err) {
1568
+ if (err.code !== "ENOENT") throw err;
1569
+ }
1570
+ const line = `default_permission_mode: ${mode}`;
1571
+ let updated;
1572
+ if (/^default_permission_mode:\s*.+$/m.test(content)) {
1573
+ updated = content.replace(/^default_permission_mode:\s*.+$/m, line);
1574
+ } else if (content.length === 0 || content.endsWith("\n")) {
1575
+ updated = `${content}${line}
1576
+ `;
1577
+ } else {
1578
+ updated = `${content}
1579
+ ${line}
1580
+ `;
1581
+ }
1582
+ const tmpFile = `${file2}.tmp`;
1583
+ (0, import_fs.writeFileSync)(tmpFile, updated, { encoding: "utf-8", mode: 384 });
1584
+ (0, import_fs.chmodSync)(tmpFile, 384);
1585
+ (0, import_fs.renameSync)(tmpFile, file2);
1586
+ }
1551
1587
  function validatePublicUrl(raw2) {
1552
1588
  let parsed;
1553
1589
  try {
@@ -123699,6 +123735,68 @@ var init_check_sqlite_abi = __esm({
123699
123735
  }
123700
123736
  });
123701
123737
 
123738
+ // src/lifecycle/prompt.ts
123739
+ var prompt_exports = {};
123740
+ __export(prompt_exports, {
123741
+ interactivePermissionModePrompt: () => interactivePermissionModePrompt,
123742
+ interactivePrompt: () => interactivePrompt
123743
+ });
123744
+ var import_node_process2, import_promises18, interactivePrompt, interactivePermissionModePrompt;
123745
+ var init_prompt = __esm({
123746
+ "src/lifecycle/prompt.ts"() {
123747
+ "use strict";
123748
+ import_node_process2 = require("process");
123749
+ import_promises18 = require("readline/promises");
123750
+ interactivePrompt = async ({ prodPort, suggestedAltPort, prodActive }) => {
123751
+ const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
123752
+ try {
123753
+ if (!prodActive) {
123754
+ import_node_process2.stdout.write(
123755
+ `
123756
+ Port ${prodPort} is already in use by another process.
123757
+ Running dev on port ${suggestedAltPort} instead.
123758
+ `
123759
+ );
123760
+ const rememberAns2 = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
123761
+ return {
123762
+ choice: "use-port",
123763
+ port: suggestedAltPort,
123764
+ remember: rememberAns2 === "y" || rememberAns2 === "yes"
123765
+ };
123766
+ }
123767
+ import_node_process2.stdout.write(
123768
+ `
123769
+ The supervised prod streamer is already holding port ${prodPort}.
123770
+ [r] Stop prod and take port ${prodPort}
123771
+ [p] Run dev on port ${suggestedAltPort} instead
123772
+ `
123773
+ );
123774
+ const choiceAns = (await rl.question("Choice [r/p]: ")).trim().toLowerCase();
123775
+ const rememberAns = (await rl.question("Remember this choice for this repo? [y/N]: ")).trim().toLowerCase();
123776
+ const remember = rememberAns === "y" || rememberAns === "yes";
123777
+ if (choiceAns === "r") {
123778
+ return { choice: "replace-prod", remember };
123779
+ }
123780
+ return { choice: "use-port", port: suggestedAltPort, remember };
123781
+ } finally {
123782
+ rl.close();
123783
+ }
123784
+ };
123785
+ interactivePermissionModePrompt = async () => {
123786
+ const rl = (0, import_promises18.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
123787
+ try {
123788
+ import_node_process2.stdout.write(
123789
+ "\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"
123790
+ );
123791
+ const ans = (await rl.question("Choice [a/m] (default a): ")).trim().toLowerCase();
123792
+ return ans === "m" ? "manual" : "acceptEdits";
123793
+ } finally {
123794
+ rl.close();
123795
+ }
123796
+ };
123797
+ }
123798
+ });
123799
+
123702
123800
  // src/lifecycle/prefs.ts
123703
123801
  function readPrefs() {
123704
123802
  const path2 = prefsPath();
@@ -123781,7 +123879,7 @@ async function resolveDevPlan(opts) {
123781
123879
  }
123782
123880
  const prodActive = opts.isProdActive();
123783
123881
  const portTaken = opts.portInUse(opts.requestedPort);
123784
- if (!prodActive && !portTaken) {
123882
+ if (!portTaken) {
123785
123883
  return { kind: "use-port", port: opts.requestedPort };
123786
123884
  }
123787
123885
  if (!opts.forget) {
@@ -123796,7 +123894,12 @@ async function resolveDevPlan(opts) {
123796
123894
  }
123797
123895
  }
123798
123896
  const suggested = await opts.findFreePort(opts.requestedPort + 1);
123799
- const answer = await opts.prompt({ prodPort: opts.requestedPort, suggestedAltPort: suggested });
123897
+ const answer = await opts.prompt({
123898
+ prodPort: opts.requestedPort,
123899
+ suggestedAltPort: suggested,
123900
+ prodActive,
123901
+ portTaken
123902
+ });
123800
123903
  if (answer.remember && opts.repoToplevel) {
123801
123904
  if (answer.choice === "replace-prod") {
123802
123905
  writePrefForRepo(opts.repoToplevel, { choice: "replace-prod" });
@@ -123902,41 +124005,6 @@ var init_dev_takeover = __esm({
123902
124005
  }
123903
124006
  });
123904
124007
 
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
124008
  // src/lifecycle/repo.ts
123941
124009
  var repo_exports = {};
123942
124010
  __export(repo_exports, {
@@ -124017,6 +124085,9 @@ var init_setKey = __esm({
124017
124085
  );
124018
124086
  })();
124019
124087
 
124088
+ // cli/index.ts
124089
+ var import_node_process3 = require("process");
124090
+
124020
124091
  // node_modules/commander/lib/error.js
124021
124092
  var CommanderError = class extends Error {
124022
124093
  /**
@@ -136542,11 +136613,11 @@ var ConversationCache = class _ConversationCache {
136542
136613
  INSERT INTO conversation_meta
136543
136614
  (id, file_path, project_path, project_name, title, model, account, branch,
136544
136615
  message_count, last_activity, first_message, last_message, preview, updated_at,
136545
- mtime_ms, file_size, provider)
136616
+ mtime_ms, file_size, provider, scanner_meta_json)
136546
136617
  VALUES
136547
136618
  (@id, @file_path, @project_path, @project_name, @title, @model, @account, @branch,
136548
136619
  @message_count, @last_activity, @first_message, @last_message, @preview, @updated_at,
136549
- @mtime_ms, @file_size, @provider)
136620
+ @mtime_ms, @file_size, @provider, @scanner_meta_json)
136550
136621
  ON CONFLICT(id) DO UPDATE SET
136551
136622
  file_path = excluded.file_path,
136552
136623
  project_path = excluded.project_path,
@@ -136566,7 +136637,8 @@ var ConversationCache = class _ConversationCache {
136566
136637
  updated_at = excluded.updated_at,
136567
136638
  mtime_ms = excluded.mtime_ms,
136568
136639
  file_size = excluded.file_size,
136569
- provider = excluded.provider
136640
+ provider = excluded.provider,
136641
+ scanner_meta_json = excluded.scanner_meta_json
136570
136642
  WHERE conversation_meta.updated_at < excluded.updated_at
136571
136643
  `),
136572
136644
  getTail: db.prepare("SELECT * FROM conversation_tail WHERE conversation_id = ?"),
@@ -136603,6 +136675,27 @@ var ConversationCache = class _ConversationCache {
136603
136675
  allFileStats: db.prepare(
136604
136676
  "SELECT file_path, mtime_ms, file_size FROM conversation_meta WHERE mtime_ms IS NOT NULL AND file_size IS NOT NULL"
136605
136677
  ),
136678
+ allScannerStatCacheRows: db.prepare(
136679
+ "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"
136680
+ ),
136681
+ updateScannerCache: db.prepare(
136682
+ "UPDATE conversation_meta SET mtime_ms = ?, file_size = ?, scanner_meta_json = ? WHERE id = ?"
136683
+ ),
136684
+ getFileMetadata: db.prepare(
136685
+ "SELECT mtime_ms, file_size, is_agent, agent_entrypoints_key FROM conversation_file_metadata WHERE file_path = ?"
136686
+ ),
136687
+ upsertFileMetadata: db.prepare(`
136688
+ INSERT INTO conversation_file_metadata
136689
+ (file_path, mtime_ms, file_size, is_agent, agent_entrypoints_key, updated_at)
136690
+ VALUES
136691
+ (@file_path, @mtime_ms, @file_size, @is_agent, @agent_entrypoints_key, @updated_at)
136692
+ ON CONFLICT(file_path) DO UPDATE SET
136693
+ mtime_ms = excluded.mtime_ms,
136694
+ file_size = excluded.file_size,
136695
+ is_agent = excluded.is_agent,
136696
+ agent_entrypoints_key = excluded.agent_entrypoints_key,
136697
+ updated_at = excluded.updated_at
136698
+ `),
136606
136699
  upsertSessionName: db.prepare(`
136607
136700
  INSERT INTO session_names (session_id, name, updated_at)
136608
136701
  VALUES (?, ?, ?)
@@ -136643,8 +136736,35 @@ var ConversationCache = class _ConversationCache {
136643
136736
  getDatabase() {
136644
136737
  return this.db;
136645
136738
  }
136646
- getAgentEntrypoints() {
136647
- return this.agentEntrypoints;
136739
+ agentEntrypointsKey() {
136740
+ return [...this.agentEntrypoints].sort().join(",");
136741
+ }
136742
+ classifyAgentFile(filePath, mtimeMs, fileSize) {
136743
+ if (this.agentEntrypoints.size === 0) return false;
136744
+ const entrypointsKey = this.agentEntrypointsKey();
136745
+ const cached3 = this.stmts.getFileMetadata.get(filePath);
136746
+ if (cached3 && cached3.mtime_ms === mtimeMs && cached3.file_size === fileSize && cached3.agent_entrypoints_key === entrypointsKey) {
136747
+ return cached3.is_agent === 1;
136748
+ }
136749
+ const isAgent = isAgentFile(filePath, this.agentEntrypoints);
136750
+ this.stmts.upsertFileMetadata.run({
136751
+ file_path: filePath,
136752
+ mtime_ms: mtimeMs,
136753
+ file_size: fileSize,
136754
+ is_agent: isAgent ? 1 : 0,
136755
+ agent_entrypoints_key: entrypointsKey,
136756
+ updated_at: Date.now()
136757
+ });
136758
+ return isAgent;
136759
+ }
136760
+ isAgentFileCached(filePath) {
136761
+ let s3;
136762
+ try {
136763
+ s3 = (0, import_fs16.statSync)(filePath);
136764
+ } catch {
136765
+ return false;
136766
+ }
136767
+ return this.classifyAgentFile(filePath, s3.mtimeMs, s3.size);
136648
136768
  }
136649
136769
  static open(dbPath, tailSize = 10, migrationsDir, options) {
136650
136770
  (0, import_fs16.mkdirSync)((0, import_path12.dirname)(dbPath), { recursive: true });
@@ -136820,11 +136940,9 @@ var ConversationCache = class _ConversationCache {
136820
136940
  // in conversation-cache.test.ts).
136821
136941
  upsertFromScannerMeta(metas) {
136822
136942
  const filter = this.filterAgentConversations;
136823
- const entrypoints = this.agentEntrypoints;
136824
136943
  const upsertedIds = [];
136825
136944
  const run2 = this.db.transaction((items) => {
136826
136945
  for (const m2 of items) {
136827
- if (filter && isAgentFile(m2.filePath, entrypoints)) continue;
136828
136946
  const id = m2.sessionId || m2.id.split("/").pop()?.replace(/\.jsonl$/, "") || m2.id;
136829
136947
  const lastActivityMs = m2.timestamp ? new Date(m2.timestamp).getTime() : null;
136830
136948
  let mtimeMs = null;
@@ -136836,6 +136954,10 @@ var ConversationCache = class _ConversationCache {
136836
136954
  } catch {
136837
136955
  }
136838
136956
  const seq = ++this.tailSeq;
136957
+ if (filter && mtimeMs !== null && fileSize !== null && this.classifyAgentFile(m2.filePath, mtimeMs, fileSize)) {
136958
+ continue;
136959
+ }
136960
+ const scannerMetaJson = JSON.stringify(m2);
136839
136961
  this.stmts.upsertFull.run({
136840
136962
  id,
136841
136963
  file_path: m2.filePath,
@@ -136853,8 +136975,10 @@ var ConversationCache = class _ConversationCache {
136853
136975
  updated_at: seq,
136854
136976
  mtime_ms: mtimeMs,
136855
136977
  file_size: fileSize,
136856
- provider: m2.provider ?? CLAUDE_CODE_PROVIDER2
136978
+ provider: m2.provider ?? CLAUDE_CODE_PROVIDER2,
136979
+ scanner_meta_json: scannerMetaJson
136857
136980
  });
136981
+ this.stmts.updateScannerCache.run(mtimeMs, fileSize, scannerMetaJson, id);
136858
136982
  if (this.fileIndexLoaded) this.fileIndex.set(m2.filePath, id);
136859
136983
  upsertedIds.push(id);
136860
136984
  }
@@ -136978,6 +137102,21 @@ var ConversationCache = class _ConversationCache {
136978
137102
  }
136979
137103
  return map2;
136980
137104
  }
137105
+ getScannerStatCache() {
137106
+ const rows = this.stmts.allScannerStatCacheRows.all();
137107
+ const map2 = /* @__PURE__ */ new Map();
137108
+ for (const r of rows) {
137109
+ try {
137110
+ const meta3 = JSON.parse(r.scanner_meta_json);
137111
+ map2.set(r.file_path, {
137112
+ stat: { mtimeMs: r.mtime_ms, size: r.file_size },
137113
+ meta: meta3
137114
+ });
137115
+ } catch {
137116
+ }
137117
+ }
137118
+ return map2;
137119
+ }
136981
137120
  getMetaById(id) {
136982
137121
  const row = this.stmts.getFullById.get(id);
136983
137122
  if (!row) return null;
@@ -138280,6 +138419,37 @@ function detectShellPrompt(lines) {
138280
138419
  return null;
138281
138420
  }
138282
138421
 
138422
+ // src/utils/debounce.ts
138423
+ function debounce(fn, waitMs) {
138424
+ let timer = null;
138425
+ let lastArgs = null;
138426
+ const run2 = () => {
138427
+ timer = null;
138428
+ if (lastArgs) {
138429
+ const args = lastArgs;
138430
+ lastArgs = null;
138431
+ fn(...args);
138432
+ }
138433
+ };
138434
+ const debounced = (...args) => {
138435
+ lastArgs = args;
138436
+ if (timer) clearTimeout(timer);
138437
+ timer = setTimeout(run2, waitMs);
138438
+ };
138439
+ debounced.cancel = () => {
138440
+ if (timer) clearTimeout(timer);
138441
+ timer = null;
138442
+ lastArgs = null;
138443
+ };
138444
+ debounced.flush = () => {
138445
+ if (timer) {
138446
+ clearTimeout(timer);
138447
+ run2();
138448
+ }
138449
+ };
138450
+ return debounced;
138451
+ }
138452
+
138283
138453
  // src/pty-manager.ts
138284
138454
  var OUTPUT_BUFFER_MAX2 = 65536;
138285
138455
  var PTY_COLS2 = 120;
@@ -138287,11 +138457,13 @@ var PTY_ROWS2 = 40;
138287
138457
  var SCREEN_SCROLLBACK2 = 1e3;
138288
138458
  var CLAUDE_PROMPT_MARKERS = ["\u256D", "\u276F"];
138289
138459
  var PROMPT_MARKER_FALLBACK_MS = 1e4;
138460
+ var QUIET_DETECT_MS = 500;
138290
138461
  function buildPasteBytes(input) {
138291
138462
  return `\x1B[200~${input}\x1B[201~`;
138292
138463
  }
138293
138464
  var SUBMIT_BYTES2 = "\r";
138294
138465
  var SUBMIT_DELAY_MS = 16;
138466
+ var SUBMIT_MAX_WAIT_MS = 500;
138295
138467
  function digestBytes2(s3) {
138296
138468
  const escaped = s3.replace(new RegExp(String.fromCharCode(27), "g"), "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
138297
138469
  if (escaped.length <= 200) return escaped;
@@ -138368,6 +138540,10 @@ var PTYManager = class {
138368
138540
  // to a given input or fell silent. Reset on dispose().
138369
138541
  chunkIndex = /* @__PURE__ */ new Map();
138370
138542
  lastChunkAt = /* @__PURE__ */ new Map();
138543
+ // Per-session debounced "went quiet" checker, re-armed on every chunk. Fires
138544
+ // QUIET_DETECT_MS after the last chunk so ready/prompt detection doesn't
138545
+ // wait for another chunk that may never arrive (Claude blocked on input).
138546
+ quietCheckers = /* @__PURE__ */ new Map();
138371
138547
  // In-flight start()/startFresh() calls keyed by sessionId. A second
138372
138548
  // concurrent resume for the same session (double-tap, client retry) awaits
138373
138549
  // the first call's promise instead of spawning a duplicate PTY (CRITICAL #3).
@@ -138383,16 +138559,19 @@ var PTYManager = class {
138383
138559
  }
138384
138560
  // Resume an existing Claude conversation. sessionId is the JSONL UUID.
138385
138561
  //
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
138562
+ // options.permissionMode defaults to `acceptEdits` rather than
138563
+ // `--dangerously-skip-permissions`/`bypassPermissions`. Both suppress
138564
+ // file-edit prompts, but in an interactive (TUI) launch the skip-permissions
138565
+ // flag renders a blocking "Bypass Permissions mode" warning menu on every
138566
+ // boot that no known ~/.claude.json flag suppressed (as of Claude CLI
138567
+ // v2.1.x) the session never reaches a usable prompt, so the mobile app
138568
+ // shows an empty/stuck screen. `acceptEdits` auto-approves file edits
138392
138569
  // without that warning gate, while still prompting for shell commands.
138570
+ // `manual` (prompt for everything) is the only other mode callers may pass;
138571
+ // `bypassPermissions`/`plan`/`dontAsk`/`auto` are not supported here.
138393
138572
  // (The other first-run gates — onboarding/theme, workspace trust,
138394
138573
  // custom-API-key — are cleared by the seeded ~/.claude.json in
138395
- // docker/entrypoint.sh.) startFresh() uses the same flag for the same reason.
138574
+ // docker/entrypoint.sh.) startFresh() uses the same default for the same reason.
138396
138575
  async start(sessionId, options) {
138397
138576
  const existing = this.sessions.get(sessionId);
138398
138577
  if (existing) return toPublicSession2(existing);
@@ -138411,7 +138590,7 @@ var PTYManager = class {
138411
138590
  resolveClaudeExe(),
138412
138591
  [
138413
138592
  "--permission-mode",
138414
- "acceptEdits",
138593
+ options.permissionMode ?? "acceptEdits",
138415
138594
  "--settings",
138416
138595
  '{"spinnerTipsEnabled":false}',
138417
138596
  "--resume",
@@ -138460,7 +138639,7 @@ var PTYManager = class {
138460
138639
  const projectName = options.projectName ?? (0, import_path17.basename)(options.projectPath);
138461
138640
  const args = [
138462
138641
  "--permission-mode",
138463
- "acceptEdits",
138642
+ options.permissionMode ?? "acceptEdits",
138464
138643
  "--settings",
138465
138644
  '{"spinnerTipsEnabled":false}',
138466
138645
  "--session-id",
@@ -138554,9 +138733,20 @@ var PTYManager = class {
138554
138733
  session.promptCount++;
138555
138734
  return session.promptCount;
138556
138735
  }
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.
138736
+ // Two-step paste-then-submit. Writes the bracketed-paste body, then waits
138737
+ // for the PTY to go quiet before writing \r. See buildPasteBytes() for why
138738
+ // the split matters.
138739
+ //
138740
+ // The wait is quiescence-based, not a flat delay: a fixed SUBMIT_DELAY_MS
138741
+ // timer (the original fix) still races a TUI that's mid-redraw of its own
138742
+ // output (e.g. re-painting right after posting a question) when the paste
138743
+ // lands — the timer can elapse and fire \r while the TUI is still busy,
138744
+ // and that \r gets absorbed by the redraw instead of submitting (see
138745
+ // 2026-07 session 14dda340: a "Yes" reply was accepted into the input line
138746
+ // but never landed as a submitted JSONL turn). Polling in SUBMIT_DELAY_MS
138747
+ // steps and only submitting once lastChunkAt hasn't advanced for a full
138748
+ // step gives the TUI as many extra ticks as it needs, capped at
138749
+ // SUBMIT_MAX_WAIT_MS so a silent/wedged PTY still gets its \r eventually.
138560
138750
  writeSubmit(sessionId, session, input, path2, promptCount) {
138561
138751
  const pasteBytes = buildPasteBytes(input);
138562
138752
  this.log.info(
@@ -138571,12 +138761,21 @@ var PTYManager = class {
138571
138761
  phase: "paste"
138572
138762
  }
138573
138763
  );
138764
+ const pasteAt = Date.now();
138574
138765
  session.process.write(pasteBytes);
138575
- setTimeout(() => {
138766
+ const trySubmit = () => {
138576
138767
  const current = this.sessions.get(sessionId);
138577
138768
  if (!current || current !== session) return;
138769
+ const now = Date.now();
138770
+ const lastChunk = this.lastChunkAt.get(sessionId) ?? pasteAt;
138771
+ const quiet = now - lastChunk >= SUBMIT_DELAY_MS;
138772
+ const timedOut = now - pasteAt >= SUBMIT_MAX_WAIT_MS;
138773
+ if (!quiet && !timedOut) {
138774
+ setTimeout(trySubmit, SUBMIT_DELAY_MS);
138775
+ return;
138776
+ }
138578
138777
  this.log.info(
138579
- `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r`,
138778
+ `[pty.input.submit] ${sessionId.slice(0, 8)} promptCount=${promptCount} digest=\\r waitedMs=${now - pasteAt} timedOut=${timedOut}`,
138580
138779
  {
138581
138780
  event: "pty.input_write",
138582
138781
  sessionId,
@@ -138584,11 +138783,14 @@ var PTYManager = class {
138584
138783
  byteLen: SUBMIT_BYTES2.length,
138585
138784
  digest: "\\r",
138586
138785
  path: path2,
138587
- phase: "submit"
138786
+ phase: "submit",
138787
+ waitedMs: now - pasteAt,
138788
+ timedOut
138588
138789
  }
138589
138790
  );
138590
138791
  current.process.write(SUBMIT_BYTES2);
138591
- }, SUBMIT_DELAY_MS);
138792
+ };
138793
+ setTimeout(trySubmit, SUBMIT_DELAY_MS);
138592
138794
  }
138593
138795
  // Drain any inputs that were sent while the session was still pendingReady,
138594
138796
  // writing them in arrival order now that Claude is at its prompt.
@@ -138637,6 +138839,8 @@ var PTYManager = class {
138637
138839
  this.permissionOpen.delete(sessionId);
138638
138840
  this.lastScreenQuestionKey.delete(sessionId);
138639
138841
  this.shellPromptOpen.delete(sessionId);
138842
+ this.quietCheckers.get(sessionId)?.cancel();
138843
+ this.quietCheckers.delete(sessionId);
138640
138844
  try {
138641
138845
  session.process.kill("SIGINT");
138642
138846
  } catch {
@@ -138696,6 +138900,8 @@ var PTYManager = class {
138696
138900
  this.firstChunkAt.clear();
138697
138901
  this.chunkIndex.clear();
138698
138902
  this.lastChunkAt.clear();
138903
+ for (const quiet of this.quietCheckers.values()) quiet.cancel();
138904
+ this.quietCheckers.clear();
138699
138905
  this.permissionOpen.clear();
138700
138906
  this.lastScreenQuestionKey.clear();
138701
138907
  this.shellPromptOpen.clear();
@@ -138749,6 +138955,12 @@ var PTYManager = class {
138749
138955
  err
138750
138956
  });
138751
138957
  });
138958
+ let quiet = this.quietCheckers.get(sessionId);
138959
+ if (!quiet) {
138960
+ quiet = debounce(() => this.handleQuiet(sessionId), QUIET_DETECT_MS);
138961
+ this.quietCheckers.set(sessionId, quiet);
138962
+ }
138963
+ quiet();
138752
138964
  }
138753
138965
  // Detect permission gates (OSC 777 + scraped options) and AskUserQuestion
138754
138966
  // menus from the rendered screen, firing the additive callbacks. Async because
@@ -138824,6 +139036,24 @@ var PTYManager = class {
138824
139036
  }
138825
139037
  }
138826
139038
  }
139039
+ // Fired QUIET_DETECT_MS after the last PTY chunk. Re-runs the same
139040
+ // ready/prompt detection handleOutput() runs per-chunk, using the last
139041
+ // rendered output — a session blocked on a prompt (or an unmarked boot
139042
+ // screen) may never produce another chunk to trigger detection otherwise.
139043
+ handleQuiet(sessionId) {
139044
+ const session = this.sessions.get(sessionId);
139045
+ if (session?.status !== "running") return;
139046
+ if (this.pendingReady.has(sessionId)) {
139047
+ this.markReady(sessionId, session, "quiet:timeout");
139048
+ }
139049
+ this.detectLivePrompts(sessionId, "", session.lastOutput).catch((err) => {
139050
+ this.log.warn("[pty.prompt_detect] failed", {
139051
+ event: "pty.prompt_detect_failed",
139052
+ sessionId,
139053
+ err
139054
+ });
139055
+ });
139056
+ }
138827
139057
  // Transition a session from "running" to "waiting_input", clear pendingReady,
138828
139058
  // and flush any queued input. Idempotent: callers can invoke at any chunk.
138829
139059
  markReady(sessionId, session, reason) {
@@ -138864,6 +139094,8 @@ var PTYManager = class {
138864
139094
  this.permissionOpen.delete(sessionId);
138865
139095
  this.lastScreenQuestionKey.delete(sessionId);
138866
139096
  this.shellPromptOpen.delete(sessionId);
139097
+ this.quietCheckers.get(sessionId)?.cancel();
139098
+ this.quietCheckers.delete(sessionId);
138867
139099
  }
138868
139100
  };
138869
139101
  function toPublicSession2(s3) {
@@ -141091,7 +141323,7 @@ function pruneAgentConversations(cache) {
141091
141323
  missing += 1;
141092
141324
  continue;
141093
141325
  }
141094
- if (isAgentFile(row.file_path, cache.getAgentEntrypoints())) {
141326
+ if (cache.isAgentFileCached(row.file_path)) {
141095
141327
  cache.deleteByFilePath(row.file_path);
141096
141328
  pruned += 1;
141097
141329
  }
@@ -141511,37 +141743,6 @@ function computeConversationEtag({
141511
141743
  return `"${digest}"`;
141512
141744
  }
141513
141745
 
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
141746
  // node_modules/date-fns/constants.js
141546
141747
  var daysInYear = 365.2425;
141547
141748
  var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
@@ -141900,6 +142101,7 @@ var WSHub = class {
141900
142101
  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
142102
  var DEFAULT_SYSTEM_PROMPT = "When presenting options or choices to the user, limit the options to at most 3.";
141902
142103
  var DEFAULT_PTY_GRACE_PERIOD_MS = 27e4;
142104
+ var START_READY_TIMEOUT_MS = 15e3;
141903
142105
  function parseIncludeAgentsEnv(raw2) {
141904
142106
  if (raw2 === void 0) return false;
141905
142107
  const v3 = raw2.trim().toLowerCase();
@@ -141968,6 +142170,7 @@ var StreamerServer = class {
141968
142170
  sessionInputAttempts = /* @__PURE__ */ new Map();
141969
142171
  ptyGracePeriodMs;
141970
142172
  defaultSystemPrompt;
142173
+ defaultPermissionMode;
141971
142174
  // Map of sessionId → grace timer; fires to kill PTY after WS disconnect
141972
142175
  ptyGraceTimers = /* @__PURE__ */ new Map();
141973
142176
  // Map of sessionId → set of subscribed WS clients
@@ -142016,6 +142219,7 @@ var StreamerServer = class {
142016
142219
  this.codexRoots = config2.codexRoots ?? [(0, import_path21.join)((0, import_os10.homedir)(), ".codex", "sessions")];
142017
142220
  this.ptyGracePeriodMs = config2.ptyGracePeriodMs ?? DEFAULT_PTY_GRACE_PERIOD_MS;
142018
142221
  this.defaultSystemPrompt = config2.defaultSystemPrompt ?? DEFAULT_SYSTEM_PROMPT;
142222
+ this.defaultPermissionMode = config2.defaultPermissionMode ?? loadDefaultPermissionMode() ?? "acceptEdits";
142019
142223
  this.cacheDir = config2.cacheDir ?? loadCacheDir() ?? (0, import_path21.join)((0, import_os10.homedir)(), ".threadbase", "cache");
142020
142224
  this.tailSize = config2.tailSize ?? loadTailSize() ?? 10;
142021
142225
  this.directoryDebounceMs = parseDirScanDebounceEnv(process.env.THREADBASE_DIR_SCAN_DEBOUNCE_MS) ?? config2.directoryScanDebounceMs ?? 1e3;
@@ -142445,9 +142649,9 @@ var StreamerServer = class {
142445
142649
  );
142446
142650
  this.scannerPersistenceDisabled = true;
142447
142651
  }
142448
- const warmupScanner = this.newScanner();
142449
- this.allScanners.add(warmupScanner);
142450
142652
  const warmupStatCache = this.buildStatCache(null);
142653
+ const warmupScanner = this.newScanner(warmupStatCache ? { persistent: false } : void 0);
142654
+ this.allScanners.add(warmupScanner);
142451
142655
  const shouldEmitProgress = createScanProgressThrottle();
142452
142656
  const scanOpts = {
142453
142657
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
@@ -142887,6 +143091,10 @@ var StreamerServer = class {
142887
143091
  }
142888
143092
  buildStatCache(previousScanner) {
142889
143093
  if (!this.cache) return void 0;
143094
+ if (!previousScanner) {
143095
+ const persisted = this.cache.getScannerStatCache();
143096
+ return persisted.size > 0 ? persisted : void 0;
143097
+ }
142890
143098
  const dbStats = this.cache.getFileStats();
142891
143099
  if (dbStats.size === 0) return void 0;
142892
143100
  const metaByPath = /* @__PURE__ */ new Map();
@@ -142916,9 +143124,9 @@ var StreamerServer = class {
142916
143124
  // this — its per-file refreshFile (in findConversationByUuid) already
142917
143125
  // reconciles the one conversation being requested, so paying a full-tree
142918
143126
  // rescan just because some OTHER file changed is the stall this avoids.
142919
- newScanner() {
143127
+ newScanner(options) {
142920
143128
  return new ConversationScanner(
142921
- this.scannerPersistenceDisabled ? { persistent: false } : void 0
143129
+ options ?? (this.scannerPersistenceDisabled ? { persistent: false } : void 0)
142922
143130
  );
142923
143131
  }
142924
143132
  async getScanner(skipStaleRescan = false) {
@@ -142937,7 +143145,7 @@ var StreamerServer = class {
142937
143145
  }
142938
143146
  this.scannerStale = false;
142939
143147
  const statCache = this.buildStatCache(this.scanner);
142940
- this.scanner = this.newScanner();
143148
+ this.scanner = this.newScanner(statCache ? { persistent: false } : void 0);
142941
143149
  this.allScanners.add(this.scanner);
142942
143150
  this.scannerReady = this.scanner.scan({
142943
143151
  ...this.scanProfiles ? { profiles: this.scanProfiles } : {},
@@ -142960,9 +143168,8 @@ var StreamerServer = class {
142960
143168
  this.scannerReady = null;
142961
143169
  return this.getScanner();
142962
143170
  }
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
143171
+ // refresh=1's scan: reuse the WARM scanner and re-run its scan with
143172
+ // fullRescan:true the escape hatch that bypasses the scanner's
142966
143173
  // dir-mtime discovery gate, since an explicit user pull-to-refresh is exactly
142967
143174
  // the "don't trust the gate, check disk for real" signal. Unlike
142968
143175
  // getFreshScanner() this does NOT discard the warm scanner. scannerReady is
@@ -143350,7 +143557,8 @@ var StreamerServer = class {
143350
143557
  provider,
143351
143558
  projectPath,
143352
143559
  projectName: body.projectName,
143353
- branch: body.branch
143560
+ branch: body.branch,
143561
+ permissionMode: this.defaultPermissionMode
143354
143562
  });
143355
143563
  this.sessionStore.addManaged(session);
143356
143564
  void this.watchConversationFile(sessionId);
@@ -143674,7 +143882,8 @@ var StreamerServer = class {
143674
143882
  const session = await this.ptyManager.start(convId, {
143675
143883
  projectPath,
143676
143884
  projectName,
143677
- branch
143885
+ branch,
143886
+ permissionMode: this.defaultPermissionMode
143678
143887
  });
143679
143888
  this.sessionStore.addManaged(session);
143680
143889
  void this.watchConversationFile(session.id);
@@ -143744,10 +143953,35 @@ var StreamerServer = class {
143744
143953
  provider,
143745
143954
  projectPath: resolvedPath,
143746
143955
  projectName: body.projectName,
143747
- systemPrompt: systemPromptParts.join("\n")
143956
+ systemPrompt: systemPromptParts.join("\n"),
143957
+ permissionMode: this.defaultPermissionMode
143748
143958
  });
143749
143959
  this.sessionStore.addManaged(session);
143750
- json2(res, 202, { id: session.id, status: "pending" });
143960
+ const readyOrFailed = new Promise((resolve6) => {
143961
+ const handler = (status) => {
143962
+ if (status === "waiting_input" || status === "idle") {
143963
+ this.sessionStatusBus.off(`status:${session.id}`, handler);
143964
+ resolve6(status === "waiting_input" ? "ready" : "failed");
143965
+ }
143966
+ };
143967
+ this.sessionStatusBus.on(`status:${session.id}`, handler);
143968
+ });
143969
+ const timeoutPromise = new Promise(
143970
+ (resolve6) => setTimeout(() => resolve6("timeout"), START_READY_TIMEOUT_MS)
143971
+ );
143972
+ const outcome = await Promise.race([readyOrFailed, timeoutPromise]);
143973
+ const current = this.sessionStore.get(session.id, this.ptyAttachedIds());
143974
+ if (outcome === "ready" && current) {
143975
+ json2(res, 200, { session: current });
143976
+ } else if (outcome === "failed" && current) {
143977
+ json2(res, 502, {
143978
+ id: session.id,
143979
+ status: "idle",
143980
+ error: current.failureReason ?? "Session exited before becoming ready"
143981
+ });
143982
+ } else {
143983
+ json2(res, 202, { id: session.id, status: "pending" });
143984
+ }
143751
143985
  if (provider === CODEX_CLI_PROVIDER2) {
143752
143986
  this.watchForCodexRollout(session.id, resolvedPath);
143753
143987
  } else {
@@ -147937,7 +148171,10 @@ program2.name("threadbase-streamer").description("PTY session management, WebSoc
147937
148171
  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
148172
  "--public-url <url>",
147939
148173
  "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(
148174
+ ).option(
148175
+ "--default-permission-mode <mode>",
148176
+ "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)."
148177
+ ).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
148178
  "--prod",
147942
148179
  "Run as if invoked by launchd: skip the dev-takeover prompt and signal handlers",
147943
148180
  false
@@ -147956,11 +148193,25 @@ program2.command("serve").description("Start the streamer server").option("-p, -
147956
148193
  if (opts.multiAgentFlow) {
147957
148194
  process.env.MULTI_AGENT_FLOW = "true";
147958
148195
  }
148196
+ if (opts.defaultPermissionMode !== void 0 && opts.defaultPermissionMode !== "acceptEdits" && opts.defaultPermissionMode !== "manual") {
148197
+ log7.error(
148198
+ `Invalid --default-permission-mode: ${opts.defaultPermissionMode}`,
148199
+ void 0,
148200
+ "console"
148201
+ );
148202
+ process.exit(1);
148203
+ }
147959
148204
  const requestedPort = Number.parseInt(opts.port, 10);
147960
148205
  const apiKey = opts.apiKey ?? loadOrCreateApiKey();
147961
148206
  const publicUrl = opts.publicUrl ?? loadPublicUrl() ?? null;
147962
148207
  const isProdInvocation = opts.prod === true || process.ppid === 1;
147963
148208
  if (!isProdInvocation) appendDevSessionMarker();
148209
+ let resolvedDefaultPermissionMode = opts.defaultPermissionMode;
148210
+ if (resolvedDefaultPermissionMode === void 0 && !isProdInvocation && process.env.THREADBASE_SKIP_PERMISSION_MODE_PROMPT !== "true" && loadDefaultPermissionMode() === void 0 && import_node_process3.stdin.isTTY) {
148211
+ const { interactivePermissionModePrompt: interactivePermissionModePrompt2 } = await Promise.resolve().then(() => (init_prompt(), prompt_exports));
148212
+ resolvedDefaultPermissionMode = await interactivePermissionModePrompt2();
148213
+ setDefaultPermissionMode(resolvedDefaultPermissionMode);
148214
+ }
147964
148215
  let resolvedPort = requestedPort;
147965
148216
  if (!isProdInvocation) {
147966
148217
  const { resolveDevPlan: resolveDevPlan2, detectProdActive: detectProdActive2, isPortInUse: isPortInUse2, findFreePort: findFreePort2, takeoverProd: takeoverProd2 } = await Promise.resolve().then(() => (init_dev_takeover(), dev_takeover_exports));
@@ -147992,7 +148243,8 @@ program2.command("serve").description("Start the streamer server").option("-p, -
147992
148243
  verbose: opts.verbose,
147993
148244
  logMenubarRequests: opts.logMenubarRequests,
147994
148245
  browseRoot: opts.browseRoot,
147995
- publicUrl: opts.publicUrl
148246
+ publicUrl: opts.publicUrl,
148247
+ defaultPermissionMode: resolvedDefaultPermissionMode
147996
148248
  });
147997
148249
  await server.listen(resolvedPort);
147998
148250
  {
@@ -148006,13 +148258,21 @@ program2.command("serve").description("Start the streamer server").option("-p, -
148006
148258
  wsUrl: `ws://localhost:${resolvedPort}/ws`
148007
148259
  });
148008
148260
  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
- }
148261
+ try {
148262
+ await printServerBanner({
148263
+ port: resolvedPort,
148264
+ apiKey,
148265
+ publicUrl,
148266
+ includeQr: opts.pairQr !== false
148267
+ });
148268
+ } catch (err) {
148269
+ const message = err instanceof Error ? err.message : String(err);
148270
+ log7.warn(`(skipped pairing QR: ${message})`, { reason: message });
148271
+ log7.info(
148272
+ printUrlBanner({ url: resolveServerUrl({ publicUrl, port: resolvedPort }) }),
148273
+ void 0,
148274
+ "console"
148275
+ );
148016
148276
  }
148017
148277
  const shutdown = async () => {
148018
148278
  log7.info("Shutting down...");
@@ -148067,7 +148327,7 @@ program2.command("pair").description("Print a pairing QR code (server must alrea
148067
148327
  const port = Number.parseInt(opts.port, 10);
148068
148328
  const apiKey = loadOrCreateApiKey();
148069
148329
  const publicUrl = loadPublicUrl() ?? null;
148070
- await printPairQR({ port, apiKey, publicUrl });
148330
+ await printServerBanner({ port, apiKey, publicUrl, includeQr: true });
148071
148331
  });
148072
148332
  program2.command("set-key [key]").description("Set the streamer API key in ~/.threadbase/server.yaml").action(async (key) => {
148073
148333
  const { runSetKey: runSetKey2 } = await Promise.resolve().then(() => (init_setKey(), setKey_exports));
@@ -148163,11 +148423,49 @@ program2.command("update").description("Check for streamer updates from GitHub R
148163
148423
  });
148164
148424
  registerProdCommands(program2);
148165
148425
  program2.parse();
148166
- async function printPairQR({
148426
+ function generateQr(payload) {
148427
+ return new Promise((resolve6) => {
148428
+ import_qrcode_terminal.default.generate(payload, { small: true }, resolve6);
148429
+ });
148430
+ }
148431
+ function printUrlBanner({
148432
+ url: url2,
148433
+ qr: qr2,
148434
+ expiresAt
148435
+ }) {
148436
+ const contentLines = ["Threadbase Streamer \u2014 server address", "", url2];
148437
+ if (qr2) {
148438
+ contentLines.push("", ...qr2.split("\n").filter((l) => l.length > 0));
148439
+ if (expiresAt !== void 0) {
148440
+ contentLines.push(
148441
+ "",
148442
+ `Scan to pair a mobile client (expires ${new Date(expiresAt).toLocaleTimeString()})`
148443
+ );
148444
+ } else {
148445
+ contentLines.push("", "Scan to pair a mobile client");
148446
+ }
148447
+ }
148448
+ const width = Math.max(...contentLines.map((l) => l.length));
148449
+ const pad = (l) => `\u2551 ${l}${" ".repeat(width - l.length)} \u2551`;
148450
+ const top = `\u2554${"\u2550".repeat(width + 2)}\u2557`;
148451
+ const bottom = `\u255A${"\u2550".repeat(width + 2)}\u255D`;
148452
+ return `
148453
+ ${top}
148454
+ ${contentLines.map(pad).join("\n")}
148455
+ ${bottom}
148456
+ `;
148457
+ }
148458
+ async function printServerBanner({
148167
148459
  port,
148168
148460
  apiKey,
148169
- publicUrl
148461
+ publicUrl,
148462
+ includeQr
148170
148463
  }) {
148464
+ const url2 = resolveServerUrl({ publicUrl, port });
148465
+ if (!includeQr) {
148466
+ log7.info(printUrlBanner({ url: url2 }), void 0, "console");
148467
+ return;
148468
+ }
148171
148469
  const res = await fetch(`http://localhost:${port}/api/pair/start`, {
148172
148470
  method: "POST",
148173
148471
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
@@ -148177,19 +148475,12 @@ async function printPairQR({
148177
148475
  throw new Error(`/api/pair/start returned ${res.status}`);
148178
148476
  }
148179
148477
  const { token, expiresAt, expiresInSeconds } = await res.json();
148180
- const url2 = resolveServerUrl({ publicUrl, port });
148181
148478
  const expSeconds = Math.floor(expiresAt / 1e3);
148182
148479
  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
- );
148480
+ const qr2 = await generateQr(payload);
148481
+ log7.info(printUrlBanner({ url: url2, qr: qr2, expiresAt }), void 0, "console");
148482
+ log7.info(`Pair URL: ${payload}`, void 0, "console");
148483
+ log7.info(`Expires in ${expiresInSeconds}s`, void 0, "console");
148193
148484
  }
148194
148485
  /*! Bundled license information:
148195
148486