smooth-operator-mcp 2.2.0 → 2.3.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/README.md CHANGED
@@ -27,7 +27,7 @@ Or straight from GitHub:
27
27
  npm install -g github:Gitshop77/Smooth-Operator && smooth-operator install opencode
28
28
  ~~~
29
29
 
30
- Wizard asks 6 questions (managed vs personal Chrome, headless, domains, JS). Run `smooth-operator install` with no harness to pick one interactively (TTY only; piped/CI runs print usage and exit). `--yes` skips every prompt, so give it a target: `smooth-operator install opencode --yes`. Personal Chrome → wizard launches Chrome on `9222` for you.
30
+ Wizard asks 7 questions (browser — any Chromium-based browser is detected, managed vs personal Chrome, headless, domains, JS). Run `smooth-operator install` with no harness to pick one interactively (TTY only; piped/CI runs print usage and exit). `--yes` skips every prompt, so give it a target: `smooth-operator install opencode --yes`. Personal Chrome → wizard launches Chrome on `9222` for you.
31
31
 
32
32
  Requires Node 22.23.2+ and Chrome. Profile at `~/.smooth-operator/browser` — sign in once.
33
33
 
@@ -287,7 +287,8 @@ var init_errors = __esm({
287
287
  var discovery_exports = {};
288
288
  __export(discovery_exports, {
289
289
  chromeExecutableSearchPaths: () => chromeExecutableSearchPaths,
290
- findChromeExecutable: () => findChromeExecutable
290
+ findChromeExecutable: () => findChromeExecutable,
291
+ findChromiumExecutables: () => findChromiumExecutables
291
292
  });
292
293
  import * as nodeFs from "node:fs";
293
294
  import { homedir as homedir2 } from "node:os";
@@ -296,6 +297,9 @@ import { env as env2 } from "node:process";
296
297
  function findChromeExecutable(fs = nodeFs) {
297
298
  return chromeExecutableCandidates().find((candidate) => fs.existsSync(candidate.path)) ?? null;
298
299
  }
300
+ function findChromiumExecutables(fs = nodeFs) {
301
+ return chromeExecutableCandidates().filter((candidate) => fs.existsSync(candidate.path));
302
+ }
299
303
  function chromeExecutableSearchPaths() {
300
304
  return chromeExecutableCandidates().map((candidate) => candidate.path);
301
305
  }
@@ -309,16 +313,23 @@ function chromeExecutableCandidates() {
309
313
  function macOsCandidates() {
310
314
  const applicationDirectories = ["/Applications", join2(homedir2(), "Applications")];
311
315
  return [
312
- ...applicationBundleCandidates(applicationDirectories, "Google Chrome", "Google Chrome", "stable"),
313
- ...applicationBundleCandidates(applicationDirectories, "Google Chrome Beta", "Google Chrome Beta", "beta"),
314
- ...applicationBundleCandidates(applicationDirectories, "Google Chrome Dev", "Google Chrome Dev", "dev"),
315
- ...applicationBundleCandidates(applicationDirectories, "Google Chrome Canary", "Google Chrome Canary", "canary")
316
+ ...applicationBundleCandidates(applicationDirectories, "Google Chrome", "Google Chrome", "Google Chrome", "stable"),
317
+ ...applicationBundleCandidates(applicationDirectories, "Google Chrome Beta", "Google Chrome Beta", "Google Chrome Beta", "beta"),
318
+ ...applicationBundleCandidates(applicationDirectories, "Google Chrome Dev", "Google Chrome Dev", "Google Chrome Dev", "dev"),
319
+ ...applicationBundleCandidates(applicationDirectories, "Google Chrome Canary", "Google Chrome Canary", "Google Chrome Canary", "canary"),
320
+ ...applicationBundleCandidates(applicationDirectories, "Brave Browser", "Brave Browser", "Brave", "stable"),
321
+ ...applicationBundleCandidates(applicationDirectories, "Microsoft Edge", "Microsoft Edge", "Microsoft Edge", "stable"),
322
+ ...applicationBundleCandidates(applicationDirectories, "Chromium", "Chromium", "Chromium", "stable"),
323
+ ...applicationBundleCandidates(applicationDirectories, "Vivaldi", "Vivaldi", "Vivaldi", "stable"),
324
+ ...applicationBundleCandidates(applicationDirectories, "Arc", "Arc", "Arc", "stable"),
325
+ ...applicationBundleCandidates(applicationDirectories, "Opera", "Opera", "Opera", "stable")
316
326
  ];
317
327
  }
318
- function applicationBundleCandidates(applicationDirectories, bundleName, executableName, channel) {
328
+ function applicationBundleCandidates(applicationDirectories, bundleName, executableName, label, channel) {
319
329
  return applicationDirectories.map((applicationDirectory) => ({
320
330
  path: join2(applicationDirectory, `${bundleName}.app`, "Contents", "MacOS", executableName),
321
- channel
331
+ channel,
332
+ label
322
333
  }));
323
334
  }
324
335
  function windowsCandidates() {
@@ -328,29 +339,39 @@ function windowsCandidates() {
328
339
  env2.LOCALAPPDATA
329
340
  ].filter((directory) => Boolean(directory));
330
341
  return [
331
- ...windowsChannelCandidates(directories, "Chrome", "stable"),
332
- ...windowsChannelCandidates(directories, "Chrome Beta", "beta"),
333
- ...windowsChannelCandidates(directories, "Chrome Dev", "dev"),
334
- ...windowsChannelCandidates(directories, "Chrome SxS", "canary")
342
+ ...windowsChannelCandidates(directories, ["Google", "Chrome"], "chrome.exe", "Google Chrome", "stable"),
343
+ ...windowsChannelCandidates(directories, ["Google", "Chrome Beta"], "chrome.exe", "Google Chrome Beta", "beta"),
344
+ ...windowsChannelCandidates(directories, ["Google", "Chrome Dev"], "chrome.exe", "Google Chrome Dev", "dev"),
345
+ ...windowsChannelCandidates(directories, ["Google", "Chrome SxS"], "chrome.exe", "Google Chrome Canary", "canary"),
346
+ ...windowsChannelCandidates(directories, ["BraveSoftware", "Brave-Browser"], "brave.exe", "Brave", "stable"),
347
+ ...windowsChannelCandidates(directories, ["Microsoft", "Edge"], "msedge.exe", "Microsoft Edge", "stable"),
348
+ ...windowsChannelCandidates(directories, ["Chromium"], "chrome.exe", "Chromium", "stable"),
349
+ ...windowsChannelCandidates(directories, ["Vivaldi"], "vivaldi.exe", "Vivaldi", "stable")
335
350
  ];
336
351
  }
337
- function windowsChannelCandidates(directories, directory, channel) {
352
+ function windowsChannelCandidates(directories, directoryParts, executable, label, channel) {
338
353
  return directories.map((baseDirectory) => ({
339
- path: win32.join(baseDirectory, "Google", directory, "Application", "chrome.exe"),
340
- channel
354
+ path: win32.join(baseDirectory, ...directoryParts, "Application", executable),
355
+ channel,
356
+ label
341
357
  }));
342
358
  }
343
359
  function linuxCandidates() {
344
360
  const searchDirectories = (env2.PATH ?? "").split(delimiter).filter(Boolean);
345
361
  const commands = [
346
- ["google-chrome", "stable"],
347
- ["google-chrome-stable", "stable"],
348
- ["google-chrome-beta", "beta"],
349
- ["google-chrome-unstable", "dev"],
350
- ["chromium", "stable"],
351
- ["chromium-browser", "stable"]
362
+ ["google-chrome", "Google Chrome", "stable"],
363
+ ["google-chrome-stable", "Google Chrome", "stable"],
364
+ ["google-chrome-beta", "Google Chrome Beta", "beta"],
365
+ ["google-chrome-unstable", "Google Chrome Dev", "dev"],
366
+ ["brave-browser", "Brave", "stable"],
367
+ ["microsoft-edge", "Microsoft Edge", "stable"],
368
+ ["microsoft-edge-stable", "Microsoft Edge", "stable"],
369
+ ["chromium", "Chromium", "stable"],
370
+ ["chromium-browser", "Chromium", "stable"],
371
+ ["vivaldi", "Vivaldi", "stable"],
372
+ ["vivaldi-stable", "Vivaldi", "stable"]
352
373
  ];
353
- return commands.flatMap(([command, channel]) => searchDirectories.map((directory) => ({ path: join2(directory, command), channel })));
374
+ return commands.flatMap(([command, label, channel]) => searchDirectories.map((directory) => ({ path: join2(directory, command), channel, label })));
354
375
  }
355
376
  var init_discovery = __esm({
356
377
  "src/server/browser/discovery.ts"() {
@@ -366,8 +387,8 @@ __export(installer_exports, {
366
387
  planHarnessInstall: () => planHarnessInstall,
367
388
  supportedHarnessTargets: () => supportedHarnessTargets
368
389
  });
369
- import { constants as constants2 } from "node:fs";
370
- import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open2, rename as rename2, unlink as unlink3, writeFile } from "node:fs/promises";
390
+ import { constants as constants2, accessSync, existsSync } from "node:fs";
391
+ import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open2, rename as rename3, unlink as unlink3, writeFile } from "node:fs/promises";
371
392
  import { execFile } from "node:child_process";
372
393
  import { randomUUID as randomUUID3 } from "node:crypto";
373
394
  import { homedir as homedir3, platform as platform2 } from "node:os";
@@ -394,7 +415,7 @@ function planHarnessInstall(target, options = {}) {
394
415
  return { kind: "cli", target: normalized, command: "codex", args: ["mcp", "add", SERVER_NAME, "--", cliEntry.command, ...cliEntry.args] };
395
416
  }
396
417
  if (normalized === "gemini") {
397
- return { kind: "cli", target: normalized, command: "gemini", args: ["mcp", "add", SERVER_NAME, cliEntry.command, ...cliEntry.args, "--scope", "user"] };
418
+ return { kind: "cli", target: normalized, command: "gemini", args: ["mcp", "add", "--scope", "user", SERVER_NAME, cliEntry.command, ...cliEntry.args] };
398
419
  }
399
420
  if (normalized === "vscode") {
400
421
  return {
@@ -444,10 +465,21 @@ function normalizeTarget(target) {
444
465
  function resolveServerEntry() {
445
466
  const modulePath = fileURLToPath(import.meta.url);
446
467
  if (basename4(modulePath) === "smooth-operator.mjs") {
447
- return { command: process.execPath, args: [modulePath] };
468
+ return { command: resolveStableNodeExecutable(), args: [modulePath] };
448
469
  }
449
470
  return { command: "smooth-operator", args: [] };
450
471
  }
472
+ function resolveStableNodeExecutable() {
473
+ const candidates = platform2() === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node"];
474
+ for (const candidate of candidates) {
475
+ try {
476
+ accessSync(candidate, constants2.X_OK);
477
+ return candidate;
478
+ } catch {
479
+ }
480
+ }
481
+ return process.execPath;
482
+ }
451
483
  async function runCliInstall(plan, executeCommand) {
452
484
  const runner = executeCommand ?? (async (command, args) => {
453
485
  await execFileAsync(command, [...args], {
@@ -556,7 +588,7 @@ async function installJsonConfig(target, plannedPath, options, allowOpenCodeJson
556
588
  `, { mode: 384, flag: "wx" });
557
589
  await chmod2(tempPath, 384);
558
590
  await rejectSymlink2(path, "configuration file");
559
- await rename2(tempPath, path);
591
+ await rename3(tempPath, path);
560
592
  return `Installed SmoothOperator in ${path}${backupPath ? ` (backup: ${backupPath})` : ""}. Restart the harness.`;
561
593
  } catch (error) {
562
594
  await unlink3(tempPath).catch(() => void 0);
@@ -614,6 +646,26 @@ async function chooseExistingOpenCodePath(plannedPath) {
614
646
  }
615
647
  return plannedPath;
616
648
  }
649
+ function isStaleEmbeddedCommand(command) {
650
+ if (typeof command !== "string" || !command.includes("/")) {
651
+ return false;
652
+ }
653
+ return !existsSync(command);
654
+ }
655
+ function isStaleEmbeddedCommandArray(command) {
656
+ if (!Array.isArray(command) || command.length === 0) {
657
+ return false;
658
+ }
659
+ const interpreter = command[0];
660
+ return typeof interpreter === "string" && interpreter.includes("/") && !existsSync(interpreter);
661
+ }
662
+ function isRepairableStdioEntry(existing, entry) {
663
+ return isStaleEmbeddedCommand(existing.command) && Array.isArray(existing.args) && existing.args.length === entry.args.length && existing.args.every((arg, index) => arg === entry.args[index]);
664
+ }
665
+ function isRepairableOpenCodeEntry(existing, desired) {
666
+ const repaired = { ...existing, command: desired.command };
667
+ return isStaleEmbeddedCommandArray(existing.command) && sameOpenCodeEntry(repaired, desired);
668
+ }
617
669
  function mergeMcpServersConfig(config, entry, path) {
618
670
  if (config.mcpServers !== void 0 && !isRecord3(config.mcpServers)) {
619
671
  throw new AppError("INSTALL_CONFIG_INVALID", `The mcpServers value in ${path} must be an object; refusing to replace it.`);
@@ -621,9 +673,16 @@ function mergeMcpServersConfig(config, entry, path) {
621
673
  const servers = isRecord3(config.mcpServers) ? { ...config.mcpServers } : {};
622
674
  const existing = servers[SERVER_NAME];
623
675
  if (existing !== void 0) {
624
- if (!isRecord3(existing) || !sameStdioEntry(existing, entry)) {
676
+ if (!isRecord3(existing)) {
625
677
  throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
626
678
  }
679
+ if (!sameStdioEntry(existing, entry)) {
680
+ if (!isRepairableStdioEntry(existing, entry)) {
681
+ throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
682
+ }
683
+ servers[SERVER_NAME] = { command: entry.command, args: [...entry.args] };
684
+ return { config: { ...config, mcpServers: servers }, alreadyConfigured: false };
685
+ }
627
686
  return { config, alreadyConfigured: true };
628
687
  }
629
688
  servers[SERVER_NAME] = { command: entry.command, args: [...entry.args] };
@@ -645,10 +704,24 @@ function mergeOpenCodeConfig(config, entry, path) {
645
704
  const existing = servers[SERVER_NAME];
646
705
  const desired = modernSchema ? { type: "local", command: [entry.command, ...entry.args] } : { type: "local", command: [entry.command, ...entry.args], enabled: true };
647
706
  if (existing !== void 0) {
648
- if (!isRecord3(existing) || !sameOpenCodeEntry(existing, desired)) {
707
+ if (!isRecord3(existing)) {
649
708
  throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
650
709
  }
651
- return { config, alreadyConfigured: true };
710
+ if (!sameOpenCodeEntry(existing, desired)) {
711
+ if (!isRepairableOpenCodeEntry(existing, desired)) {
712
+ throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
713
+ }
714
+ existing.command = desired.command;
715
+ } else {
716
+ return { config, alreadyConfigured: true };
717
+ }
718
+ servers[SERVER_NAME] = existing;
719
+ if (modernSchema) {
720
+ mcp.servers = servers;
721
+ } else {
722
+ Object.assign(mcp, servers);
723
+ }
724
+ return { config: { ...config, mcp }, alreadyConfigured: false };
652
725
  }
653
726
  servers[SERVER_NAME] = desired;
654
727
  if (modernSchema) {
@@ -985,6 +1058,7 @@ __export(installer_wizard_exports, {
985
1058
  runWizard: () => runWizard
986
1059
  });
987
1060
  import { join as join7 } from "node:path";
1061
+ import { existsSync as existsSync2 } from "node:fs";
988
1062
  import { homedir as homedir4 } from "node:os";
989
1063
  function isRecord4(value) {
990
1064
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -1041,9 +1115,43 @@ function recommendedDefaults(homeDir) {
1041
1115
  allowedDomains: [],
1042
1116
  blockedDomains: [],
1043
1117
  allowEval: false,
1044
- dataDir: join7(homeDir ?? homedir4(), ".smooth-operator")
1118
+ dataDir: join7(homeDir ?? homedir4(), ".smooth-operator"),
1119
+ browserExecutablePath: void 0
1045
1120
  };
1046
1121
  }
1122
+ async function askBrowser(session, ui) {
1123
+ const { findChromiumExecutables: findChromiumExecutables2 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
1124
+ const detected = findChromiumExecutables2();
1125
+ if (detected.length > 0) {
1126
+ detected.forEach((candidate, index) => {
1127
+ ui.option(index + 1, candidate.label, candidate.path, index === 0);
1128
+ });
1129
+ while (true) {
1130
+ const answer = (await session.question(`Browser [1]: `)).trim();
1131
+ if (!answer || answer === "1") return detected[0].path;
1132
+ const numeric = Number.parseInt(answer, 10);
1133
+ if (`${numeric}` === answer && numeric >= 1 && numeric <= detected.length) {
1134
+ return detected[numeric - 1].path;
1135
+ }
1136
+ if (/^\d+$/.test(answer)) continue;
1137
+ if (answer.startsWith("/") && existsSync2(answer)) return answer;
1138
+ ui.failure("Enter a listed number or an existing absolute path.");
1139
+ }
1140
+ }
1141
+ while (true) {
1142
+ const answer = (await session.question("Browser executable path (Enter = auto-detect): ")).trim();
1143
+ if (!answer) return void 0;
1144
+ if (!answer.startsWith("/")) {
1145
+ ui.failure("Enter an absolute path.");
1146
+ continue;
1147
+ }
1148
+ if (!existsSync2(answer)) {
1149
+ ui.failure("That path does not exist.");
1150
+ continue;
1151
+ }
1152
+ return answer;
1153
+ }
1154
+ }
1047
1155
  function tolerantQuestion(rl) {
1048
1156
  return {
1049
1157
  async question(prompt) {
@@ -1097,7 +1205,7 @@ async function runWizard(harness, opts) {
1097
1205
  ui.note(`Configuring: ${harness}`);
1098
1206
  ui.note("Answer each question, or press Enter to accept the recommended default.");
1099
1207
  ui.note(`You can re-run \`smooth-operator install ${harness}\` at any time to change these.`);
1100
- ui.step(1, 6, "Browser mode");
1208
+ ui.step(1, WIZARD_STEP_TOTAL, "Browser mode");
1101
1209
  ui.explain([
1102
1210
  "Who owns the Chrome window your AI drives?",
1103
1211
  "",
@@ -1136,16 +1244,24 @@ async function runWizard(harness, opts) {
1136
1244
  }
1137
1245
  ui.failure("Enter 1, 2, or 3.");
1138
1246
  }
1247
+ let browserExecutablePath;
1139
1248
  let headlessChoice = false;
1140
1249
  if (mode !== "disabled") {
1141
- ui.step(2, 6, "Headless mode");
1250
+ ui.step(2, WIZARD_STEP_TOTAL, "Browser");
1251
+ ui.explain([
1252
+ "Any Chromium-based browser works: Chrome, Brave, Edge, Chromium,",
1253
+ "Vivaldi, Arc, Opera. The AI gets its own isolated profile inside the",
1254
+ "browser you pick - your everyday profiles are never touched."
1255
+ ]);
1256
+ browserExecutablePath = await askBrowser(session, ui);
1257
+ ui.step(3, WIZARD_STEP_TOTAL, "Headless mode");
1142
1258
  ui.explain([
1143
1259
  "Headless runs Chrome with no visible window - lighter and invisible.",
1144
1260
  "Visible Chrome lets you watch clicks happen and handle CAPTCHAs or",
1145
1261
  "logins yourself when a site pauses for human verification."
1146
1262
  ]);
1147
1263
  headlessChoice = await askYesNo(session, "Run Chrome headless (no window)?", false);
1148
- ui.step(3, 6, "Allowed domains");
1264
+ ui.step(4, WIZARD_STEP_TOTAL, "Allowed domains");
1149
1265
  ui.explain([
1150
1266
  "Restrict which sites the AI may open, e.g. docs.example.com, *.wikipedia.org",
1151
1267
  "Leave empty to allow every site. Blocked domains always win over allowed ones."
@@ -1158,7 +1274,7 @@ async function runWizard(harness, opts) {
1158
1274
  }
1159
1275
  ui.failure("That did not look like a domain list. Example: example.com, *.shop.test");
1160
1276
  }
1161
- ui.step(4, 6, "Blocked domains");
1277
+ ui.step(5, WIZARD_STEP_TOTAL, "Blocked domains");
1162
1278
  ui.explain(["Never open these sites, even when everything else is allowed."]);
1163
1279
  while (true) {
1164
1280
  const parsed = parseDomainList(await session.question("Blocked domains (comma-separated, Enter for none): "));
@@ -1168,13 +1284,13 @@ async function runWizard(harness, opts) {
1168
1284
  }
1169
1285
  ui.failure("That did not look like a domain list. Example: ads.example.com");
1170
1286
  }
1171
- ui.step(5, 6, "JavaScript execution");
1287
+ ui.step(6, WIZARD_STEP_TOTAL, "JavaScript execution");
1172
1288
  ui.explain([
1173
1289
  "browser_evaluate runs arbitrary JavaScript on a page - powerful for scraping",
1174
1290
  "but it can also trigger bot defenses. Most users never need it on."
1175
1291
  ]);
1176
1292
  allowEval = await askYesNo(session, "Allow the AI to run JavaScript on pages?", false);
1177
- ui.step(6, 6, "Data directory");
1293
+ ui.step(7, WIZARD_STEP_TOTAL, "Data directory");
1178
1294
  ui.explain([
1179
1295
  "Where the private Chrome profile, logs, and downloads live.",
1180
1296
  "Permissions are locked to 0600 so only your user can read them."
@@ -1182,8 +1298,8 @@ async function runWizard(harness, opts) {
1182
1298
  while (dataDir === defaults.dataDir) {
1183
1299
  const answer = (await session.question(`Data directory [${defaults.dataDir}]: `)).trim();
1184
1300
  if (!answer) break;
1185
- if (!answer.startsWith("/")) {
1186
- ui.failure("Enter an absolute path (starting with /).");
1301
+ if (!answer.startsWith("/") || answer.replace(/\/+$/, "") === "") {
1302
+ ui.failure("Enter an absolute path other than the filesystem root.");
1187
1303
  continue;
1188
1304
  }
1189
1305
  dataDir = answer;
@@ -1202,8 +1318,8 @@ async function runWizard(harness, opts) {
1202
1318
  }
1203
1319
  }
1204
1320
  }
1205
- writeSummary(ui, harness, { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir });
1206
- return { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir, browserUrl };
1321
+ writeSummary(ui, harness, { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir, browserExecutablePath });
1322
+ return { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir, browserUrl, browserExecutablePath };
1207
1323
  } finally {
1208
1324
  rl.close();
1209
1325
  }
@@ -1217,6 +1333,7 @@ function writeSummary(ui, harness, choices) {
1217
1333
  ui.banner("Configuration Summary", `Ready to configure ${harness}`, "");
1218
1334
  ui.keyValues([
1219
1335
  ["Browser mode", choices.mode === "connect" ? modeLabel.connect : modeLabel[choices.mode] ?? choices.mode],
1336
+ ...choices.browserExecutablePath ? [["Browser", choices.browserExecutablePath]] : [],
1220
1337
  ["Headless", choices.headless ? "yes - no visible window" : "no - you can watch and intervene"],
1221
1338
  ...choices.mode === "disabled" ? [] : [
1222
1339
  ["Allowed sites", choices.allowedDomains.length ? choices.allowedDomains.join(", ") : "all sites"],
@@ -1241,7 +1358,7 @@ async function defaultProbe(url, timeoutMs) {
1241
1358
  }
1242
1359
  async function persistWizardConfig(choices, homeDir) {
1243
1360
  const { join: join8, dirname: dirname5, resolve: resolve6 } = await import("node:path");
1244
- const { mkdir: mkdir4, chmod: chmod3, lstat: lstat4, readFile: readFile3, writeFile: writeFile2, rename: rename3 } = await import("node:fs/promises");
1361
+ const { mkdir: mkdir4, chmod: chmod3, lstat: lstat4, readFile: readFile3, writeFile: writeFile2, rename: rename4 } = await import("node:fs/promises");
1245
1362
  const configPath = resolve6(join8(homeDir, ".smooth-operator/config.json"));
1246
1363
  await mkdir4(dirname5(configPath), { recursive: true, mode: 448 });
1247
1364
  await chmod3(dirname5(configPath), 448).catch(() => {
@@ -1273,6 +1390,9 @@ async function persistWizardConfig(choices, homeDir) {
1273
1390
  if (choices.browserUrl) {
1274
1391
  chosenBrowser.url = choices.browserUrl;
1275
1392
  }
1393
+ if (choices.browserExecutablePath) {
1394
+ chosenBrowser.executablePath = choices.browserExecutablePath;
1395
+ }
1276
1396
  const chosenSecurity = {
1277
1397
  allowEval: choices.allowEval,
1278
1398
  allowedDomains: choices.allowedDomains,
@@ -1287,7 +1407,8 @@ async function persistWizardConfig(choices, homeDir) {
1287
1407
  if (choices.dataDir !== defaultDataDir) {
1288
1408
  config.dataDir = choices.dataDir;
1289
1409
  }
1290
- const tmpPath = `${configPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1410
+ const { randomUUID: randomUUID4 } = await import("node:crypto");
1411
+ const tmpPath = `${configPath}.tmp-${process.pid}-${randomUUID4()}`;
1291
1412
  await writeFile2(tmpPath, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
1292
1413
  await chmod3(tmpPath, 384);
1293
1414
  try {
@@ -1312,7 +1433,7 @@ async function persistWizardConfig(choices, homeDir) {
1312
1433
  } catch (error) {
1313
1434
  if (!isMissingPathError2(error)) throw error;
1314
1435
  }
1315
- await rename3(tmpPath, configPath);
1436
+ await rename4(tmpPath, configPath);
1316
1437
  await chmod3(configPath, 384);
1317
1438
  }
1318
1439
  async function launchPersonalChrome(opts) {
@@ -1340,7 +1461,7 @@ async function launchPersonalChrome(opts) {
1340
1461
  }
1341
1462
  throw new AppError2("BROWSER_CONNECT_TIMEOUT", `Chrome DevTools endpoint on port ${port} did not become ready after ${attempts} probes. Close Chrome or choose another port.`);
1342
1463
  }
1343
- var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, HARNESS_MENU;
1464
+ var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, HARNESS_MENU, WIZARD_STEP_TOTAL;
1344
1465
  var init_installer_wizard = __esm({
1345
1466
  "src/server/installer-wizard.ts"() {
1346
1467
  "use strict";
@@ -1358,6 +1479,7 @@ var init_installer_wizard = __esm({
1358
1479
  { id: "windsurf", label: "Windsurf", description: "Adds SmoothOperator to Windsurf's mcp_config.json" },
1359
1480
  { id: "claude-desktop", label: "Claude Desktop", description: "Updates claude_desktop_config.json" }
1360
1481
  ];
1482
+ WIZARD_STEP_TOTAL = 7;
1361
1483
  }
1362
1484
  });
1363
1485
 
@@ -2230,7 +2352,7 @@ init_errors();
2230
2352
  init_logger();
2231
2353
 
2232
2354
  // src/server/version.ts
2233
- var SERVER_VERSION = "2.2.0";
2355
+ var SERVER_VERSION = "2.3.0";
2234
2356
 
2235
2357
  // src/server/mcp.ts
2236
2358
  var EmptyInputSchema = z3.object({}).strict();
@@ -3055,7 +3177,7 @@ function boundToolError(result) {
3055
3177
  init_logger();
3056
3178
 
3057
3179
  // src/server/runtime.ts
3058
- import { chmod, lstat as lstat2, mkdir as mkdir2, open, readFile as readFile2, realpath as realpath2, unlink as unlink2 } from "node:fs/promises";
3180
+ import { chmod, lstat as lstat2, mkdir as mkdir2, open, readFile as readFile2, realpath as realpath2, rename as rename2, unlink as unlink2 } from "node:fs/promises";
3059
3181
  import { randomUUID as randomUUID2 } from "node:crypto";
3060
3182
  import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
3061
3183
  import process3 from "node:process";
@@ -7806,12 +7928,41 @@ async function acquireBrowserProfileLease(profileDirectory) {
7806
7928
  throw new AppError("BROWSER_PROFILE_LOCK_FAILED", "The native browser profile has an unreadable lock. Verify that no SmoothOperator process is using it, then remove the lock file.", { retryable: true });
7807
7929
  }
7808
7930
  if (existing === "stale") {
7809
- throw new AppError("BROWSER_PROFILE_LOCK_FAILED", "The native browser profile has a stale lock. Verify that no SmoothOperator process is using it, then remove the lock file and retry.", { retryable: true });
7931
+ if (await reclaimStaleLock(lockPath)) {
7932
+ continue;
7933
+ }
7934
+ throw new AppError("BROWSER_PROFILE_LOCK_FAILED", "The native browser profile has a stale lock that could not be reclaimed. Verify that no SmoothOperator process is using it, then remove the lock file and retry.", { retryable: true });
7810
7935
  }
7811
7936
  }
7812
7937
  }
7813
7938
  throw new AppError("BROWSER_PROFILE_IN_USE", "The native browser profile became busy while it was being acquired.", { retryable: true });
7814
7939
  }
7940
+ async function reclaimStaleLock(lockPath) {
7941
+ try {
7942
+ const before = await lstat2(lockPath);
7943
+ const raw = await readFile2(lockPath, "utf8");
7944
+ const pid = JSON.parse(raw).pid;
7945
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
7946
+ return false;
7947
+ }
7948
+ try {
7949
+ process3.kill(pid, 0);
7950
+ return false;
7951
+ } catch (error) {
7952
+ if (fileSystemErrorCode(error) !== "ESRCH") {
7953
+ return false;
7954
+ }
7955
+ }
7956
+ const after = await lstat2(lockPath);
7957
+ if (after.ino !== before.ino || after.dev !== before.dev) {
7958
+ return false;
7959
+ }
7960
+ await rename2(lockPath, `${lockPath}.stale-${randomUUID2()}`);
7961
+ return true;
7962
+ } catch {
7963
+ return false;
7964
+ }
7965
+ }
7815
7966
  async function readProfileLock(lockPath) {
7816
7967
  let raw;
7817
7968
  try {
@@ -8029,6 +8180,7 @@ async function main(args = process4.argv.slice(2)) {
8029
8180
  if (!harness) {
8030
8181
  throw new AppError("CONFIG_INVALID", "The install command requires a harness target.");
8031
8182
  }
8183
+ planHarnessInstall(harness, { homeDirectory: homedir5(), environment: process4.env });
8032
8184
  const wizardChoices = await runWizard2(harness, { yes, stdin: process4.stdin, stdout: process4.stdout, homeDir: homedir5(), env: process4.env, version: SERVER_VERSION });
8033
8185
  await persistWizardConfig2(wizardChoices, homedir5());
8034
8186
  const installMessage = await installHarness(harness);