smooth-operator-mcp 2.1.1 → 2.2.1
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/dist/smooth-operator.mjs +414 -57
- package/dist/smooth-operator.mjs.map +3 -3
- package/package.json +1 -1
package/dist/smooth-operator.mjs
CHANGED
|
@@ -366,8 +366,8 @@ __export(installer_exports, {
|
|
|
366
366
|
planHarnessInstall: () => planHarnessInstall,
|
|
367
367
|
supportedHarnessTargets: () => supportedHarnessTargets
|
|
368
368
|
});
|
|
369
|
-
import { constants as constants2 } from "node:fs";
|
|
370
|
-
import { chmod as chmod2, lstat as lstat3, mkdir as mkdir3, open as open2, rename as
|
|
369
|
+
import { constants as constants2, accessSync, existsSync } from "node:fs";
|
|
370
|
+
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
371
|
import { execFile } from "node:child_process";
|
|
372
372
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
373
373
|
import { homedir as homedir3, platform as platform2 } from "node:os";
|
|
@@ -394,7 +394,7 @@ function planHarnessInstall(target, options = {}) {
|
|
|
394
394
|
return { kind: "cli", target: normalized, command: "codex", args: ["mcp", "add", SERVER_NAME, "--", cliEntry.command, ...cliEntry.args] };
|
|
395
395
|
}
|
|
396
396
|
if (normalized === "gemini") {
|
|
397
|
-
return { kind: "cli", target: normalized, command: "gemini", args: ["mcp", "add", SERVER_NAME, cliEntry.command, ...cliEntry.args
|
|
397
|
+
return { kind: "cli", target: normalized, command: "gemini", args: ["mcp", "add", "--scope", "user", SERVER_NAME, cliEntry.command, ...cliEntry.args] };
|
|
398
398
|
}
|
|
399
399
|
if (normalized === "vscode") {
|
|
400
400
|
return {
|
|
@@ -444,10 +444,21 @@ function normalizeTarget(target) {
|
|
|
444
444
|
function resolveServerEntry() {
|
|
445
445
|
const modulePath = fileURLToPath(import.meta.url);
|
|
446
446
|
if (basename4(modulePath) === "smooth-operator.mjs") {
|
|
447
|
-
return { command:
|
|
447
|
+
return { command: resolveStableNodeExecutable(), args: [modulePath] };
|
|
448
448
|
}
|
|
449
449
|
return { command: "smooth-operator", args: [] };
|
|
450
450
|
}
|
|
451
|
+
function resolveStableNodeExecutable() {
|
|
452
|
+
const candidates = platform2() === "darwin" ? ["/opt/homebrew/bin/node", "/usr/local/bin/node"] : ["/usr/local/bin/node", "/usr/bin/node"];
|
|
453
|
+
for (const candidate of candidates) {
|
|
454
|
+
try {
|
|
455
|
+
accessSync(candidate, constants2.X_OK);
|
|
456
|
+
return candidate;
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return process.execPath;
|
|
461
|
+
}
|
|
451
462
|
async function runCliInstall(plan, executeCommand) {
|
|
452
463
|
const runner = executeCommand ?? (async (command, args) => {
|
|
453
464
|
await execFileAsync(command, [...args], {
|
|
@@ -556,7 +567,7 @@ async function installJsonConfig(target, plannedPath, options, allowOpenCodeJson
|
|
|
556
567
|
`, { mode: 384, flag: "wx" });
|
|
557
568
|
await chmod2(tempPath, 384);
|
|
558
569
|
await rejectSymlink2(path, "configuration file");
|
|
559
|
-
await
|
|
570
|
+
await rename3(tempPath, path);
|
|
560
571
|
return `Installed SmoothOperator in ${path}${backupPath ? ` (backup: ${backupPath})` : ""}. Restart the harness.`;
|
|
561
572
|
} catch (error) {
|
|
562
573
|
await unlink3(tempPath).catch(() => void 0);
|
|
@@ -614,6 +625,26 @@ async function chooseExistingOpenCodePath(plannedPath) {
|
|
|
614
625
|
}
|
|
615
626
|
return plannedPath;
|
|
616
627
|
}
|
|
628
|
+
function isStaleEmbeddedCommand(command) {
|
|
629
|
+
if (typeof command !== "string" || !command.includes("/")) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return !existsSync(command);
|
|
633
|
+
}
|
|
634
|
+
function isStaleEmbeddedCommandArray(command) {
|
|
635
|
+
if (!Array.isArray(command) || command.length === 0) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const interpreter = command[0];
|
|
639
|
+
return typeof interpreter === "string" && interpreter.includes("/") && !existsSync(interpreter);
|
|
640
|
+
}
|
|
641
|
+
function isRepairableStdioEntry(existing, entry) {
|
|
642
|
+
return isStaleEmbeddedCommand(existing.command) && Array.isArray(existing.args) && existing.args.length === entry.args.length && existing.args.every((arg, index) => arg === entry.args[index]);
|
|
643
|
+
}
|
|
644
|
+
function isRepairableOpenCodeEntry(existing, desired) {
|
|
645
|
+
const repaired = { ...existing, command: desired.command };
|
|
646
|
+
return isStaleEmbeddedCommandArray(existing.command) && sameOpenCodeEntry(repaired, desired);
|
|
647
|
+
}
|
|
617
648
|
function mergeMcpServersConfig(config, entry, path) {
|
|
618
649
|
if (config.mcpServers !== void 0 && !isRecord3(config.mcpServers)) {
|
|
619
650
|
throw new AppError("INSTALL_CONFIG_INVALID", `The mcpServers value in ${path} must be an object; refusing to replace it.`);
|
|
@@ -621,9 +652,16 @@ function mergeMcpServersConfig(config, entry, path) {
|
|
|
621
652
|
const servers = isRecord3(config.mcpServers) ? { ...config.mcpServers } : {};
|
|
622
653
|
const existing = servers[SERVER_NAME];
|
|
623
654
|
if (existing !== void 0) {
|
|
624
|
-
if (!isRecord3(existing)
|
|
655
|
+
if (!isRecord3(existing)) {
|
|
625
656
|
throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
|
|
626
657
|
}
|
|
658
|
+
if (!sameStdioEntry(existing, entry)) {
|
|
659
|
+
if (!isRepairableStdioEntry(existing, entry)) {
|
|
660
|
+
throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
|
|
661
|
+
}
|
|
662
|
+
servers[SERVER_NAME] = { command: entry.command, args: [...entry.args] };
|
|
663
|
+
return { config: { ...config, mcpServers: servers }, alreadyConfigured: false };
|
|
664
|
+
}
|
|
627
665
|
return { config, alreadyConfigured: true };
|
|
628
666
|
}
|
|
629
667
|
servers[SERVER_NAME] = { command: entry.command, args: [...entry.args] };
|
|
@@ -645,10 +683,24 @@ function mergeOpenCodeConfig(config, entry, path) {
|
|
|
645
683
|
const existing = servers[SERVER_NAME];
|
|
646
684
|
const desired = modernSchema ? { type: "local", command: [entry.command, ...entry.args] } : { type: "local", command: [entry.command, ...entry.args], enabled: true };
|
|
647
685
|
if (existing !== void 0) {
|
|
648
|
-
if (!isRecord3(existing)
|
|
686
|
+
if (!isRecord3(existing)) {
|
|
649
687
|
throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
|
|
650
688
|
}
|
|
651
|
-
|
|
689
|
+
if (!sameOpenCodeEntry(existing, desired)) {
|
|
690
|
+
if (!isRepairableOpenCodeEntry(existing, desired)) {
|
|
691
|
+
throw new AppError("INSTALL_CONFIG_CONFLICT", `The '${SERVER_NAME}' server in ${path} has a conflicting configuration; refusing to overwrite it.`);
|
|
692
|
+
}
|
|
693
|
+
existing.command = desired.command;
|
|
694
|
+
} else {
|
|
695
|
+
return { config, alreadyConfigured: true };
|
|
696
|
+
}
|
|
697
|
+
servers[SERVER_NAME] = existing;
|
|
698
|
+
if (modernSchema) {
|
|
699
|
+
mcp.servers = servers;
|
|
700
|
+
} else {
|
|
701
|
+
Object.assign(mcp, servers);
|
|
702
|
+
}
|
|
703
|
+
return { config: { ...config, mcp }, alreadyConfigured: false };
|
|
652
704
|
}
|
|
653
705
|
servers[SERVER_NAME] = desired;
|
|
654
706
|
if (modernSchema) {
|
|
@@ -882,6 +934,99 @@ var init_installer = __esm({
|
|
|
882
934
|
}
|
|
883
935
|
});
|
|
884
936
|
|
|
937
|
+
// src/server/ui.ts
|
|
938
|
+
var ui_exports = {};
|
|
939
|
+
__export(ui_exports, {
|
|
940
|
+
createUi: () => createUi
|
|
941
|
+
});
|
|
942
|
+
function colorEnabled(stdout) {
|
|
943
|
+
if (!stdout?.isTTY) return false;
|
|
944
|
+
if (process.env.NO_COLOR) return false;
|
|
945
|
+
const term = process.env.TERM ?? "";
|
|
946
|
+
return term !== "dumb";
|
|
947
|
+
}
|
|
948
|
+
function createUi(stdout) {
|
|
949
|
+
const enabled = colorEnabled(stdout);
|
|
950
|
+
const write = typeof stdout?.write === "function" ? stdout.write.bind(stdout) : (() => true);
|
|
951
|
+
const paint = (code, text) => enabled ? `${code}${text}${RESET}` : text;
|
|
952
|
+
return {
|
|
953
|
+
colors: enabled,
|
|
954
|
+
bold: (text) => paint(BOLD, text),
|
|
955
|
+
dim: (text) => paint(DIM, text),
|
|
956
|
+
cyan: (text) => paint(CYAN, text),
|
|
957
|
+
green: (text) => paint(GREEN, text),
|
|
958
|
+
yellow: (text) => paint(YELLOW, text),
|
|
959
|
+
red: (text) => paint(RED, text),
|
|
960
|
+
/** Application banner shown once at the top of the wizard. */
|
|
961
|
+
banner(name, tagline, version = "") {
|
|
962
|
+
const line = "\u2500".repeat(Math.max(name.length + tagline.length + 8, 44));
|
|
963
|
+
write(`
|
|
964
|
+
${paint(CYAN, line)}
|
|
965
|
+
`);
|
|
966
|
+
write(` ${paint(BOLD, name)}${version ? ` ${paint(DIM, `v${version}`)}` : ""}
|
|
967
|
+
`);
|
|
968
|
+
write(` ${paint(CYAN, tagline)}
|
|
969
|
+
`);
|
|
970
|
+
write(`${paint(CYAN, line)}
|
|
971
|
+
|
|
972
|
+
`);
|
|
973
|
+
},
|
|
974
|
+
/** Numbered step header, e.g. "── [2/6] Headless mode ──". */
|
|
975
|
+
step(current, total, title) {
|
|
976
|
+
write(`
|
|
977
|
+
${paint(BOLD, `[${current}/${total}] ${title}`)}
|
|
978
|
+
`);
|
|
979
|
+
},
|
|
980
|
+
/** Indented explanatory paragraph under a question. */
|
|
981
|
+
explain(lines) {
|
|
982
|
+
for (const line of lines) {
|
|
983
|
+
write(` ${paint(DIM, line)}
|
|
984
|
+
`);
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
/** One option row in a numbered choice list. */
|
|
988
|
+
option(index, label, description, recommended = false) {
|
|
989
|
+
const badge = recommended ? paint(GREEN, " (recommended)") : "";
|
|
990
|
+
write(` ${paint(CYAN, `${index})`)} ${paint(BOLD, label)}${badge}
|
|
991
|
+
`);
|
|
992
|
+
write(` ${paint(DIM, description)}
|
|
993
|
+
`);
|
|
994
|
+
},
|
|
995
|
+
keyValues(rows) {
|
|
996
|
+
const width = Math.max(...rows.map(([key]) => key.length));
|
|
997
|
+
for (const [key, value] of rows) {
|
|
998
|
+
write(` ${paint(DIM, key.padEnd(width))} ${value}
|
|
999
|
+
`);
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
success(text) {
|
|
1003
|
+
write(`${paint(GREEN, "\u2714")} ${text}
|
|
1004
|
+
`);
|
|
1005
|
+
},
|
|
1006
|
+
failure(text) {
|
|
1007
|
+
write(`${paint(RED, "\u2716")} ${text}
|
|
1008
|
+
`);
|
|
1009
|
+
},
|
|
1010
|
+
note(text) {
|
|
1011
|
+
write(` ${paint(YELLOW, "\u203A")} ${paint(DIM, text)}
|
|
1012
|
+
`);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
var RESET, BOLD, DIM, CYAN, GREEN, YELLOW, RED;
|
|
1017
|
+
var init_ui = __esm({
|
|
1018
|
+
"src/server/ui.ts"() {
|
|
1019
|
+
"use strict";
|
|
1020
|
+
RESET = "\x1B[0m";
|
|
1021
|
+
BOLD = "\x1B[1m";
|
|
1022
|
+
DIM = "\x1B[2m";
|
|
1023
|
+
CYAN = "\x1B[36m";
|
|
1024
|
+
GREEN = "\x1B[32m";
|
|
1025
|
+
YELLOW = "\x1B[33m";
|
|
1026
|
+
RED = "\x1B[31m";
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
885
1030
|
// src/server/installer-wizard.ts
|
|
886
1031
|
var installer_wizard_exports = {};
|
|
887
1032
|
__export(installer_wizard_exports, {
|
|
@@ -904,26 +1049,43 @@ function isInteractive() {
|
|
|
904
1049
|
}
|
|
905
1050
|
async function promptForHarness(opts) {
|
|
906
1051
|
const { createInterface } = await import("node:readline/promises");
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
});
|
|
1052
|
+
const ui = createUi(opts.stdout);
|
|
1053
|
+
const input = opts.stdin;
|
|
1054
|
+
const output = opts.stdout;
|
|
1055
|
+
const rl = createInterface({ input, output });
|
|
911
1056
|
try {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1057
|
+
if (opts.stdout.isTTY) {
|
|
1058
|
+
ui.step(0, HARNESS_MENU.length, "Which harness should get a browser?");
|
|
1059
|
+
ui.explain(["The AI harness you use every day. Pick yours; you can re-run this later for others."]);
|
|
1060
|
+
HARNESS_MENU.forEach((entry, index) => ui.option(index + 1, entry.label, entry.description));
|
|
1061
|
+
}
|
|
1062
|
+
while (true) {
|
|
1063
|
+
const answer = await rl.question(`Choose 1-${HARNESS_MENU.length} or type a name [opencode]: `);
|
|
1064
|
+
const trimmed = answer.trim().toLowerCase();
|
|
1065
|
+
if (!trimmed) return "opencode";
|
|
1066
|
+
const numeric = Number.parseInt(trimmed, 10);
|
|
1067
|
+
if (`${numeric}` === trimmed && numeric >= 1 && numeric <= HARNESS_MENU.length) {
|
|
1068
|
+
return HARNESS_MENU[numeric - 1].id;
|
|
1069
|
+
}
|
|
1070
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
return normalizeHarnessName(trimmed);
|
|
1074
|
+
}
|
|
923
1075
|
} finally {
|
|
924
1076
|
rl.close();
|
|
925
1077
|
}
|
|
926
1078
|
}
|
|
1079
|
+
function normalizeHarnessName(name) {
|
|
1080
|
+
const aliases = {
|
|
1081
|
+
"claude": "claude-code",
|
|
1082
|
+
"github-copilot": "copilot",
|
|
1083
|
+
"codex-cli": "codex",
|
|
1084
|
+
"gemini-cli": "gemini",
|
|
1085
|
+
"vs-code": "vscode"
|
|
1086
|
+
};
|
|
1087
|
+
return aliases[name] ?? name;
|
|
1088
|
+
}
|
|
927
1089
|
function recommendedDefaults(homeDir) {
|
|
928
1090
|
return {
|
|
929
1091
|
mode: "managed",
|
|
@@ -934,52 +1096,188 @@ function recommendedDefaults(homeDir) {
|
|
|
934
1096
|
dataDir: join7(homeDir ?? homedir4(), ".smooth-operator")
|
|
935
1097
|
};
|
|
936
1098
|
}
|
|
937
|
-
|
|
1099
|
+
function tolerantQuestion(rl) {
|
|
1100
|
+
return {
|
|
1101
|
+
async question(prompt) {
|
|
1102
|
+
try {
|
|
1103
|
+
return await rl.question(prompt);
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
const code = error.code;
|
|
1106
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
1107
|
+
if (aborted || code === "ABORT_ERR" || code === "ERR_USE_AFTER_CLOSE") {
|
|
1108
|
+
return "";
|
|
1109
|
+
}
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
async function askYesNo(session, prompt, fallback) {
|
|
1116
|
+
while (true) {
|
|
1117
|
+
const hint = fallback ? "[Y/n]" : "[y/N]";
|
|
1118
|
+
const answer = (await session.question(`${prompt} ${hint}: `)).trim().toLowerCase();
|
|
1119
|
+
if (!answer) return fallback;
|
|
1120
|
+
if (["y", "yes"].includes(answer)) return true;
|
|
1121
|
+
if (["n", "no"].includes(answer)) return false;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function parseDomainList(raw) {
|
|
1125
|
+
const domains = raw.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
1126
|
+
const invalid = domains.find((domain) => !/^(?:\*\.)?[a-z0-9-]+(?:\.[a-z0-9-]+)+$/.test(domain));
|
|
1127
|
+
return invalid === void 0 ? domains : void 0;
|
|
1128
|
+
}
|
|
1129
|
+
async function runWizard(harness, opts) {
|
|
1130
|
+
const defaults = recommendedDefaults(opts.homeDir);
|
|
938
1131
|
if (opts.yes) {
|
|
939
|
-
return
|
|
1132
|
+
return defaults;
|
|
940
1133
|
}
|
|
941
1134
|
const stdin = opts.stdin ?? process.stdin;
|
|
942
1135
|
const stdout = opts.stdout ?? process.stdout;
|
|
943
1136
|
const interactive = Boolean(stdin.isTTY && stdout.isTTY && !process.env.CI);
|
|
944
1137
|
if (!interactive) {
|
|
945
|
-
return
|
|
1138
|
+
return defaults;
|
|
946
1139
|
}
|
|
1140
|
+
const ui = createUi(stdout);
|
|
947
1141
|
const { createInterface } = await import("node:readline/promises");
|
|
948
1142
|
const rl = createInterface({
|
|
949
1143
|
input: stdin,
|
|
950
1144
|
output: stdout
|
|
951
1145
|
});
|
|
1146
|
+
const session = tolerantQuestion(rl);
|
|
952
1147
|
try {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1148
|
+
ui.banner("SmoothOperator Setup", `Give ${harness} a real Chrome it can drive`, opts.version ?? "2.2.0");
|
|
1149
|
+
ui.note(`Configuring: ${harness}`);
|
|
1150
|
+
ui.note("Answer each question, or press Enter to accept the recommended default.");
|
|
1151
|
+
ui.note(`You can re-run \`smooth-operator install ${harness}\` at any time to change these.`);
|
|
1152
|
+
ui.step(1, 6, "Browser mode");
|
|
1153
|
+
ui.explain([
|
|
1154
|
+
"Who owns the Chrome window your AI drives?",
|
|
1155
|
+
"",
|
|
1156
|
+
"Managed gives the AI its own private Chrome profile at ~/.smooth-operator/browser.",
|
|
1157
|
+
"Your daily browser stays untouched; logins for the AI live separately.",
|
|
1158
|
+
"",
|
|
1159
|
+
"Connect attaches to your real Chrome instead, so the AI uses everything",
|
|
1160
|
+
"you are already signed into. Only pick this if you need your existing logins.",
|
|
1161
|
+
"",
|
|
1162
|
+
"Disabled keeps the server but turns all browsing tools off."
|
|
1163
|
+
]);
|
|
1164
|
+
ui.option(1, "Managed private Chrome", "Isolated profile owned by SmoothOperator. Safest default.", true);
|
|
1165
|
+
ui.option(2, "Personal Chrome (connect)", "Reuse your real browser and its existing sign-ins.");
|
|
1166
|
+
ui.option(3, "Disabled", "No browser. Tools that need a page will report an error.");
|
|
1167
|
+
let mode = "managed";
|
|
957
1168
|
let browserUrl;
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1169
|
+
let headless = false;
|
|
1170
|
+
let allowedDomains = [];
|
|
1171
|
+
let blockedDomains = [];
|
|
1172
|
+
let allowEval = false;
|
|
1173
|
+
let dataDir = defaults.dataDir;
|
|
1174
|
+
while (true) {
|
|
1175
|
+
const answer = (await session.question("Mode [1]: ")).trim();
|
|
1176
|
+
if (!answer || answer === "1") {
|
|
1177
|
+
mode = "managed";
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
if (answer === "2") {
|
|
1181
|
+
mode = "connect";
|
|
1182
|
+
browserUrl = "http://127.0.0.1:9222";
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
if (answer === "3") {
|
|
1186
|
+
mode = "disabled";
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
ui.failure("Enter 1, 2, or 3.");
|
|
1190
|
+
}
|
|
1191
|
+
let headlessChoice = false;
|
|
1192
|
+
if (mode !== "disabled") {
|
|
1193
|
+
ui.step(2, 6, "Headless mode");
|
|
1194
|
+
ui.explain([
|
|
1195
|
+
"Headless runs Chrome with no visible window - lighter and invisible.",
|
|
1196
|
+
"Visible Chrome lets you watch clicks happen and handle CAPTCHAs or",
|
|
1197
|
+
"logins yourself when a site pauses for human verification."
|
|
1198
|
+
]);
|
|
1199
|
+
headlessChoice = await askYesNo(session, "Run Chrome headless (no window)?", false);
|
|
1200
|
+
ui.step(3, 6, "Allowed domains");
|
|
1201
|
+
ui.explain([
|
|
1202
|
+
"Restrict which sites the AI may open, e.g. docs.example.com, *.wikipedia.org",
|
|
1203
|
+
"Leave empty to allow every site. Blocked domains always win over allowed ones."
|
|
1204
|
+
]);
|
|
1205
|
+
while (true) {
|
|
1206
|
+
const parsed = parseDomainList(await session.question("Allowed domains (comma-separated, Enter for all): "));
|
|
1207
|
+
if (parsed !== void 0) {
|
|
1208
|
+
allowedDomains = parsed;
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
ui.failure("That did not look like a domain list. Example: example.com, *.shop.test");
|
|
1212
|
+
}
|
|
1213
|
+
ui.step(4, 6, "Blocked domains");
|
|
1214
|
+
ui.explain(["Never open these sites, even when everything else is allowed."]);
|
|
1215
|
+
while (true) {
|
|
1216
|
+
const parsed = parseDomainList(await session.question("Blocked domains (comma-separated, Enter for none): "));
|
|
1217
|
+
if (parsed !== void 0) {
|
|
1218
|
+
blockedDomains = parsed;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
ui.failure("That did not look like a domain list. Example: ads.example.com");
|
|
1222
|
+
}
|
|
1223
|
+
ui.step(5, 6, "JavaScript execution");
|
|
1224
|
+
ui.explain([
|
|
1225
|
+
"browser_evaluate runs arbitrary JavaScript on a page - powerful for scraping",
|
|
1226
|
+
"but it can also trigger bot defenses. Most users never need it on."
|
|
1227
|
+
]);
|
|
1228
|
+
allowEval = await askYesNo(session, "Allow the AI to run JavaScript on pages?", false);
|
|
1229
|
+
ui.step(6, 6, "Data directory");
|
|
1230
|
+
ui.explain([
|
|
1231
|
+
"Where the private Chrome profile, logs, and downloads live.",
|
|
1232
|
+
"Permissions are locked to 0600 so only your user can read them."
|
|
1233
|
+
]);
|
|
1234
|
+
while (dataDir === defaults.dataDir) {
|
|
1235
|
+
const answer = (await session.question(`Data directory [${defaults.dataDir}]: `)).trim();
|
|
1236
|
+
if (!answer) break;
|
|
1237
|
+
if (!answer.startsWith("/") || answer.replace(/\/+$/, "") === "") {
|
|
1238
|
+
ui.failure("Enter an absolute path other than the filesystem root.");
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
dataDir = answer;
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
headless = headlessChoice;
|
|
1245
|
+
if (mode === "connect") {
|
|
1246
|
+
ui.note("Starting your personal Chrome with remote debugging on port 9222...");
|
|
1247
|
+
try {
|
|
1248
|
+
const launched = await launchPersonalChrome({ dataDir, spawn: opts.spawn, probe: opts.probe ?? defaultProbe, port: 9222 });
|
|
1249
|
+
browserUrl = launched.url;
|
|
1250
|
+
ui.success(`Connected to your Chrome at ${launched.url}`);
|
|
1251
|
+
} catch {
|
|
1252
|
+
browserUrl = "http://127.0.0.1:9222";
|
|
1253
|
+
ui.note("Could not reach Chrome on port 9222 yet - keeping the default URL.");
|
|
1254
|
+
}
|
|
976
1255
|
}
|
|
977
1256
|
}
|
|
1257
|
+
writeSummary(ui, harness, { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir });
|
|
978
1258
|
return { mode, headless, allowedDomains, blockedDomains, allowEval, dataDir, browserUrl };
|
|
979
1259
|
} finally {
|
|
980
1260
|
rl.close();
|
|
981
1261
|
}
|
|
982
1262
|
}
|
|
1263
|
+
function writeSummary(ui, harness, choices) {
|
|
1264
|
+
const modeLabel = {
|
|
1265
|
+
managed: "Managed private Chrome (isolated profile)",
|
|
1266
|
+
connect: "Your personal Chrome via debugging port",
|
|
1267
|
+
disabled: "Disabled - no browser tools"
|
|
1268
|
+
};
|
|
1269
|
+
ui.banner("Configuration Summary", `Ready to configure ${harness}`, "");
|
|
1270
|
+
ui.keyValues([
|
|
1271
|
+
["Browser mode", choices.mode === "connect" ? modeLabel.connect : modeLabel[choices.mode] ?? choices.mode],
|
|
1272
|
+
["Headless", choices.headless ? "yes - no visible window" : "no - you can watch and intervene"],
|
|
1273
|
+
...choices.mode === "disabled" ? [] : [
|
|
1274
|
+
["Allowed sites", choices.allowedDomains.length ? choices.allowedDomains.join(", ") : "all sites"],
|
|
1275
|
+
["Blocked sites", choices.blockedDomains.length ? choices.blockedDomains.join(", ") : "none"],
|
|
1276
|
+
["Page JavaScript", choices.allowEval ? "enabled" : "off (recommended)"],
|
|
1277
|
+
["Data directory", choices.dataDir]
|
|
1278
|
+
]
|
|
1279
|
+
]);
|
|
1280
|
+
}
|
|
983
1281
|
async function defaultProbe(url, timeoutMs) {
|
|
984
1282
|
try {
|
|
985
1283
|
const controller = new AbortController();
|
|
@@ -995,7 +1293,7 @@ async function defaultProbe(url, timeoutMs) {
|
|
|
995
1293
|
}
|
|
996
1294
|
async function persistWizardConfig(choices, homeDir) {
|
|
997
1295
|
const { join: join8, dirname: dirname5, resolve: resolve6 } = await import("node:path");
|
|
998
|
-
const { mkdir: mkdir4, chmod: chmod3, lstat: lstat4, readFile: readFile3, writeFile: writeFile2, rename:
|
|
1296
|
+
const { mkdir: mkdir4, chmod: chmod3, lstat: lstat4, readFile: readFile3, writeFile: writeFile2, rename: rename4 } = await import("node:fs/promises");
|
|
999
1297
|
const configPath = resolve6(join8(homeDir, ".smooth-operator/config.json"));
|
|
1000
1298
|
await mkdir4(dirname5(configPath), { recursive: true, mode: 448 });
|
|
1001
1299
|
await chmod3(dirname5(configPath), 448).catch(() => {
|
|
@@ -1041,7 +1339,8 @@ async function persistWizardConfig(choices, homeDir) {
|
|
|
1041
1339
|
if (choices.dataDir !== defaultDataDir) {
|
|
1042
1340
|
config.dataDir = choices.dataDir;
|
|
1043
1341
|
}
|
|
1044
|
-
const
|
|
1342
|
+
const { randomUUID: randomUUID4 } = await import("node:crypto");
|
|
1343
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${randomUUID4()}`;
|
|
1045
1344
|
await writeFile2(tmpPath, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
1046
1345
|
await chmod3(tmpPath, 384);
|
|
1047
1346
|
try {
|
|
@@ -1066,7 +1365,7 @@ async function persistWizardConfig(choices, homeDir) {
|
|
|
1066
1365
|
} catch (error) {
|
|
1067
1366
|
if (!isMissingPathError2(error)) throw error;
|
|
1068
1367
|
}
|
|
1069
|
-
await
|
|
1368
|
+
await rename4(tmpPath, configPath);
|
|
1070
1369
|
await chmod3(configPath, 384);
|
|
1071
1370
|
}
|
|
1072
1371
|
async function launchPersonalChrome(opts) {
|
|
@@ -1094,12 +1393,24 @@ async function launchPersonalChrome(opts) {
|
|
|
1094
1393
|
}
|
|
1095
1394
|
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
1395
|
}
|
|
1097
|
-
var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS;
|
|
1396
|
+
var PROBE_INTERVAL_MS, DEFAULT_PROBE_ATTEMPTS, HARNESS_MENU;
|
|
1098
1397
|
var init_installer_wizard = __esm({
|
|
1099
1398
|
"src/server/installer-wizard.ts"() {
|
|
1100
1399
|
"use strict";
|
|
1400
|
+
init_ui();
|
|
1101
1401
|
PROBE_INTERVAL_MS = 300;
|
|
1102
1402
|
DEFAULT_PROBE_ATTEMPTS = 33;
|
|
1403
|
+
HARNESS_MENU = [
|
|
1404
|
+
{ id: "opencode", label: "OpenCode", description: "Configures ~/.config/opencode/opencode.json" },
|
|
1405
|
+
{ id: "claude-code", label: "Claude Code", description: "Runs `claude mcp add` for your user scope" },
|
|
1406
|
+
{ id: "copilot", label: "GitHub Copilot CLI", description: "Runs `copilot mcp add`" },
|
|
1407
|
+
{ id: "codex", label: "OpenAI Codex CLI", description: "Runs `codex mcp add`" },
|
|
1408
|
+
{ id: "gemini", label: "Gemini CLI", description: "Runs `gemini mcp add` for your user scope" },
|
|
1409
|
+
{ id: "vscode", label: "VS Code", description: "Runs `code --add-mcp`" },
|
|
1410
|
+
{ id: "cursor", label: "Cursor", description: "Adds SmoothOperator to ~/.cursor/mcp.json" },
|
|
1411
|
+
{ id: "windsurf", label: "Windsurf", description: "Adds SmoothOperator to Windsurf's mcp_config.json" },
|
|
1412
|
+
{ id: "claude-desktop", label: "Claude Desktop", description: "Updates claude_desktop_config.json" }
|
|
1413
|
+
];
|
|
1103
1414
|
}
|
|
1104
1415
|
});
|
|
1105
1416
|
|
|
@@ -1972,7 +2283,7 @@ init_errors();
|
|
|
1972
2283
|
init_logger();
|
|
1973
2284
|
|
|
1974
2285
|
// src/server/version.ts
|
|
1975
|
-
var SERVER_VERSION = "2.
|
|
2286
|
+
var SERVER_VERSION = "2.2.1";
|
|
1976
2287
|
|
|
1977
2288
|
// src/server/mcp.ts
|
|
1978
2289
|
var EmptyInputSchema = z3.object({}).strict();
|
|
@@ -2797,7 +3108,7 @@ function boundToolError(result) {
|
|
|
2797
3108
|
init_logger();
|
|
2798
3109
|
|
|
2799
3110
|
// src/server/runtime.ts
|
|
2800
|
-
import { chmod, lstat as lstat2, mkdir as mkdir2, open, readFile as readFile2, realpath as realpath2, unlink as unlink2 } from "node:fs/promises";
|
|
3111
|
+
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";
|
|
2801
3112
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2802
3113
|
import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
|
|
2803
3114
|
import process3 from "node:process";
|
|
@@ -7548,12 +7859,41 @@ async function acquireBrowserProfileLease(profileDirectory) {
|
|
|
7548
7859
|
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 });
|
|
7549
7860
|
}
|
|
7550
7861
|
if (existing === "stale") {
|
|
7551
|
-
|
|
7862
|
+
if (await reclaimStaleLock(lockPath)) {
|
|
7863
|
+
continue;
|
|
7864
|
+
}
|
|
7865
|
+
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 });
|
|
7552
7866
|
}
|
|
7553
7867
|
}
|
|
7554
7868
|
}
|
|
7555
7869
|
throw new AppError("BROWSER_PROFILE_IN_USE", "The native browser profile became busy while it was being acquired.", { retryable: true });
|
|
7556
7870
|
}
|
|
7871
|
+
async function reclaimStaleLock(lockPath) {
|
|
7872
|
+
try {
|
|
7873
|
+
const before = await lstat2(lockPath);
|
|
7874
|
+
const raw = await readFile2(lockPath, "utf8");
|
|
7875
|
+
const pid = JSON.parse(raw).pid;
|
|
7876
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
|
|
7877
|
+
return false;
|
|
7878
|
+
}
|
|
7879
|
+
try {
|
|
7880
|
+
process3.kill(pid, 0);
|
|
7881
|
+
return false;
|
|
7882
|
+
} catch (error) {
|
|
7883
|
+
if (fileSystemErrorCode(error) !== "ESRCH") {
|
|
7884
|
+
return false;
|
|
7885
|
+
}
|
|
7886
|
+
}
|
|
7887
|
+
const after = await lstat2(lockPath);
|
|
7888
|
+
if (after.ino !== before.ino || after.dev !== before.dev) {
|
|
7889
|
+
return false;
|
|
7890
|
+
}
|
|
7891
|
+
await rename2(lockPath, `${lockPath}.stale-${randomUUID2()}`);
|
|
7892
|
+
return true;
|
|
7893
|
+
} catch {
|
|
7894
|
+
return false;
|
|
7895
|
+
}
|
|
7896
|
+
}
|
|
7557
7897
|
async function readProfileLock(lockPath) {
|
|
7558
7898
|
let raw;
|
|
7559
7899
|
try {
|
|
@@ -7771,10 +8111,27 @@ async function main(args = process4.argv.slice(2)) {
|
|
|
7771
8111
|
if (!harness) {
|
|
7772
8112
|
throw new AppError("CONFIG_INVALID", "The install command requires a harness target.");
|
|
7773
8113
|
}
|
|
7774
|
-
|
|
8114
|
+
planHarnessInstall(harness, { homeDirectory: homedir5(), environment: process4.env });
|
|
8115
|
+
const wizardChoices = await runWizard2(harness, { yes, stdin: process4.stdin, stdout: process4.stdout, homeDir: homedir5(), env: process4.env, version: SERVER_VERSION });
|
|
7775
8116
|
await persistWizardConfig2(wizardChoices, homedir5());
|
|
7776
|
-
|
|
8117
|
+
const installMessage = await installHarness(harness);
|
|
8118
|
+
const { createUi: createUi2 } = await Promise.resolve().then(() => (init_ui(), ui_exports));
|
|
8119
|
+
const ui = createUi2(process4.stdout);
|
|
8120
|
+
if (process4.stdout.isTTY) {
|
|
8121
|
+
ui.banner("Installation Complete", `${harness} can now drive a browser`, SERVER_VERSION);
|
|
8122
|
+
ui.keyValues([
|
|
8123
|
+
["Config file", `${homedir5()}/.smooth-operator/config.json`],
|
|
8124
|
+
["Browser mode", wizardChoices.mode]
|
|
8125
|
+
]);
|
|
8126
|
+
process4.stdout.write("\n");
|
|
8127
|
+
ui.step(0, 2, "Next steps");
|
|
8128
|
+
ui.option(1, "Restart the harness", "Quit and reopen it so it picks up the new MCP server.");
|
|
8129
|
+
ui.option(2, "Verify", "Ask your AI to run server_health and browser_doctor.");
|
|
8130
|
+
ui.success(installMessage);
|
|
8131
|
+
} else {
|
|
8132
|
+
process4.stdout.write(`${installMessage}
|
|
7777
8133
|
`);
|
|
8134
|
+
}
|
|
7778
8135
|
return;
|
|
7779
8136
|
}
|
|
7780
8137
|
if (args.length === 1 && (args[0] === "--version" || args[0] === "-V")) {
|