smooth-operator-mcp 2.1.1 → 2.2.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.
@@ -882,6 +882,99 @@ var init_installer = __esm({
882
882
  }
883
883
  });
884
884
 
885
+ // src/server/ui.ts
886
+ var ui_exports = {};
887
+ __export(ui_exports, {
888
+ createUi: () => createUi
889
+ });
890
+ function colorEnabled(stdout) {
891
+ if (!stdout?.isTTY) return false;
892
+ if (process.env.NO_COLOR) return false;
893
+ const term = process.env.TERM ?? "";
894
+ return term !== "dumb";
895
+ }
896
+ function createUi(stdout) {
897
+ const enabled = colorEnabled(stdout);
898
+ const write = typeof stdout?.write === "function" ? stdout.write.bind(stdout) : (() => true);
899
+ const paint = (code, text) => enabled ? `${code}${text}${RESET}` : text;
900
+ return {
901
+ colors: enabled,
902
+ bold: (text) => paint(BOLD, text),
903
+ dim: (text) => paint(DIM, text),
904
+ cyan: (text) => paint(CYAN, text),
905
+ green: (text) => paint(GREEN, text),
906
+ yellow: (text) => paint(YELLOW, text),
907
+ red: (text) => paint(RED, text),
908
+ /** Application banner shown once at the top of the wizard. */
909
+ banner(name, tagline, version = "") {
910
+ const line = "\u2500".repeat(Math.max(name.length + tagline.length + 8, 44));
911
+ write(`
912
+ ${paint(CYAN, line)}
913
+ `);
914
+ write(` ${paint(BOLD, name)}${version ? ` ${paint(DIM, `v${version}`)}` : ""}
915
+ `);
916
+ write(` ${paint(CYAN, tagline)}
917
+ `);
918
+ write(`${paint(CYAN, line)}
919
+
920
+ `);
921
+ },
922
+ /** Numbered step header, e.g. "── [2/6] Headless mode ──". */
923
+ step(current, total, title) {
924
+ write(`
925
+ ${paint(BOLD, `[${current}/${total}] ${title}`)}
926
+ `);
927
+ },
928
+ /** Indented explanatory paragraph under a question. */
929
+ explain(lines) {
930
+ for (const line of lines) {
931
+ write(` ${paint(DIM, line)}
932
+ `);
933
+ }
934
+ },
935
+ /** One option row in a numbered choice list. */
936
+ option(index, label, description, recommended = false) {
937
+ const badge = recommended ? paint(GREEN, " (recommended)") : "";
938
+ write(` ${paint(CYAN, `${index})`)} ${paint(BOLD, label)}${badge}
939
+ `);
940
+ write(` ${paint(DIM, description)}
941
+ `);
942
+ },
943
+ keyValues(rows) {
944
+ const width = Math.max(...rows.map(([key]) => key.length));
945
+ for (const [key, value] of rows) {
946
+ write(` ${paint(DIM, key.padEnd(width))} ${value}
947
+ `);
948
+ }
949
+ },
950
+ success(text) {
951
+ write(`${paint(GREEN, "\u2714")} ${text}
952
+ `);
953
+ },
954
+ failure(text) {
955
+ write(`${paint(RED, "\u2716")} ${text}
956
+ `);
957
+ },
958
+ note(text) {
959
+ write(` ${paint(YELLOW, "\u203A")} ${paint(DIM, text)}
960
+ `);
961
+ }
962
+ };
963
+ }
964
+ var RESET, BOLD, DIM, CYAN, GREEN, YELLOW, RED;
965
+ var init_ui = __esm({
966
+ "src/server/ui.ts"() {
967
+ "use strict";
968
+ RESET = "\x1B[0m";
969
+ BOLD = "\x1B[1m";
970
+ DIM = "\x1B[2m";
971
+ CYAN = "\x1B[36m";
972
+ GREEN = "\x1B[32m";
973
+ YELLOW = "\x1B[33m";
974
+ RED = "\x1B[31m";
975
+ }
976
+ });
977
+
885
978
  // src/server/installer-wizard.ts
886
979
  var installer_wizard_exports = {};
