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 +1 -1
- package/dist/smooth-operator.mjs +199 -47
- package/dist/smooth-operator.mjs.map +2 -2
- package/package.json +1 -1
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
|
|
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
|
|
package/dist/smooth-operator.mjs
CHANGED
|
@@ -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,
|
|
352
|
+
function windowsChannelCandidates(directories, directoryParts, executable, label, channel) {
|
|
338
353
|
return directories.map((baseDirectory) => ({
|
|
339
|
-
path: win32.join(baseDirectory,
|
|
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
|
-
["
|
|
351
|
-
["
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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);
|