smooth-operator-mcp 2.2.0 → 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 +99 -16
- package/dist/smooth-operator.mjs.map +2 -2
- 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) {
|
|
@@ -1182,8 +1234,8 @@ async function runWizard(harness, opts) {
|
|
|
1182
1234
|
while (dataDir === defaults.dataDir) {
|
|
1183
1235
|
const answer = (await session.question(`Data directory [${defaults.dataDir}]: `)).trim();
|
|
1184
1236
|
if (!answer) break;
|
|
1185
|
-
if (!answer.startsWith("/")) {
|
|
1186
|
-
ui.failure("Enter an absolute path
|
|
1237
|
+
if (!answer.startsWith("/") || answer.replace(/\/+$/, "") === "") {
|
|
1238
|
+
ui.failure("Enter an absolute path other than the filesystem root.");
|
|
1187
1239
|
continue;
|
|
1188
1240
|
}
|
|
1189
1241
|
dataDir = answer;
|
|
@@ -1241,7 +1293,7 @@ async function defaultProbe(url, timeoutMs) {
|
|
|
1241
1293
|
}
|
|
1242
1294
|
async function persistWizardConfig(choices, homeDir) {
|
|
1243
1295
|
const { join: join8, dirname: dirname5, resolve: resolve6 } = await import("node:path");
|
|
1244
|
-
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");
|
|
1245
1297
|
const configPath = resolve6(join8(homeDir, ".smooth-operator/config.json"));
|
|
1246
1298
|
await mkdir4(dirname5(configPath), { recursive: true, mode: 448 });
|
|
1247
1299
|
await chmod3(dirname5(configPath), 448).catch(() => {
|
|
@@ -1287,7 +1339,8 @@ async function persistWizardConfig(choices, homeDir) {
|
|
|
1287
1339
|
if (choices.dataDir !== defaultDataDir) {
|
|
1288
1340
|
config.dataDir = choices.dataDir;
|
|
1289
1341
|
}
|
|
1290
|
-
const
|
|
1342
|
+
const { randomUUID: randomUUID4 } = await import("node:crypto");
|
|
1343
|
+
const tmpPath = `${configPath}.tmp-${process.pid}-${randomUUID4()}`;
|
|
1291
1344
|
await writeFile2(tmpPath, JSON.stringify(config, null, 2) + "\n", { mode: 384, flag: "wx" });
|
|
1292
1345
|
await chmod3(tmpPath, 384);
|
|
1293
1346
|
try {
|
|
@@ -1312,7 +1365,7 @@ async function persistWizardConfig(choices, homeDir) {
|
|
|
1312
1365
|
} catch (error) {
|
|
1313
1366
|
if (!isMissingPathError2(error)) throw error;
|
|
1314
1367
|
}
|
|
1315
|
-
await
|
|
1368
|
+
await rename4(tmpPath, configPath);
|
|
1316
1369
|
await chmod3(configPath, 384);
|
|
1317
1370
|
}
|
|
1318
1371
|
async function launchPersonalChrome(opts) {
|
|
@@ -2230,7 +2283,7 @@ init_errors();
|
|
|
2230
2283
|
init_logger();
|
|
2231
2284
|
|
|
2232
2285
|
// src/server/version.ts
|
|
2233
|
-
var SERVER_VERSION = "2.2.
|
|
2286
|
+
var SERVER_VERSION = "2.2.1";
|
|
2234
2287
|
|
|
2235
2288
|
// src/server/mcp.ts
|
|
2236
2289
|
var EmptyInputSchema = z3.object({}).strict();
|
|
@@ -3055,7 +3108,7 @@ function boundToolError(result) {
|
|
|
3055
3108
|
init_logger();
|
|
3056
3109
|
|
|
3057
3110
|
// 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";
|
|
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";
|
|
3059
3112
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3060
3113
|
import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
|
|
3061
3114
|
import process3 from "node:process";
|
|
@@ -7806,12 +7859,41 @@ async function acquireBrowserProfileLease(profileDirectory) {
|
|
|
7806
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 });
|
|
7807
7860
|
}
|
|
7808
7861
|
if (existing === "stale") {
|
|
7809
|
-
|
|
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 });
|
|
7810
7866
|
}
|
|
7811
7867
|
}
|
|
7812
7868
|
}
|
|
7813
7869
|
throw new AppError("BROWSER_PROFILE_IN_USE", "The native browser profile became busy while it was being acquired.", { retryable: true });
|
|
7814
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
|
+
}
|
|
7815
7897
|
async function readProfileLock(lockPath) {
|
|
7816
7898
|
let raw;
|
|
7817
7899
|
try {
|
|
@@ -8029,6 +8111,7 @@ async function main(args = process4.argv.slice(2)) {
|
|
|
8029
8111
|
if (!harness) {
|
|
8030
8112
|
throw new AppError("CONFIG_INVALID", "The install command requires a harness target.");
|
|
8031
8113
|
}
|
|
8114
|
+
planHarnessInstall(harness, { homeDirectory: homedir5(), environment: process4.env });
|
|
8032
8115
|
const wizardChoices = await runWizard2(harness, { yes, stdin: process4.stdin, stdout: process4.stdout, homeDir: homedir5(), env: process4.env, version: SERVER_VERSION });
|
|
8033
8116
|
await persistWizardConfig2(wizardChoices, homedir5());
|
|
8034
8117
|
const installMessage = await installHarness(harness);
|