smooth-operator-mcp 3.0.2 → 3.0.5

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.
@@ -354,7 +354,7 @@ function truncateUtf8(value, maxBytes) {
354
354
  while (low < high) {
355
355
  const midpoint = Math.ceil((low + high) / 2);
356
356
  const candidate = decoder.decode(bytes.slice(0, midpoint));
357
- if (UTF8_ENCODER.encode(candidate).byteLength <= maxBytes) {
357
+ if (UTF8_ENCODER.encode(candidate).byteLength <= boundedMaxBytes) {
358
358
  low = midpoint;
359
359
  } else {
360
360
  high = midpoint - 1;
@@ -397,7 +397,7 @@ var SERVER_VERSION;
397
397
  var init_version = __esm({
398
398
  "src/server/version.ts"() {
399
399
  "use strict";
400
- SERVER_VERSION = "3.0.2";
400
+ SERVER_VERSION = "3.0.5";
401
401
  }
402
402
  });
403
403
 
@@ -639,9 +639,9 @@ function resolveConfigPath(target, homeDirectory = homedir3(), environment = pro
639
639
  return join6(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
640
640
  }
641
641
  if (platform2() === "win32") {
642
- return join6(environment.APPDATA ?? join6(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
642
+ return join6(resolveConfigDirectory(environment.APPDATA, join6(home, "AppData", "Roaming")), "Claude", "claude_desktop_config.json");
643
643
  }
644
- return join6(environment.XDG_CONFIG_HOME ?? join6(home, ".config"), "Claude", "claude_desktop_config.json");
644
+ return join6(resolveConfigDirectory(environment.XDG_CONFIG_HOME, join6(home, ".config")), "Claude", "claude_desktop_config.json");
645
645
  }
646
646
  function resolveOpenCodeConfigPath(homeDirectory = homedir3(), environment = process.env, override) {
647
647
  if (override) {
@@ -650,9 +650,17 @@ function resolveOpenCodeConfigPath(homeDirectory = homedir3(), environment = pro
650
650
  if (environment.OPENCODE_CONFIG) {
651
651
  return resolve5(environment.OPENCODE_CONFIG);
652
652
  }
653
- const configDirectory = environment.OPENCODE_CONFIG_DIR ?? join6(homeDirectory || homedir3(), ".config", "opencode");
653
+ const configDirectory = resolveConfigDirectory(environment.OPENCODE_CONFIG_DIR, join6(homeDirectory || homedir3(), ".config", "opencode"));
654
654
  return join6(configDirectory, "opencode.json");
655
655
  }
656
+ function resolveConfigDirectory(value, fallback) {
657
+ const candidate = value?.trim() || fallback;
658
+ try {
659
+ return resolve5(candidate);
660
+ } catch (error) {
661
+ throw new AppError("INSTALL_CONFIG_INVALID", "Configuration directory paths must be valid filesystem paths.", { cause: error });
662
+ }
663
+ }
656
664
  async function installJsonConfig(target, plannedPath, options, allowOpenCodeJsoncFallback = false) {
657
665
  const path = target === "opencode" && allowOpenCodeJsoncFallback ? await chooseExistingOpenCodePath(plannedPath) : plannedPath;
658
666
  await ensureSecureDirectory(dirname4(path));
@@ -898,6 +906,9 @@ function sameOpenCodeEntry(value, desired) {
898
906
  }
899
907
  async function ensureSecureDirectory(path) {
900
908
  const absolute = resolve5(path);
909
+ if (parse3(absolute).root === absolute) {
910
+ throw new AppError("INSTALL_CONFIG_FAILED", "Configuration directories must not be filesystem roots.");
911
+ }
901
912
  await assertNoSymlinkComponents2(absolute, "configuration directory");
902
913
  await mkdir3(absolute, { recursive: true, mode: 448 });
903
914
  await assertNoSymlinkComponents2(absolute, "configuration directory");
@@ -1227,7 +1238,7 @@ __export(installer_wizard_exports, {
1227
1238
  runWizard: () => runWizard
1228
1239
  });
1229
1240
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7, parse as parse4, resolve as resolve6, win32 as win323 } from "node:path";
1230
- import { existsSync as existsSync2 } from "node:fs";
1241
+ import { accessSync as accessSync2, constants as constants3, statSync } from "node:fs";
1231
1242
  import { chmod as chmod3, lstat as lstat4, rename as rename4, unlink as unlink4, writeFile as writeFile2 } from "node:fs/promises";
1232
1243
  import { homedir as homedir4 } from "node:os";
1233
1244
  import { isIP as isIP3 } from "node:net";
@@ -1244,6 +1255,19 @@ function isAbsolutePath(value) {
1244
1255
  function isFilesystemRoot(value) {
1245
1256
  return isAbsolute4(value) && parse4(value).root === value || win323.isAbsolute(value) && win323.parse(value).root === value;
1246
1257
  }
1258
+ function isExecutableFile(path) {
1259
+ try {
1260
+ if (!statSync(path).isFile()) {
1261
+ return false;
1262
+ }
1263
+ if (process.platform !== "win32") {
1264
+ accessSync2(path, constants3.X_OK);
1265
+ }
1266
+ return true;
1267
+ } catch {
1268
+ return false;
1269
+ }
1270
+ }
1247
1271
  function isInteractive() {
1248
1272
  return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
1249
1273
  }
@@ -1313,8 +1337,8 @@ async function askBrowser(session, ui) {
1313
1337
  return detected[numeric - 1].path;
1314
1338
  }
1315
1339
  if (/^\d+$/.test(answer)) continue;
1316
- if (isAbsolutePath(answer) && existsSync2(answer)) return answer;
1317
- ui.failure("Enter a listed number or an existing absolute path.");
1340
+ if (isAbsolutePath(answer) && isExecutableFile(answer)) return answer;
1341
+ ui.failure("Enter a listed number or an existing executable file path.");
1318
1342
  }
1319
1343
  }
1320
1344
  while (true) {
@@ -1324,8 +1348,8 @@ async function askBrowser(session, ui) {
1324
1348
  ui.failure("Enter an absolute path.");
1325
1349
  continue;
1326
1350
  }
1327
- if (!existsSync2(answer)) {
1328
- ui.failure("That path does not exist.");
1351
+ if (!isExecutableFile(answer)) {
1352
+ ui.failure("That path is not an executable file.");
1329
1353
  continue;
1330
1354
  }
1331
1355
  return answer;
@@ -1560,16 +1584,65 @@ function writeSummary(ui, harness, choices) {
1560
1584
  ]);
1561
1585
  }
1562
1586
  async function defaultProbe(url, timeoutMs) {
1587
+ const controller = new AbortController();
1588
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1563
1589
  try {
1564
- const controller = new AbortController();
1565
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1566
1590
  const response = await fetch(url, { signal: controller.signal });
1567
- clearTimeout(timer);
1568
1591
  if (!response.ok) return { state: "no-file" };
1569
- const version = await response.json().catch(() => ({}));
1570
- return { state: "live", version };
1592
+ const version = await readProbeJson(response, controller.signal);
1593
+ return isDevToolsVersion(version) ? { state: "live", version } : { state: "no-file" };
1571
1594
  } catch {
1572
1595
  return { state: "no-file" };
1596
+ } finally {
1597
+ clearTimeout(timer);
1598
+ }
1599
+ }
1600
+ function isDevToolsVersion(value) {
1601
+ return isRecord4(value) && typeof value.Browser === "string" && typeof value.webSocketDebuggerUrl === "string" && /^wss?:\/\//i.test(value.webSocketDebuggerUrl);
1602
+ }
1603
+ async function readProbeJson(response, signal) {
1604
+ const declaredLength = Number(response.headers.get("content-length"));
1605
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_PROBE_RESPONSE_BYTES) {
1606
+ return void 0;
1607
+ }
1608
+ if (!response.body) {
1609
+ return void 0;
1610
+ }
1611
+ const reader = response.body.getReader();
1612
+ const chunks = [];
1613
+ let total = 0;
1614
+ try {
1615
+ while (true) {
1616
+ if (signal.aborted) {
1617
+ return void 0;
1618
+ }
1619
+ const next = await reader.read();
1620
+ if (next.done) {
1621
+ break;
1622
+ }
1623
+ if (!(next.value instanceof Uint8Array)) {
1624
+ return void 0;
1625
+ }
1626
+ total += next.value.byteLength;
1627
+ if (total > MAX_PROBE_RESPONSE_BYTES) {
1628
+ return void 0;
1629
+ }
1630
+ chunks.push(next.value);
1631
+ }
1632
+ } finally {
1633
+ await reader.cancel().catch(() => void 0);
1634
+ reader.releaseLock();
1635
+ }
1636
+ const bytes = new Uint8Array(total);
1637
+ let offset = 0;
1638
+ for (const chunk of chunks) {
1639
+ bytes.set(chunk, offset);
1640
+ offset += chunk.byteLength;
1641
+ }
1642
+ try {
1643
+ return JSON.parse(new TextDecoder().decode(bytes));
1644
+ } catch {
1645
+ return void 0;
1573
1646
  }
1574
1647
  }
1575
1648
  async function assertPrivateWizardConfig(handle) {
@@ -1671,16 +1744,21 @@ async function launchPersonalChrome(opts) {
1671
1744
  if (!safeDataDir || !isAbsolutePath(rawDataDir) || isFilesystemRoot(safeDataDir) || /[\u0000-\u001f\u007f]/.test(rawDataDir)) {
1672
1745
  throw new AppError("INSTALL_CONFIG_INVALID", "The personal Chrome data directory must be an absolute non-root path without control characters.");
1673
1746
  }
1747
+ if (opts.probeAttempts !== void 0 && (!Number.isSafeInteger(opts.probeAttempts) || opts.probeAttempts < 1 || opts.probeAttempts > MAX_PROBE_ATTEMPTS)) {
1748
+ throw new AppError("INSTALL_CONFIG_INVALID", `The personal Chrome probe attempt count must be an integer between 1 and ${MAX_PROBE_ATTEMPTS}.`);
1749
+ }
1750
+ await ensureSecureDirectory(safeDataDir);
1751
+ const personalProfileDir = join7(safeDataDir, "personal-chrome");
1752
+ await ensureSecureDirectory(personalProfileDir);
1674
1753
  const { findChromeExecutable: findChromeExecutable2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
1675
1754
  const executable = opts.executablePath ?? findChromeExecutable2()?.path;
1676
- if (!executable) {
1755
+ if (!executable || !isExecutableFile(executable)) {
1677
1756
  throw new AppError("BROWSER_NOT_CONFIGURED", "Install Chrome or set SMOOTH_OPERATOR_BROWSER_EXECUTABLE");
1678
1757
  }
1679
- await ensureSecureDirectory(safeDataDir);
1680
1758
  const spawnFn = opts.spawn ?? (await import("node:child_process")).spawn;
1681
1759
  const args = [
1682
1760
  `--remote-debugging-port=${port}`,
1683
- `--user-data-dir=${join7(safeDataDir, "personal-chrome")}`,
1761
+ `--user-data-dir=${personalProfileDir}`,
1684
1762
  "--no-first-run",
1685
1763
  "--no-default-browser-check",
1686
1764
  ...opts.headless ? ["--headless=new"] : []
@@ -1689,19 +1767,44 @@ async function launchPersonalChrome(opts) {
1689
1767
  child.unref();
1690
1768
  const probe = opts.probe;
1691
1769
  const attempts = opts.probeAttempts ?? DEFAULT_PROBE_ATTEMPTS;
1770
+ const deadline = opts.probeAttempts === void 0 ? Date.now() + DEFAULT_PROBE_DEADLINE_MS : void 0;
1771
+ let attemptsMade = 0;
1692
1772
  for (let attempt = 0; attempt < attempts; attempt += 1) {
1773
+ if (deadline !== void 0 && Date.now() >= deadline) {
1774
+ break;
1775
+ }
1693
1776
  if (attempt > 0) {
1694
- await new Promise((resolveTimeout) => setTimeout(resolveTimeout, PROBE_INTERVAL_MS));
1777
+ const remaining2 = deadline === void 0 ? PROBE_INTERVAL_MS : deadline - Date.now();
1778
+ if (remaining2 <= 0) break;
1779
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, Math.min(PROBE_INTERVAL_MS, remaining2)));
1695
1780
  }
1781
+ const remaining = deadline === void 0 ? PROBE_TIMEOUT_MS : Math.min(PROBE_TIMEOUT_MS, deadline - Date.now());
1782
+ if (remaining <= 0) break;
1783
+ attemptsMade += 1;
1696
1784
  try {
1697
- const res = await probe(`http://127.0.0.1:${port}/json/version`, 1e3);
1785
+ const res = await boundedProbe(probe, `http://127.0.0.1:${port}/json/version`, remaining);
1698
1786
  if (res.state === "live") return { url: `http://127.0.0.1:${port}` };
1699
1787
  } catch {
1700
1788
  }
1701
1789
  }
1702
- throw new AppError("BROWSER_CONNECT_TIMEOUT", `Chrome DevTools endpoint on port ${port} did not become ready after ${attempts} probes. Close Chrome or choose another port.`);
1790
+ throw new AppError("BROWSER_CONNECT_TIMEOUT", `Chrome DevTools endpoint on port ${port} did not become ready after ${attemptsMade} probes. Close Chrome or choose another port.`);
1791
+ }
1792
+ async function boundedProbe(probe, url, timeoutMs) {
1793
+ let timer;
1794
+ try {
1795
+ return await Promise.race([
1796
+ Promise.resolve().then(() => probe(url, timeoutMs)),
1797
+ new Promise((resolveProbe) => {
1798
+ timer = setTimeout(() => resolveProbe({ state: "timeout" }), timeoutMs);
1799
+ })
1800
+ ]);
1801
+ } finally {
1802
+ if (timer) {
1803
+ clearTimeout(timer);
1804
+ }
1805
+ }
1703
1806
  }
1704
- var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, MAX_WIZARD_CONFIG_BYTES, HARNESS_MENU, WIZARD_STEP_TOTAL;
1807
+ var PROBE_INTERVAL_MS, PROBE_TIMEOUT_MS, DEFAULT_PROBE_ATTEMPTS, DEFAULT_PROBE_DEADLINE_MS, MAX_PROBE_ATTEMPTS, MAX_PROBE_RESPONSE_BYTES, MAX_WIZARD_CONFIG_BYTES, HARNESS_MENU, WIZARD_STEP_TOTAL;
1705
1808
  var init_installer_wizard = __esm({
1706
1809
  "src/server/installer-wizard.ts"() {
1707
1810
  "use strict";
@@ -1710,7 +1813,11 @@ var init_installer_wizard = __esm({
1710
1813
  init_ui();
1711
1814
  init_version();
1712
1815
  PROBE_INTERVAL_MS = 300;
1816
+ PROBE_TIMEOUT_MS = 1e3;
1713
1817
  DEFAULT_PROBE_ATTEMPTS = 33;
1818
+ DEFAULT_PROBE_DEADLINE_MS = 1e4;
1819
+ MAX_PROBE_ATTEMPTS = 100;
1820
+ MAX_PROBE_RESPONSE_BYTES = 64 * 1024;
1714
1821
  MAX_WIZARD_CONFIG_BYTES = 2e6;
1715
1822
  HARNESS_MENU = [
1716
1823
  { id: "opencode", label: "OpenCode", description: "Configures ~/.config/opencode/opencode.json" },
@@ -1907,7 +2014,7 @@ function isValidDomainPattern(pattern) {
1907
2014
  }
1908
2015
  const bracketless = base.replace(/^\[|\]$/g, "");
1909
2016
  if (isIP(bracketless) !== 0) {
1910
- return true;
2017
+ return !wildcard;
1911
2018
  }
1912
2019
  const ascii = domainToASCII(base);
1913
2020
  return Boolean(ascii) && ascii.split(".").every((label) => label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label));
@@ -1999,6 +2106,18 @@ function canonicalizeAllowedFileRoots(rawRoots) {
1999
2106
  if (parse(canonicalRoot).root === canonicalRoot) {
2000
2107
  throw new AppError("CONFIG_INVALID", "Configured file roots must not be filesystem roots.");
2001
2108
  }
2109
+ try {
2110
+ if (!lstatSync(canonicalRoot).isDirectory()) {
2111
+ throw new AppError("CONFIG_INVALID", "Configured file roots must be directories.");
2112
+ }
2113
+ } catch (error) {
2114
+ if (error instanceof AppError) {
2115
+ throw error;
2116
+ }
2117
+ if (!isMissingPathError(error)) {
2118
+ throw new AppError("CONFIG_INSECURE", "Configured file roots could not be inspected safely.", { cause: error });
2119
+ }
2120
+ }
2002
2121
  if (!roots.includes(canonicalRoot)) {
2003
2122
  roots.push(canonicalRoot);
2004
2123
  }
@@ -2312,11 +2431,21 @@ function normalizeHostPattern(value) {
2312
2431
  throw new AppError("CONFIG_INVALID", "Configuration failed validation: configured HTTP host allowlists must contain hostnames or bracketed IPv6 addresses without ports.");
2313
2432
  }
2314
2433
  try {
2315
- return new URL(`http://${trimmed}`).hostname.toLowerCase();
2434
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
2435
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
2316
2436
  } catch (error) {
2317
2437
  throw new AppError("CONFIG_INVALID", "Configuration failed validation: configured HTTP host allowlists contain an invalid hostname.", { cause: error });
2318
2438
  }
2319
2439
  }
2440
+ function normalizeListenHost(value) {
2441
+ const trimmed = value.trim();
2442
+ try {
2443
+ const hostname = new URL(`http://${trimmed}`).hostname.toLowerCase();
2444
+ return hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
2445
+ } catch {
2446
+ return trimmed;
2447
+ }
2448
+ }
2320
2449
  function normalizeHostList(values) {
2321
2450
  return normalizeList(values).map(normalizeHostPattern);
2322
2451
  }
@@ -2329,7 +2458,7 @@ function isValidDomainPattern2(value) {
2329
2458
  }
2330
2459
  const bracketless = base.replace(/^\[|\]$/g, "");
2331
2460
  if (isIP2(bracketless) !== 0) {
2332
- return true;
2461
+ return !wildcard;
2333
2462
  }
2334
2463
  let ascii;
2335
2464
  try {
@@ -2575,7 +2704,7 @@ function loadServerConfig(args = [], environment = env, homeDirectory = homedir(
2575
2704
  const config = {
2576
2705
  transport: argValue("--transport") ?? environment.SMOOTH_OPERATOR_TRANSPORT ?? fileConfig.transport ?? "stdio",
2577
2706
  http: {
2578
- host: (argValue("--host") ?? environment.SMOOTH_OPERATOR_HTTP_HOST ?? nestedHttp.host ?? "127.0.0.1").trim(),
2707
+ host: normalizeListenHost(argValue("--host") ?? environment.SMOOTH_OPERATOR_HTTP_HOST ?? nestedHttp.host ?? "127.0.0.1"),
2579
2708
  port: parseInteger(argValue("--port") ?? environment.SMOOTH_OPERATOR_HTTP_PORT, nestedHttp.port ?? 3344),
2580
2709
  path: (environment.SMOOTH_OPERATOR_HTTP_PATH ?? nestedHttp.path ?? "/mcp").trim(),
2581
2710
  token: environment.SMOOTH_OPERATOR_HTTP_TOKEN ?? nestedHttp.token,
@@ -4554,11 +4683,31 @@ var DEFAULT_TYPE = {
4554
4683
  rng: Math.random
4555
4684
  };
4556
4685
  function randomRange(min, max, rand = Math.random) {
4557
- return min + rand() * (max - min);
4686
+ const sample = rand();
4687
+ const boundedSample = Number.isFinite(sample) ? Math.min(1, Math.max(0, sample)) : 0;
4688
+ return min + boundedSample * (max - min);
4558
4689
  }
4559
- function sleep(ms) {
4560
- return new Promise((resolve7) => {
4561
- setTimeout(resolve7, Math.max(0, ms));
4690
+ function sleep(ms, signal) {
4691
+ if (signal?.aborted) {
4692
+ return Promise.reject(new Error("Operation aborted"));
4693
+ }
4694
+ return new Promise((resolve7, reject) => {
4695
+ let settled = false;
4696
+ const timer = setTimeout(() => finish(resolve7), Math.max(0, ms));
4697
+ const onAbort = () => finish(() => reject(new Error("Operation aborted")));
4698
+ const finish = (callback) => {
4699
+ if (settled) {
4700
+ return;
4701
+ }
4702
+ settled = true;
4703
+ clearTimeout(timer);
4704
+ signal?.removeEventListener("abort", onAbort);
4705
+ callback();
4706
+ };
4707
+ signal?.addEventListener("abort", onAbort, { once: true });
4708
+ if (signal?.aborted) {
4709
+ onAbort();
4710
+ }
4562
4711
  });
4563
4712
  }
4564
4713
  async function humanMouseMove(page, x1, y1, x2, y2, durationMs = 80, options = {}) {
@@ -4577,15 +4726,18 @@ async function humanType(page, text, options = {}) {
4577
4726
  const cfg = { ...DEFAULT_TYPE, ...options, rng };
4578
4727
  const keyboard = page.keyboard;
4579
4728
  for (const char of text) {
4729
+ if (cfg.signal?.aborted) {
4730
+ throw new Error("Operation aborted");
4731
+ }
4580
4732
  if (char === " ") {
4581
4733
  await keyboard.down("Space");
4582
4734
  await keyboard.up("Space");
4583
4735
  } else {
4584
4736
  await keyboard.type(char);
4585
4737
  }
4586
- await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng));
4738
+ await sleep(randomRange(cfg.minDelayMs, cfg.maxDelayMs, cfg.rng), cfg.signal);
4587
4739
  if (cfg.rng() < cfg.thinkPauseChance) {
4588
- await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng));
4740
+ await sleep(randomRange(cfg.thinkPauseMinMs, cfg.thinkPauseMaxMs, cfg.rng), cfg.signal);
4589
4741
  }
4590
4742
  }
4591
4743
  }
@@ -9307,6 +9459,7 @@ var BrowserService = class {
9307
9459
  try {
9308
9460
  await humanMouseMove(state.page, 0, 0, centerX, centerY, 80);
9309
9461
  } catch {
9462
+ throwIfAborted(signal);
9310
9463
  }
9311
9464
  }
9312
9465
  async clickTarget(state, target, button, clickCount, signal, frame = state.page.mainFrame(), pointerType = "mouse") {
@@ -9820,7 +9973,7 @@ var BrowserService = class {
9820
9973
  if (!nativeControlValueSet) {
9821
9974
  throwIfAborted(signal);
9822
9975
  if (this.stealthSettings().behaviorEnabled) {
9823
- await humanType(state.page, text);
9976
+ await humanType(state.page, text, { signal });
9824
9977
  } else {
9825
9978
  await state.page.keyboard.type(text);
9826
9979
  }