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.
@@ -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 rename2, unlink as unlink3, writeFile } from "node:fs/promises";
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, "--scope", "user"] };
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: process.execPath, args: [modulePath] };
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 rename2(tempPath, path);
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) || !sameStdioEntry(existing, entry)) {
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) || !sameOpenCodeEntry(existing, desired)) {
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
- return { config, alreadyConfigured: true };
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 (starting with /).");
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: rename3 } = await import("node:fs/promises");
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 tmpPath = `${configPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
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 rename3(tmpPath, configPath);
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.0";
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
- throw new AppError("BROWSER_PROFILE_LOCK_FAILED", "The native browser profile has a stale lock. Verify that no SmoothOperator process is using it, then remove the lock file and retry.", { retryable: true });
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);