@threadbase-sh/streamer 1.47.0 → 1.47.2

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/README.md CHANGED
@@ -43,7 +43,9 @@ npm and Homebrew installs can auto-update: [docs/guides/auto-update.md](docs/gui
43
43
 
44
44
  ## Remote Access
45
45
 
46
- The server only binds to `127.0.0.1:8766` by default. To let the mobile app reach it from outside your LAN, expose it via a tunnel — the fastest is a Cloudflare quick-tunnel (no account needed):
46
+ The server listens on port 8766 on all interfaces, so devices on the same LAN can already reach it. To let the mobile app reach it from *outside* your LAN, expose it via a tunnel — the fastest is a Cloudflare quick-tunnel (no account needed):
47
+
48
+ > **Before you do this, read [Security](#security--read-this-before-exposing-the-server).** A tunnel makes the server reachable from the public internet, and the API key is the only thing between a stranger and a shell on this machine.
47
49
 
48
50
  ```bash
49
51
  bash scripts/remote-access/cloudflare.sh # macOS/Linux/WSL/Git Bash
@@ -64,9 +66,47 @@ npm run migrate # apply SQLite migrations
64
66
  npm run db:validate # check for missing/duplicate/orphaned project_id data
65
67
  ```
66
68
 
69
+ ## Security — read this before exposing the server
70
+
71
+ **The API key is equivalent to a shell on this machine.** The streamer exists to start Claude Code sessions and type into them, so anyone holding the key can run commands as your user, in any project the server can reach. Treat it like an SSH private key, not like a web session token.
72
+
73
+ The key lives at `~/.threadbase/server.yaml` as plaintext (`chmod 0600`), which is the same posture as `~/.aws/credentials` or `~/.npmrc`. Rotate it with `POST /api/auth/rotate` if it leaks; re-pair your devices afterwards.
74
+
75
+ **The server listens on all network interfaces**, not just loopback — `httpServer.listen(port)` is called without a host, which is Node's listen-everywhere default, and there is currently no flag to narrow it. So every device on your LAN can already reach port 8766, and the API key is the only thing stopping them. On a home network that is usually fine; on café Wi-Fi, a co-working space, or a corporate VLAN it is not.
76
+
77
+ **A tunnel extends that to the entire internet.** From that moment the key is the only thing between a stranger and your filesystem.
78
+
79
+ If you need it strictly local today, bind it yourself at the firewall or run it inside a network namespace — the server will not do it for you. Tracked as [#517](https://github.com/RonenMars/threadbase-streamer/issues/517).
80
+
81
+ ### There is no spend limit
82
+
83
+ Nothing in this server caps what a session costs. There is no per-session, per-day, or total ceiling, and no way to configure one — a runaway agent bills your provider account until you stop it. (`--max-budget-usd` is not a workaround: it only applies to `claude --print`, which this server never uses, so it was silently inert and has been removed.)
84
+
85
+ The practical bound is that a session waits for input between turns. Auto-resume and bypass modes both weaken that bound — see below.
86
+
87
+ ### Permission modes decide how much a session can do unattended
88
+
89
+ Sessions spawn with `--permission-mode`, default `acceptEdits`: file edits are auto-approved, shell commands still prompt. The first-run prompt offers only `acceptEdits` and `manual`, so the unattended modes are never something you pick by accident.
90
+
91
+ `bypassPermissions` and `dontAsk` are available through `--default-permission-mode` and `PUT /api/config/claude-flags`, and they remove the confirmation step entirely. **A session in either mode is unattended arbitrary code execution**: a leaked key no longer stops at a human-in-the-loop prompt. `--add-dir` compounds this by widening filesystem scope beyond the project.
92
+
93
+ If you enable one, you are accepting that the auth boundary is the *only* remaining control. Two guardrails exist and neither is a substitute for that judgement: `PUT /api/config/claude-flags` is refused with 403 while `--local-no-auth` is active, and every flag change is logged with its old and new value.
94
+
95
+ ### Auto-resume starts agents without you
96
+
97
+ `auto_resume_on_boot` in `server.yaml` re-starts sessions that a previous run was interrupted mid-flight. It defaults to `false` and is never enabled implicitly — it is the one setting that lets the streamer start an agent nobody asked for in that moment, and combined with a bypass permission mode that is unattended code execution after a reboot.
98
+
99
+ Leave it off unless you have a reason, and do not combine it with a bypass mode unless you fully trust everything that can reach the API.
100
+
101
+ ### Reporting a vulnerability
102
+
103
+ See [SECURITY.md](https://github.com/RonenMars/threadbase/blob/main/SECURITY.md) in the umbrella repo.
104
+
67
105
  ## Persistence
68
106
 
69
- Conversation metadata is cached in SQLite at `~/.threadbase/cache/cache.db`, created and migrated automatically — no setup needed. Sessions are in-memory: a restart drops live PTYs, but history is on disk and any session can be resumed with `POST /api/sessions/resume`.
107
+ Conversation metadata is cached in SQLite at `~/.threadbase/cache/cache.db`, created and migrated automatically — no setup needed.
108
+
109
+ Live PTYs do not survive a restart, but sessions are not lost: the managed-session registry (`~/.threadbase/runtime.db`) brings interrupted sessions back in `GET /api/sessions` as resumable stubs, and any conversation can be resumed with `POST /api/sessions/resume`. History itself is always on disk, written by the provider.
70
110
 
71
111
  PostgreSQL is optional and only stores upload records today. Enable it by setting `THREADBASE_DATABASE_URL`; migrations run automatically.
72
112
 
package/dist/cli.cjs CHANGED
@@ -3604,7 +3604,7 @@ var require_thread_stream = __commonJS({
3604
3604
  var { version: version2 } = require_package();
3605
3605
  var { EventEmitter: EventEmitter5 } = require("events");
3606
3606
  var { Worker } = require("worker_threads");
3607
- var { join: join30 } = require("path");
3607
+ var { join: join29 } = require("path");
3608
3608
  var { pathToFileURL } = require("url");
3609
3609
  var { wait } = require_wait();
3610
3610
  var {
@@ -3655,7 +3655,7 @@ var require_thread_stream = __commonJS({
3655
3655
  function createWorker(stream, opts) {
3656
3656
  const { filename, workerData } = opts;
3657
3657
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
3658
- const toExecute = bundlerOverrides["thread-stream-worker"] || join30(__dirname, "lib", "worker.js");
3658
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join29(__dirname, "lib", "worker.js");
3659
3659
  const worker = new Worker(toExecute, {
3660
3660
  ...opts.workerOpts,
3661
3661
  name: opts.workerOpts?.name || "thread-stream",
@@ -4123,7 +4123,7 @@ var require_transport = __commonJS({
4123
4123
  var { createRequire } = require("module");
4124
4124
  var { existsSync: existsSync16 } = require("fs");
4125
4125
  var getCallers = require_caller();
4126
- var { join: join30, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
4126
+ var { join: join29, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
4127
4127
  var { fileURLToPath: fileURLToPath3 } = require("url");
4128
4128
  var sleep = require_atomic_sleep();
4129
4129
  var onExit = require_on_exit_leak_free();
@@ -4276,7 +4276,7 @@ var require_transport = __commonJS({
4276
4276
  throw new Error("only one of target or targets can be specified");
4277
4277
  }
4278
4278
  if (targets) {
4279
- target = bundlerOverrides["pino-worker"] || join30(__dirname, "worker.js");
4279
+ target = bundlerOverrides["pino-worker"] || join29(__dirname, "worker.js");
4280
4280
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
4281
4281
  return {
4282
4282
  ...dest,
@@ -4294,7 +4294,7 @@ var require_transport = __commonJS({
4294
4294
  });
4295
4295
  });
4296
4296
  } else if (pipeline2) {
4297
- target = bundlerOverrides["pino-worker"] || join30(__dirname, "worker.js");
4297
+ target = bundlerOverrides["pino-worker"] || join29(__dirname, "worker.js");
4298
4298
  options.pipelines = [pipeline2.map((dest) => {
4299
4299
  return {
4300
4300
  ...dest,
@@ -4317,7 +4317,7 @@ var require_transport = __commonJS({
4317
4317
  return origin;
4318
4318
  }
4319
4319
  if (origin === "pino/file") {
4320
- return join30(__dirname, "..", "file.js");
4320
+ return join29(__dirname, "..", "file.js");
4321
4321
  }
4322
4322
  let fixTarget2;
4323
4323
  for (const filePath of callers) {
@@ -5298,7 +5298,7 @@ var require_safe_stable_stringify = __commonJS({
5298
5298
  return circularValue;
5299
5299
  }
5300
5300
  let res = "";
5301
- let join30 = ",";
5301
+ let join29 = ",";
5302
5302
  const originalIndentation = indentation;
5303
5303
  if (Array.isArray(value)) {
5304
5304
  if (value.length === 0) {
@@ -5312,7 +5312,7 @@ var require_safe_stable_stringify = __commonJS({
5312
5312
  indentation += spacer;
5313
5313
  res += `
5314
5314
  ${indentation}`;
5315
- join30 = `,
5315
+ join29 = `,
5316
5316
  ${indentation}`;
5317
5317
  }
5318
5318
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -5320,13 +5320,13 @@ ${indentation}`;
5320
5320
  for (; i < maximumValuesToStringify - 1; i++) {
5321
5321
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
5322
5322
  res += tmp2 !== void 0 ? tmp2 : "null";
5323
- res += join30;
5323
+ res += join29;
5324
5324
  }
5325
5325
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
5326
5326
  res += tmp !== void 0 ? tmp : "null";
5327
5327
  if (value.length - 1 > maximumBreadth) {
5328
5328
  const removedKeys = value.length - maximumBreadth - 1;
5329
- res += `${join30}"... ${getItemCount(removedKeys)} not stringified"`;
5329
+ res += `${join29}"... ${getItemCount(removedKeys)} not stringified"`;
5330
5330
  }
5331
5331
  if (spacer !== "") {
5332
5332
  res += `
@@ -5347,7 +5347,7 @@ ${originalIndentation}`;
5347
5347
  let separator = "";
5348
5348
  if (spacer !== "") {
5349
5349
  indentation += spacer;
5350
- join30 = `,
5350
+ join29 = `,
5351
5351
  ${indentation}`;
5352
5352
  whitespace = " ";
5353
5353
  }
@@ -5361,13 +5361,13 @@ ${indentation}`;
5361
5361
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
5362
5362
  if (tmp !== void 0) {
5363
5363
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
5364
- separator = join30;
5364
+ separator = join29;
5365
5365
  }
5366
5366
  }
5367
5367
  if (keyLength > maximumBreadth) {
5368
5368
  const removedKeys = keyLength - maximumBreadth;
5369
5369
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
5370
- separator = join30;
5370
+ separator = join29;
5371
5371
  }
5372
5372
  if (spacer !== "" && separator.length > 1) {
5373
5373
  res = `
@@ -5408,7 +5408,7 @@ ${originalIndentation}`;
5408
5408
  }
5409
5409
  const originalIndentation = indentation;
5410
5410
  let res = "";
5411
- let join30 = ",";
5411
+ let join29 = ",";
5412
5412
  if (Array.isArray(value)) {
5413
5413
  if (value.length === 0) {
5414
5414
  return "[]";
@@ -5421,7 +5421,7 @@ ${originalIndentation}`;
5421
5421
  indentation += spacer;
5422
5422
  res += `
5423
5423
  ${indentation}`;
5424
- join30 = `,
5424
+ join29 = `,
5425
5425
  ${indentation}`;
5426
5426
  }
5427
5427
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -5429,13 +5429,13 @@ ${indentation}`;
5429
5429
  for (; i < maximumValuesToStringify - 1; i++) {
5430
5430
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
5431
5431
  res += tmp2 !== void 0 ? tmp2 : "null";
5432
- res += join30;
5432
+ res += join29;
5433
5433
  }
5434
5434
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
5435
5435
  res += tmp !== void 0 ? tmp : "null";
5436
5436
  if (value.length - 1 > maximumBreadth) {
5437
5437
  const removedKeys = value.length - maximumBreadth - 1;
5438
- res += `${join30}"... ${getItemCount(removedKeys)} not stringified"`;
5438
+ res += `${join29}"... ${getItemCount(removedKeys)} not stringified"`;
5439
5439
  }
5440
5440
  if (spacer !== "") {
5441
5441
  res += `
@@ -5448,7 +5448,7 @@ ${originalIndentation}`;
5448
5448
  let whitespace = "";
5449
5449
  if (spacer !== "") {
5450
5450
  indentation += spacer;
5451
- join30 = `,
5451
+ join29 = `,
5452
5452
  ${indentation}`;
5453
5453
  whitespace = " ";
5454
5454
  }
@@ -5457,7 +5457,7 @@ ${indentation}`;
5457
5457
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
5458
5458
  if (tmp !== void 0) {
5459
5459
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
5460
- separator = join30;
5460
+ separator = join29;
5461
5461
  }
5462
5462
  }
5463
5463
  if (spacer !== "" && separator.length > 1) {
@@ -5515,20 +5515,20 @@ ${originalIndentation}`;
5515
5515
  indentation += spacer;
5516
5516
  let res2 = `
5517
5517
  ${indentation}`;
5518
- const join31 = `,
5518
+ const join30 = `,
5519
5519
  ${indentation}`;
5520
5520
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
5521
5521
  let i = 0;
5522
5522
  for (; i < maximumValuesToStringify - 1; i++) {
5523
5523
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
5524
5524
  res2 += tmp2 !== void 0 ? tmp2 : "null";
5525
- res2 += join31;
5525
+ res2 += join30;
5526
5526
  }
5527
5527
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
5528
5528
  res2 += tmp !== void 0 ? tmp : "null";
5529
5529
  if (value.length - 1 > maximumBreadth) {
5530
5530
  const removedKeys = value.length - maximumBreadth - 1;
5531
- res2 += `${join31}"... ${getItemCount(removedKeys)} not stringified"`;
5531
+ res2 += `${join30}"... ${getItemCount(removedKeys)} not stringified"`;
5532
5532
  }
5533
5533
  res2 += `
5534
5534
  ${originalIndentation}`;
@@ -5544,16 +5544,16 @@ ${originalIndentation}`;
5544
5544
  return '"[Object]"';
5545
5545
  }
5546
5546
  indentation += spacer;
5547
- const join30 = `,
5547
+ const join29 = `,
5548
5548
  ${indentation}`;
5549
5549
  let res = "";
5550
5550
  let separator = "";
5551
5551
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
5552
5552
  if (isTypedArrayWithEntries(value)) {
5553
- res += stringifyTypedArray(value, join30, maximumBreadth);
5553
+ res += stringifyTypedArray(value, join29, maximumBreadth);
5554
5554
  keys = keys.slice(value.length);
5555
5555
  maximumPropertiesToStringify -= value.length;
5556
- separator = join30;
5556
+ separator = join29;
5557
5557
  }
5558
5558
  if (deterministic) {
5559
5559
  keys = sort(keys, comparator);
@@ -5564,13 +5564,13 @@ ${indentation}`;
5564
5564
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
5565
5565
  if (tmp !== void 0) {
5566
5566
  res += `${separator}${strEscape(key2)}: ${tmp}`;
5567
- separator = join30;
5567
+ separator = join29;
5568
5568
  }
5569
5569
  }
5570
5570
  if (keyLength > maximumBreadth) {
5571
5571
  const removedKeys = keyLength - maximumBreadth;
5572
5572
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
5573
- separator = join30;
5573
+ separator = join29;
5574
5574
  }
5575
5575
  if (separator !== "") {
5576
5576
  res = `
@@ -28983,6 +28983,10 @@ function markerPath() {
28983
28983
  function prefsPath() {
28984
28984
  return (0, import_node_path3.join)(installDir(), "dev-prefs.json");
28985
28985
  }
28986
+ function logPaths() {
28987
+ const dir = (0, import_node_path3.join)(installDir(), "logs");
28988
+ return { stdout: (0, import_node_path3.join)(dir, "stdout.log"), stderr: (0, import_node_path3.join)(dir, "stderr.log") };
28989
+ }
28986
28990
  var import_node_os2, import_node_path3, LAUNCHD_LABEL, TASK_NAME;
28987
28991
  var init_constants = __esm({
28988
28992
  "src/lifecycle/constants.ts"() {
@@ -126967,8 +126971,7 @@ function darwinPlistPath() {
126967
126971
  return `${process.env.HOME}/Library/LaunchAgents/${resolveLoadedLabel()}.plist`;
126968
126972
  }
126969
126973
  function getLogPaths() {
126970
- const dir = (0, import_node_path25.join)(installDir(), "logs");
126971
- return { stdout: (0, import_node_path25.join)(dir, "stdout.log"), stderr: (0, import_node_path25.join)(dir, "stderr.log") };
126974
+ return logPaths();
126972
126975
  }
126973
126976
  function getAgentPid() {
126974
126977
  try {
@@ -126988,12 +126991,11 @@ function getAgentPid() {
126988
126991
  return null;
126989
126992
  }
126990
126993
  }
126991
- var import_node_child_process6, import_node_path25, BREW_LAUNCHD_LABEL2;
126994
+ var import_node_child_process6, BREW_LAUNCHD_LABEL2;
126992
126995
  var init_launchd = __esm({
126993
126996
  "src/lifecycle/launchd.ts"() {
126994
126997
  "use strict";
126995
126998
  import_node_child_process6 = require("child_process");
126996
- import_node_path25 = require("path");
126997
126999
  init_constants();
126998
127000
  BREW_LAUNCHD_LABEL2 = "homebrew.mxcl.tb-streamer";
126999
127001
  }
@@ -127032,7 +127034,7 @@ function readMarker() {
127032
127034
  }
127033
127035
  function writeMarker(marker) {
127034
127036
  MarkerSchema.parse(marker);
127035
- (0, import_node_fs22.mkdirSync)((0, import_node_path26.dirname)(markerPath()), { recursive: true });
127037
+ (0, import_node_fs22.mkdirSync)((0, import_node_path25.dirname)(markerPath()), { recursive: true });
127036
127038
  const tmp = `${markerPath()}.tmp`;
127037
127039
  (0, import_node_fs22.writeFileSync)(tmp, `${JSON.stringify(marker, null, 2)}
127038
127040
  `, { mode: 384 });
@@ -127041,12 +127043,12 @@ function writeMarker(marker) {
127041
127043
  function clearMarker() {
127042
127044
  if ((0, import_node_fs22.existsSync)(markerPath())) (0, import_node_fs22.rmSync)(markerPath());
127043
127045
  }
127044
- var import_node_fs22, import_node_path26, log8;
127046
+ var import_node_fs22, import_node_path25, log8;
127045
127047
  var init_marker = __esm({
127046
127048
  "src/lifecycle/marker.ts"() {
127047
127049
  "use strict";
127048
127050
  import_node_fs22 = require("fs");
127049
- import_node_path26 = require("path");
127051
+ import_node_path25 = require("path");
127050
127052
  init_logger();
127051
127053
  init_constants();
127052
127054
  init_marker_schema();
@@ -127096,9 +127098,7 @@ function kickstartAgent2() {
127096
127098
  ps2(`Start-ScheduledTask -TaskName '${TASK_NAME}' -ErrorAction Stop`);
127097
127099
  }
127098
127100
  function getLogPaths2() {
127099
- throw new Error(
127100
- "prod logs is not yet wired on Windows \u2014 launch.cmd does not currently redirect stdout/stderr to a file. Track the fix in docs/troubleshooting.md."
127101
- );
127101
+ return logPaths();
127102
127102
  }
127103
127103
  function getAgentPid2() {
127104
127104
  try {
@@ -127188,7 +127188,7 @@ __export(log_cap_exports, {
127188
127188
  LOG_CAP_BYTES: () => LOG_CAP_BYTES,
127189
127189
  truncateOversizedLogs: () => truncateOversizedLogs
127190
127190
  });
127191
- function truncateOversizedLogs(maxBytes = LOG_CAP_BYTES, paths = Object.values(getLogPaths())) {
127191
+ function truncateOversizedLogs(maxBytes = LOG_CAP_BYTES, paths = Object.values(logPaths())) {
127192
127192
  const truncated = [];
127193
127193
  for (const file2 of paths) {
127194
127194
  try {
@@ -127205,7 +127205,7 @@ var init_log_cap = __esm({
127205
127205
  "src/lifecycle/log-cap.ts"() {
127206
127206
  "use strict";
127207
127207
  import_node_fs24 = require("fs");
127208
- init_launchd();
127208
+ init_constants();
127209
127209
  LOG_CAP_BYTES = 32 * 1024 * 1024;
127210
127210
  }
127211
127211
  });
@@ -127300,7 +127300,7 @@ function readPrefs() {
127300
127300
  }
127301
127301
  function savePrefs(p2) {
127302
127302
  const path2 = prefsPath();
127303
- (0, import_node_fs25.mkdirSync)((0, import_node_path27.dirname)(path2), { recursive: true });
127303
+ (0, import_node_fs25.mkdirSync)((0, import_node_path26.dirname)(path2), { recursive: true });
127304
127304
  const tmp = `${path2}.tmp`;
127305
127305
  (0, import_node_fs25.writeFileSync)(tmp, `${JSON.stringify(p2, null, 2)}
127306
127306
  `, { mode: 384 });
@@ -127327,12 +127327,12 @@ function forgetAll() {
127327
127327
  const path2 = prefsPath();
127328
127328
  if ((0, import_node_fs25.existsSync)(path2)) (0, import_node_fs25.rmSync)(path2);
127329
127329
  }
127330
- var import_node_fs25, import_node_path27, log10, PrefsSchema;
127330
+ var import_node_fs25, import_node_path26, log10, PrefsSchema;
127331
127331
  var init_prefs = __esm({
127332
127332
  "src/lifecycle/prefs.ts"() {
127333
127333
  "use strict";
127334
127334
  import_node_fs25 = require("fs");
127335
- import_node_path27 = require("path");
127335
+ import_node_path26 = require("path");
127336
127336
  init_zod();
127337
127337
  init_logger();
127338
127338
  init_constants();
@@ -142973,6 +142973,7 @@ init_providers();
142973
142973
 
142974
142974
  // src/services/providers/providerHealth.ts
142975
142975
  var import_child_process2 = require("child_process");
142976
+ init_platform();
142976
142977
  init_providers();
142977
142978
 
142978
142979
  // src/services/providers/capabilities.ts
@@ -143013,11 +143014,18 @@ function parseVersionOutput(output) {
143013
143014
  return match2 ? match2[0] : null;
143014
143015
  }
143015
143016
  function runVersion(exe) {
143017
+ const viaShell = isWindows2 && /\.(?:cmd|bat)$/i.test(exe);
143018
+ const file2 = viaShell ? `"${exe}"` : exe;
143016
143019
  return new Promise((resolve4) => {
143017
- (0, import_child_process2.execFile)(exe, ["--version"], { timeout: VERSION_TIMEOUT_MS }, (err, stdout2, stderr) => {
143018
- if (err && !stdout2 && !stderr) return resolve4(null);
143019
- resolve4(parseVersionOutput(`${stdout2}${stderr}`));
143020
- });
143020
+ (0, import_child_process2.execFile)(
143021
+ file2,
143022
+ ["--version"],
143023
+ { timeout: VERSION_TIMEOUT_MS, shell: viaShell, windowsHide: true },
143024
+ (err, stdout2, stderr) => {
143025
+ if (err && !stdout2 && !stderr) return resolve4(null);
143026
+ resolve4(parseVersionOutput(`${stdout2}${stderr}`));
143027
+ }
143028
+ );
143021
143029
  });
143022
143030
  }
143023
143031
  function compareToVerified(version2, verified) {
@@ -143080,7 +143088,12 @@ async function providerHealth(name, resolveExe, detect = runVersion) {
143080
143088
  ]
143081
143089
  };
143082
143090
  }
143083
- const version2 = await detect(exe);
143091
+ let version2 = null;
143092
+ try {
143093
+ version2 = await detect(exe);
143094
+ } catch {
143095
+ version2 = null;
143096
+ }
143084
143097
  const warning = compareToVerified(version2, verifiedAgainst);
143085
143098
  return {
143086
143099
  name,
@@ -156914,6 +156927,9 @@ async function runProdDoctor(opts, deps = {}) {
156914
156927
  }
156915
156928
  return { findings, repairs, ...ptyHost ? { ptyHost } : {} };
156916
156929
  }
156930
+ function toPowerShellLiteral(value) {
156931
+ return `'${value.replace(/'/g, "''")}'`;
156932
+ }
156917
156933
  var defaultSpawnTail = ({ files, lines, follow }) => {
156918
156934
  const existing = files.filter((f2) => (0, import_node_fs23.existsSync)(f2));
156919
156935
  if (existing.length === 0) {
@@ -156922,11 +156938,18 @@ var defaultSpawnTail = ({ files, lines, follow }) => {
156922
156938
  message: `no log files found at: ${files.join(", ")}. The streamer may not have started yet, or the deploy uses a different log layout.`
156923
156939
  });
156924
156940
  }
156925
- const args = ["-n", String(lines)];
156926
- if (follow) args.push("-F");
156927
- args.push(...existing);
156941
+ const isWindows3 = process.platform === "win32";
156942
+ const args = isWindows3 ? [
156943
+ "-NoLogo",
156944
+ "-NoProfile",
156945
+ "-NonInteractive",
156946
+ "-Command",
156947
+ `Get-Content -LiteralPath @(${existing.map(toPowerShellLiteral).join(", ")}) -Tail ${lines}${follow ? " -Wait" : ""}`
156948
+ ] : ["-n", String(lines), ...follow ? ["-F"] : [], ...existing];
156928
156949
  return new Promise((resolve4) => {
156929
- const child = (0, import_node_child_process8.spawn)("tail", args, { stdio: ["ignore", "inherit", "inherit"] });
156950
+ const child = (0, import_node_child_process8.spawn)(isWindows3 ? "powershell.exe" : "tail", args, {
156951
+ stdio: ["ignore", "inherit", "inherit"]
156952
+ });
156930
156953
  child.on("error", (err) => resolve4({ ok: false, message: `tail failed: ${err.message}` }));
156931
156954
  child.on("exit", (code) => resolve4({ ok: code === 0 }));
156932
156955
  });
@@ -157313,8 +157336,8 @@ program2.command("cache").description("Manage the local SQLite conversation cach
157313
157336
  `${process.env.HOME}/.threadbase/cache`
157314
157337
  ).action((opts) => {
157315
157338
  const { rmSync: rmSync6, existsSync: existsSync16 } = require("fs");
157316
- const { join: join30 } = require("path");
157317
- const dbPath = join30(opts.cacheDir, "cache.db");
157339
+ const { join: join29 } = require("path");
157340
+ const dbPath = join29(opts.cacheDir, "cache.db");
157318
157341
  for (const suffix of ["", "-shm", "-wal"]) {
157319
157342
  const f2 = dbPath + suffix;
157320
157343
  if (existsSync16(f2)) {