smooth-operator-mcp 3.0.2 → 3.0.4

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.
@@ -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.4";
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.`);
1703
1791
  }
1704
- var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, MAX_WIZARD_CONFIG_BYTES, HARNESS_MENU, WIZARD_STEP_TOTAL;
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
+ }
1806
+ }
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,