887
980
  __export(installer_wizard_exports, {
@@ -904,26 +997,43 @@ function isInteractive() {
904
997
  }
905
998
  async function promptForHarness(opts) {
906
999
  const { createInterface } = await import("node:readline/promises");
907
- const rl = createInterface({
908
- input: opts.stdin,
909
- output: opts.stdout
910
- });
1000
+ const ui = createUi(opts.stdout);
1001
+ const input = opts.stdin;
1002
+ const output = opts.stdout;
1003
+ const rl = createInterface({ input, output });
911
1004
  try {
912
- const answer = await rl.question(`Harness [opencode/claude-code/copilot/codex/gemini/vscode/cursor/windsurf/claude-desktop] [opencode]: `);
913
- const trimmed = answer.trim().toLowerCase();
914
- if (!trimmed) return "opencode";
915
- const map = {
916
- "claude": "claude-code",
917
- "github-copilot": "copilot",
918
- "codex-cli": "codex",
919
- "gemini-cli": "gemini",
920
- "vs-code": "vscode"
921
- };
922
- return map[trimmed] ?? trimmed;
1005
+ if (opts.stdout.isTTY) {
1006
+ ui.step(0, HARNESS_MENU.length, "Which harness should get a browser?");
1007
+ ui.explain(["The AI harness you use every day. Pick yours; you can re-run this later for others."]);
1008
+ HARNESS_MENU.forEach((entry, index) => ui.option(index + 1, entry.label, entry.description));
1009
+ }
1010
+ while (true) {
1011
+ const answer = await rl.question(`Choose 1-${HARNESS_MENU.length} or type a name [opencode]: `);
1012
+ const trimmed = answer.trim().toLowerCase();
1013
+ if (!trimmed) return "opencode";
1014
+ const numeric = Number.parseInt(trimmed, 10);
1015
+ if (`${numeric}` === trimmed && numeric >= 1 && numeric <= HARNESS_MENU.length) {
1016
+ return HARNESS_MENU[numeric - 1].id;
1017
+ }
1018
+ if (/^\d+$/.test(trimmed)) {
1019
+ continue;
1020
+ }
1021
+ return normalizeHarnessName(trimmed);
1022
+ }
923
1023
  } finally {
924
1024
  rl.close();
925
1025
  }
926
1026
  }
1027
+ function normalizeHarnessName(name) {
1028
+ const aliases = {
1029
+ "claude": "claude-code",
1030
+ "github-copilot": "copilot",
1031
+ "codex-cli": "codex",
1032
+ "gemini-cli": "gemini",
1033
+ "vs-code": "vscode"
1034
+ };
1035
+ return aliases[name] ?? name;
1036
+ }
927
1037
  function recommendedDefaults(homeDir) {
928
1038
  return {
929
1039
  mode: "managed",
@@ -934,52 +1044,188 @@ function recommendedDefaults(homeDir) {
934
1044
  dataDir: join7(homeDir ?? homedir4(), ".smooth-operator")
935
1045
  };
936
1046
  }
937
- async function runWizard(_harness, opts) {
1047
+ function tolerantQuestion(rl) {
1048
+ return {
1049
+ async question(prompt) {
1050
+ try {
1051
+ return await rl.question(prompt);
1052
+ } catch (error) {
1053
+ const code = error.code;
1054
+ const aborted = error instanceof Error && error.name === "AbortError";
1055
+ if (aborted || code === "ABORT_ERR" || code === "ERR_USE_AFTER_CLOSE") {
1056
+ return "";
1057
+ }
1058
+ throw error;
1059
+ }
1060
+ }
1061
+ };
1062
+ }
1063
+ async function askYesNo(session, prompt, fallback) {
1064
+ while (true) {
1065
+ const hint = fallback ? "[Y/n]" : "[y/N]";
1066
+ const answer = (await session.question(`${prompt} ${hint}: `)).trim().toLowerCase();
1067
+ if (!answer) return fallback;
1068
+ if (["y", "yes"].includes(answer)) return true;
1069
+ if (["n", "no"].includes(answer)) return false;
1070
+ }
1071
+ }
1072
+ function parseDomainList(raw) {
1073
+ const domains = raw.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
1074
+ const invalid = domains.find((domain) => !/^(?:\*\.)?[a-z0-9-]+(?:\.[a-z0-9-]+)+$/.test(domain));
1075
+ return invalid === void 0 ? domains : void 0;
1076
+ }
1077
+ async function runWizard(harness, opts) {
1078
+ const defaults = recommendedDefaults(opts.homeDir);
938
1079
  if (opts.yes) {
939
- return recommendedDefaults(opts.homeDir);
1080
+ return defaults;
940
1081
  }
941
1082
  const stdin = opts.stdin ?? process.stdin;
942
1083
  const stdout = opts.stdout ?? process.stdout;
943
1084
  const interactive = Boolean(stdin.isTTY && stdout.isTTY && !process.env.CI);
944
1085
  if (!interactive) {
945
- return recommendedDefaults(opts.homeDir);
1086
+ return defaults;
946
1087
  }
1088
+ const ui = createUi(stdout);
947
1089
  const { createInterface } = await import("node:readline/promises");
948
1090
  const rl = createInterface({
949
1091
  input: stdin,
950
1092
  output: stdout
951
1093
  });
1094
+ const session = tolerantQuestion(rl);
952
1095
  try {
953
- const modeAns = await rl.question("Browser mode: 1) managed private (recommended) 2) personal Chrome (connect) 3) disabled [1]: ");
954
- const modeMap = { "1": "managed", "2": "connect", "3": "disabled", "": "managed" };
955
- let mode = modeMap[modeAns.trim()] ?? "managed";
956
- if (!["managed", "connect", "disabled"].includes(mode)) mode = "managed";
1096
+ ui.banner("SmoothOperator Setup", `Give ${harness} a real Chrome it can drive`, opts.version ?? "2.2.0");
1097
+ ui.note(`Configuring: ${harness}`);
1098
+ ui.note("Answer each question, or press Enter to accept the recommended default.");
1099
+ ui.note(`You can re-run \`smooth-operator install ${harness}\` at any time to change these.`);
1100
+ ui.step(1, 6, "Browser mode");
1101
+ ui.explain([
1102
+ "Who owns the Chrome window your AI drives?",
1103
+ "",
1104
+ "Managed gives the AI its own private Chrome profile at ~/.smooth-operator/browser.",
1105
+ "Your daily browser stays untouched; logins for the AI live separately.",
1106
+ "",
1107
+ "Connect attaches to your real Chrome instead, so the AI uses everything",
1108
+ "you are already signed into. Only pick this if you need your existing logins.",
1109
+ "",
1110
+ "Disabled keeps the server but turns all browsing tools off."
1111
+ ]);
1112
+ ui.option(1, "Managed private Chrome", "Isolated profile owned by SmoothOperator. Safest default.", true);
1113
+ ui.option(2, "Personal Chrome (connect)", "Reuse your real browser and its existing sign-ins.");
1114
+ ui.option(3, "Disabled", "No browser. Tools that need a page will report an error.");
1115
+ let mode = "managed";
957
1116
  let browserUrl;
958
- if (mode === "connect") {
959
- browserUrl = "http://127.0.0.1:9222";
960
- }
961
- const headlessAns = await rl.question("Headless Chrome? (y/N) [N]: ");
962
- const headless = headlessAns.trim().toLowerCase() === "y";
963
- const allowedAns = await rl.question("Allowed domains (comma-separated, empty=all) []: ");
964
- const allowedDomains = allowedAns.trim() ? allowedAns.split(",").map((s) => s.trim()).filter(Boolean) : [];
965
- const blockedAns = await rl.question("Blocked domains (comma-separated) []: ");
966
- const blockedDomains = blockedAns.trim() ? blockedAns.split(",").map((s) => s.trim()).filter(Boolean) : [];
967
- const evalAns = await rl.question("Allow page JavaScript (browser_evaluate)? (y/N) [N]: ");
968
- const allowEval = evalAns.trim().toLowerCase() === "y";
969
- const dataDirAns = await rl.question(`Data directory [${join7(opts.homeDir ?? homedir4(), ".smooth-operator")}]: `);
970
- const dataDir = dataDirAns.trim() ? dataDirAns.trim() : join7(opts.homeDir ?? homedir4(), ".smooth-operator");
971
- if (mode === "connect") {
972
- try {
973
- const launched = await launchPersonalChrome({ dataDir, spawn: opts.spawn, probe: opts.probe ?? defaultProbe, port: 9222 });
974
- browserUrl = launched.url;
975
- } catch {
1117
+ let headless = false;
1118
+ let allowedDomains = [];
1119
+ let blockedDomains = [];
1120
+ let allowEval = false;
1121
+ let dataDir = defaults.dataDir;
1122
+ while (true) {
1123
+ const answer = (await session.question("Mode [1]: ")).trim();
1124
+ if (!answer || answer === "1") {
1125
+ mode = "managed";
1126
+ break;
1127
+ }
1128
+ if (answer === "2") {
1129
+ mode = "connect";
1130
+ browserUrl = "http://127.0.0.1:9222";
1131
+ break;
1132
+ }
1133
+ if (answer === "3") {
1134
+ mode = "disabled";
1135
+ break;
1136
+ }
1137
+ ui.failure("Enter 1, 2, or 3.");
1138
+ }
1139
+ let headlessChoice = false;
1140
+ if (mode !== "disabled") {
1141
+ ui.step(2, 6, "Headless mode");
1142
+ ui.explain([
1143
+ "Headless runs Chrome with no visible window - lighter and invisible.",
1144
+ "Visible Chrome lets you watch clicks happen and handle CAPTCHAs or",
1145
+ "logins yourself when a site pauses for human verification."
1146
+ ]);
1147
+ headlessChoice = await askYesNo(session, "Run Chrome headless (no window)?", false);
1148
+ ui.step(3, 6, "Allowed domains");
1149
+ ui.explain([
1150
+ "Restrict which sites the AI may open, e.g. docs.example.com, *.wikipedia.org",
1151
+ "Leave empty to allow every site. Blocked domains always win over allowed ones."
1152
+ ]);
1153
+ while (true) {
1154
+ const parsed = parseDomainList(await session.question("Allowed domains (comma-separated, Enter for all): "));
1155
+ if (parsed !== void 0) {
1156
+ allowedDomains = parsed;
1157
+ break;
1158
+ }
1159
+ ui.failure("That did not look like a domain list. Example: example.com, *.shop.test");
1160
+ }
1161
+ ui.step(4, 6, "Blocked domains");
1162
+ ui.explain(["Never open these sites, even when everything else is allowed."]);
1163
+ while (true) {
1164
+ const parsed = parseDomainList(await session.question("Blocked domains (comma-separated, Enter for none): "));
1165
+ if (parsed !== void 0) {
1166
+ blockedDomains = parsed;
1167
+ break;
1168
+ }
1169
+ ui.failure("That did not look like a domain list. Example: ads.example.com");
1170
+ }
1171
+ ui.step(5, 6, "JavaScript execution");
1172
+ ui.explain([
1173
+ "browser_evaluate runs arbitrary JavaScript on a page - powerful for scraping",
1174
+ "but it can also trigger bot defenses. Most users never need it on."
1175
+ ]);
1176
+ allowEval = await askYesNo(session, "Allow the AI to run JavaScript on pages?", false);
1177
+ ui.step(6, 6, "Data directory");
1178
+ ui.explain([
1179
+ "Where the private Chrome profile, logs, and downloads live.",
1180
+ "Permissions are locked to 0600 so only your user can read them."
1181
+ ]);
1182
+ while (dataDir === defaults.dataDir) {
1183
+ const answer = (await session.question(`Data directory [${defaults.dataDir}]: `)).trim();
1184
+ if (!answer) break;
1185
+ if (!answer.startsWith("/")) {
1186
+ ui.failure("Enter an absolute path (starting with /).");
1187
+ continue;
1188
+ }
1189
+ dataDir = answer;
1190
+ break;
1191
+ }
1192
+ headless = headlessChoice;
1193
+ if (mode === "connect") {
1194
+ ui.note("Starting your personal Chrome with remote debugging on port 9222...");
1195
+ try {
1196
+ const launched = await launchPersonalChrome({ dataDir, spawn: opts.spawn, probe: opts.probe ?? defaultProbe, port: 9222 });
1197
+ browserUrl = launched.url;
1198
+ ui.success(`Connected to your Chrome at ${launched.url}`);
1199
+ } catch {
1200
+ browserUrl = "http://127.0.0.1:9222";
1201
+ ui.note("Could not reach Chrome on port 9222 yet - keeping the default URL.");
1202
+ }
976
1203
  }
977
1204
  }
1205
+ writeSummary(ui, harness, { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir });
978
1206
  return { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir, browserUrl };
979
1207
  } finally {
980
1208
  rl.close();
981
1209
  }
982
1210
  }
1211
+ function writeSummary(ui, harness, choices) {
1212
+ const modeLabel = {
1213
+ managed: "Managed private Chrome (isolated profile)",
1214
+ connect: "Your personal Chrome via debugging port",
1215
+ disabled: "Disabled - no browser tools"
1216
+ };
1217
+ ui.banner("Configuration Summary", `Ready to configure ${harness}`, "");
1218
+ ui.keyValues([
1219
+ ["Browser mode", choices.mode === "connect" ? modeLabel.connect : modeLabel[choices.mode] ?? choices.mode],
1220
+ ["Headless", choices.headless ? "yes - no visible window" : "no - you can watch and intervene"],
1221
+ ...choices.mode === "disabled" ? [] : [
1222
+ ["Allowed sites", choices.allowedDomains.length ? choices.allowedDomains.join(", ") : "all sites"],
1223
+ ["Blocked sites", choices.blockedDomains.length ? choices.blockedDomains.join(", ") : "none"],
1224
+ ["Page JavaScript", choices.allowEval ? "enabled" : "off (recommended)"],
1225
+ ["Data directory", choices.dataDir]
1226
+ ]
1227
+ ]);
1228
+ }
983
1229
  async function defaultProbe(url, timeoutMs) {
984
1230
  try {
985
1231
  const controller = new AbortController();
@@ -1094,12 +1340,24 @@ async function launchPersonalChrome(opts) {
1094
1340
  }
1095
1341
  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.`);
1096
1342
  }
1097
- var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS;
1343
+ var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, HARNESS_MENU;
1098
1344
  var init_installer_wizard = __esm({
1099
1345
  "src/server/installer-wizard.ts"() {
1100
1346
  "use strict";
1347
+ init_ui();
1101
1348
  PROBE_INTERVAL_MS = 300;
1102
1349
  DEFAULT_PROBE_ATTEMPTS = 33;
1350
+ HARNESS_MENU = [
1351
+ { id: "opencode", label: "OpenCode", description: "Configures ~/.config/opencode/opencode.json" },
1352
+ { id: "claude-code", label: "Claude Code", description: "Runs `claude mcp add` for your user scope" },
1353
+ { id: "copilot", label: "GitHub Copilot CLI", description: "Runs `copilot mcp add`" },
1354
+ { id: "codex", label: "OpenAI Codex CLI", description: "Runs `codex mcp add`" },
1355
+ { id: "gemini", label: "Gemini CLI", description: "Runs `gemini mcp add` for your user scope" },
1356
+ { id: "vscode", label: "VS Code", description: "Runs `code --add-mcp`" },
1357
+ { id: "cursor", label: "Cursor", description: "Adds SmoothOperator to ~/.cursor/mcp.json" },
1358
+ { id: "windsurf", label: "Windsurf", description: "Adds SmoothOperator to Windsurf's mcp_config.json" },
1359
+ { id: "claude-desktop", label: "Claude Desktop", description: "Updates claude_desktop_config.json" }
1360
+ ];
1103
1361
  }
1104
1362
  });
1105
1363
 
@@ -1972,7 +2230,7 @@ init_errors();
1972
2230
  init_logger();
1973
2231
 
1974
2232
  // src/server/version.ts
1975
- var SERVER_VERSION = "2.1.1";
2233
+ var SERVER_VERSION = "2.2.0";
1976
2234
 
1977
2235
  // src/server/mcp.ts
1978
2236
  var EmptyInputSchema = z3.object({}).strict();
@@ -7771,10 +8029,26 @@ async function main(args = process4.argv.slice(2)) {
7771
8029
  if (!harness) {
7772
8030
  throw new AppError("CONFIG_INVALID", "The install command requires a harness target.");
7773
8031
  }
7774
- const wizardChoices = await runWizard2(harness, { yes, stdin: process4.stdin, stdout: process4.stdout, homeDir: homedir5(), env: process4.env });
8032
+ const wizardChoices = await runWizard2(harness, { yes, stdin: process4.stdin, stdout: process4.stdout, homeDir: homedir5(), env: process4.env, version: SERVER_VERSION });
7775
8033
  await persistWizardConfig2(wizardChoices, homedir5());
7776
- process4.stdout.write(`${await installHarness(harness)}
8034
+ const installMessage = await installHarness(harness);
8035
+ const { createUi: createUi2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
8036
+ const ui = createUi2(process4.stdout);
8037
+ if (process4.stdout.isTTY) {
8038
+ ui.banner("Installation Complete", `${harness} can now drive a browser`, SERVER_VERSION);
8039
+ ui.keyValues([
8040
+ ["Config file", `${homedir5()}/.smooth-operator/config.json`],
8041
+ ["Browser mode", wizardChoices.mode]
8042
+ ]);
8043
+ process4.stdout.write("\n");
8044
+ ui.step(0, 2, "Next steps");
8045
+ ui.option(1, "Restart the harness", "Quit and reopen it so it picks up the new MCP server.");
8046
+ ui.option(2, "Verify", "Ask your AI to run server_health and browser_doctor.");
8047
+ ui.success(installMessage);
8048
+ } else {
8049
+ process4.stdout.write(`${installMessage}
7777
8050
  `);
8051
+ }
7778
8052
  return;
7779
8053
  }
7780
8054
  if (args.length === 1 && (args[0] === "--version" || args[0] === "-V")) {