writedock-mcp 0.4.14 → 0.4.16

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.
@@ -4,7 +4,7 @@
4
4
  "name": "写作台 WriteDock",
5
5
  "short_name": "写作台",
6
6
  "description": "连接自己的 AI agent,通过 MCP 发布小红书长文、图文和视频,微博图文、视频及长文章,知乎、百家号、B站专栏文章与抖音图文视频。",
7
- "version": "0.4.14",
7
+ "version": "0.4.16",
8
8
  "minimum_chrome_version": "116",
9
9
  "icons": {
10
10
  "16": "assets/icon-16.png",
@@ -1 +1 @@
1
- import './assets/index.ts-CfQ_yCaZ.js';
1
+ import './assets/index.ts-Ch2Bzedq.js';
@@ -1,4 +1,4 @@
1
- WriteDock 安装与更新 v0.4.14
1
+ WriteDock 安装与更新 v0.4.16
2
2
 
3
3
  首次使用
4
4
  1. 把这句话发给你的 agent:帮我安装 WriteDock:先检查电脑上有没有 Node.js 22 或更高版本,没有就帮我安装,需要我操作时告诉我;然后运行 npx -y writedock-mcp@latest setup,最后告诉我怎么重启你。
package/index.cjs CHANGED
@@ -29740,7 +29740,7 @@ var MediaStore = class {
29740
29740
  // package.json
29741
29741
  var package_default = {
29742
29742
  name: "@writedock/mcp-server",
29743
- version: "0.4.14",
29743
+ version: "0.4.16",
29744
29744
  private: true,
29745
29745
  type: "module",
29746
29746
  description: "WriteDock local MCP server for the browser extension",
@@ -50411,7 +50411,7 @@ function createMcpServer(bridge) {
50411
50411
  })).catch((error2) => ({ content: [{ type: "text", text: error2 instanceof Error ? error2.message : String(error2) }], isError: true }));
50412
50412
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
50413
50413
  server.registerTool("get_connection_status", {
50414
- description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u6709 update_notice \u65F6\u8F6C\u544A\u7528\u6237\u3002extension_version \u662F\u7528\u6237\u6B63\u5728\u7528\u7684\u6269\u5C55\u7248\u672C\uFF1B\u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u8868\u793A\u7248\u672C\u672A\u77E5\uFF0C\u4E0D\u5F97\u58F0\u79F0\u6269\u5C55\u4E0E MCP \u7248\u672C\u4E00\u81F4\u3002\u914D\u7F6E\u91CC\u7684 writedock-mcp@latest \u662F\u6709\u610F\u7684\uFF0C\u7528\u6237\u9760\u5B83\u6536\u5230\u66F4\u65B0\uFF0C\u4E0D\u8981\u5EFA\u8BAE\u56FA\u5B9A\u7248\u672C\uFF0C\u4E5F\u4E0D\u8981\u4FEE\u6539\u8FD9\u6761\u914D\u7F6E\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50414
+ description: "\u68C0\u67E5\u672C\u673A MCP \u4E0E\u6269\u5C55\u7684\u8FDE\u63A5\u3001\u8FD0\u884C\u7248\u672C\u548C npm \u66F4\u65B0\u63D0\u793A\u3002update_available=true \u65F6\u544A\u77E5\u7528\u6237\u91CD\u65B0\u542F\u52A8 WriteDock MCP\uFF0C\u4FDD\u7559\u914D\u7F6E\uFF1B\u53D1\u5E03\u4EFB\u52A1\u8FDB\u884C\u4E2D\u4E0D\u8981\u81EA\u52A8\u91CD\u542F\u3002\u6709 update_notice \u65F6\u8F6C\u544A\u7528\u6237\u3002extension_version \u662F\u7528\u6237\u6B63\u5728\u7528\u7684\u6269\u5C55\u7248\u672C\uFF1B\u6CA1\u6709\u8FD9\u4E2A\u5B57\u6BB5\u8868\u793A\u7248\u672C\u672A\u77E5\uFF0C\u4E0D\u5F97\u58F0\u79F0\u6269\u5C55\u4E0E MCP \u7248\u672C\u4E00\u81F4\u3002\u8FD9\u6761 MCP \u914D\u7F6E\uFF08\u5305\u62EC start-mcp \u542F\u52A8\u811A\u672C\uFF09\u662F\u6709\u610F\u7684\uFF0C\u7528\u6237\u9760\u5B83\u6536\u5230\u66F4\u65B0\u5E76\u4F7F\u7528\u6B63\u786E\u7684 Node\uFF0C\u4E0D\u8981\u5EFA\u8BAE\u56FA\u5B9A\u7248\u672C\uFF0C\u4E5F\u4E0D\u8981\u4FEE\u6539\u8FD9\u6761\u914D\u7F6E\u3002\u65E7\u7248\u670D\u52A1\u53EF\u80FD\u4E0D\u8FD4\u56DE\u7248\u672C\u5B57\u6BB5\uFF1Blatest_version \u7F3A\u5931\u6216 update_checking=true \u65F6\uFF0C\u4E0D\u5F97\u58F0\u79F0\u5DF2\u662F\u6700\u65B0\u7248\u3002",
50415
50415
  inputSchema: {},
50416
50416
  annotations: { ...readOnly, openWorldHint: true }
50417
50417
  }, () => wrap(() => bridge.status()));
@@ -50588,48 +50588,133 @@ async function loadIdentity(port, legacyToken) {
50588
50588
 
50589
50589
  // src/setup.ts
50590
50590
  var import_node_child_process = require("child_process");
50591
- var import_promises5 = require("fs/promises");
50591
+ var import_promises6 = require("fs/promises");
50592
50592
  var import_node_os3 = require("os");
50593
+ var import_node_path6 = require("path");
50594
+
50595
+ // src/launcher.ts
50596
+ var import_promises5 = require("fs/promises");
50593
50597
  var import_node_path5 = require("path");
50594
- var NPX_ARGS = ["-y", "--prefer-online", "writedock-mcp@latest"];
50595
- var serverCommand = (platform) => platform === "win32" ? { command: "cmd", args: ["/c", "npx", ...NPX_ARGS] } : { command: "npx", args: NPX_ARGS };
50598
+ var NPX_ARGS = "-y --prefer-online writedock-mcp@latest";
50599
+ var VERSION_CHECK = `process.exit(Number(process.versions.node.split('.')[0])>=22?0:1)`;
50600
+ var NO_NODE = "WriteDock \u9700\u8981 Node.js 22 \u6216\u66F4\u9AD8\u7248\u672C\uFF0C\u8FD9\u53F0\u7535\u8111\u4E0A\u6CA1\u6709\u627E\u5230\u3002\u8BF7\u5B89\u88C5 https://nodejs.org \u7684 LTS \u7248\uFF0C\u7136\u540E\u91CD\u542F agent\u3002";
50601
+ function posixLauncher(nodePath) {
50602
+ const recorded = `'${nodePath.replace(/'/g, `'\\''`)}'`;
50603
+ return `#!/bin/sh
50604
+ # Written by writedock-mcp setup. Running setup again replaces this file.
50605
+ found=""
50606
+ check() {
50607
+ [ -n "$found" ] && return 0
50608
+ [ -x "$1" ] || return 0
50609
+ "$1" -e "${VERSION_CHECK}" >/dev/null 2>&1 && found="$1"
50610
+ return 0
50611
+ }
50612
+ check ${recorded}
50613
+ check "$(command -v node 2>/dev/null)"
50614
+ check /opt/homebrew/bin/node
50615
+ check /usr/local/bin/node
50616
+ for candidate in "$HOME"/.workbuddy/binaries/node/versions/*/bin/node "$HOME"/.workbuddy/binaries/node/versions/*/node "$HOME"/.nvm/versions/node/*/bin/node; do
50617
+ check "$candidate"
50618
+ done
50619
+ if [ -z "$found" ]; then
50620
+ echo "${NO_NODE}" >&2
50621
+ exit 1
50622
+ fi
50623
+ PATH="$(dirname "$found"):$PATH"
50624
+ export PATH
50625
+ exec npx ${NPX_ARGS} "$@"
50626
+ `;
50627
+ }
50628
+ function windowsLauncher(nodePath) {
50629
+ const script = `@echo off
50630
+ chcp 65001 >nul
50631
+ rem Written by writedock-mcp setup. Running setup again replaces this file.
50632
+ set "WRITEDOCK_NODE="
50633
+ call :check "${nodePath.replace(/%/g, "%%")}"
50634
+ if defined WRITEDOCK_NODE goto run
50635
+ for /f "delims=" %%n in ('where node 2^>nul') do if not defined WRITEDOCK_NODE call :check "%%n"
50636
+ if defined WRITEDOCK_NODE goto run
50637
+ for /d %%d in ("%USERPROFILE%\\.workbuddy\\binaries\\node\\versions\\*") do if not defined WRITEDOCK_NODE call :check "%%d\\node.exe"
50638
+ if defined WRITEDOCK_NODE goto run
50639
+ for /d %%d in ("%NVM_HOME%\\v*") do if not defined WRITEDOCK_NODE call :check "%%d\\node.exe"
50640
+ if defined WRITEDOCK_NODE goto run
50641
+ echo ${NO_NODE} 1>&2
50642
+ exit /b 1
50643
+ :run
50644
+ for %%d in ("%WRITEDOCK_NODE%") do set "PATH=%%~dpd;%PATH%"
50645
+ call npx ${NPX_ARGS} %*
50646
+ exit /b %errorlevel%
50647
+ :check
50648
+ if not exist "%~1" exit /b 0
50649
+ "%~1" -e "${VERSION_CHECK}" >nul 2>nul
50650
+ if not errorlevel 1 set "WRITEDOCK_NODE=%~1"
50651
+ exit /b 0
50652
+ `;
50653
+ return script.replace(/\n/g, "\r\n");
50654
+ }
50655
+ async function writeLauncher(home, platform, nodePath) {
50656
+ const directory = (0, import_node_path5.join)(home, "WriteDock");
50657
+ await (0, import_promises5.mkdir)(directory, { recursive: true });
50658
+ if (platform === "win32") {
50659
+ const path2 = (0, import_node_path5.join)(directory, "start-mcp.cmd");
50660
+ await (0, import_promises5.writeFile)(path2, windowsLauncher(nodePath));
50661
+ return { path: path2, command: "cmd", args: ["/c", path2] };
50662
+ }
50663
+ const path = (0, import_node_path5.join)(directory, "start-mcp.sh");
50664
+ await (0, import_promises5.writeFile)(path, posixLauncher(nodePath));
50665
+ await (0, import_promises5.chmod)(path, 493);
50666
+ return { path, command: path, args: [] };
50667
+ }
50668
+
50669
+ // src/setup.ts
50670
+ var isOurs = (text) => text.includes("writedock-mcp@latest") || text.includes("start-mcp");
50596
50671
  async function readIfPresent(path) {
50597
50672
  try {
50598
- return (await (0, import_promises5.readFile)(path, "utf8")).replace(/^\uFEFF/, "");
50673
+ return (await (0, import_promises6.readFile)(path, "utf8")).replace(/^\uFEFF/, "");
50599
50674
  } catch (error2) {
50600
50675
  if (error2.code === "ENOENT") return void 0;
50601
50676
  throw error2;
50602
50677
  }
50603
50678
  }
50604
50679
  async function save(path, content, previous) {
50605
- await (0, import_promises5.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
50606
- if (previous !== void 0) await (0, import_promises5.copyFile)(path, `${path}.writedock-backup`);
50607
- await (0, import_promises5.writeFile)(path, content);
50680
+ await (0, import_promises6.mkdir)((0, import_node_path6.dirname)(path), { recursive: true });
50681
+ if (previous !== void 0) await (0, import_promises6.copyFile)(path, `${path}.writedock-backup`);
50682
+ await (0, import_promises6.writeFile)(path, content);
50608
50683
  }
50609
- async function configureCodex(path, platform = process.platform) {
50684
+ async function configureCodex(path, server) {
50610
50685
  const previous = await readIfPresent(path);
50611
- if (previous !== void 0 && /^\s*\[mcp_servers\.writedock\]/m.test(previous)) return "present";
50612
- const { command, args } = serverCommand(platform);
50613
- const entry = `[mcp_servers.writedock]
50614
- command = "${command}"
50615
- args = ${JSON.stringify(args)}
50686
+ const lines = `command = ${JSON.stringify(server.command)}
50687
+ args = ${JSON.stringify(server.args)}
50616
50688
  startup_timeout_sec = 120
50617
50689
  `;
50690
+ const section = previous === void 0 ? null : /^[ \t]*\[mcp_servers\.writedock\][^\n]*\n([\s\S]*?)(?=^[ \t]*\[|(?![\s\S]))/m.exec(previous);
50691
+ if (section && previous !== void 0) {
50692
+ const customised = /^[ \t]*\[mcp_servers\.writedock\./m.test(previous) || !isOurs(section[1]);
50693
+ const current = section[1].includes(JSON.stringify(server.args).slice(1, -1)) && section[1].includes(JSON.stringify(server.command));
50694
+ if (customised || current) return "present";
50695
+ const start = section.index + section[0].length - section[1].length;
50696
+ await save(path, `${previous.slice(0, start)}${lines}
50697
+ ${previous.slice(start + section[1].length)}`, previous);
50698
+ return "updated";
50699
+ }
50700
+ const entry = `[mcp_servers.writedock]
50701
+ ${lines}`;
50618
50702
  await save(path, previous ? `${previous.replace(/\s*$/, "")}
50619
50703
 
50620
50704
  ${entry}` : entry, previous);
50621
50705
  return "added";
50622
50706
  }
50623
- async function configureJsonClient(path, platform = process.platform) {
50707
+ async function configureJsonClient(path, server) {
50624
50708
  const previous = await readIfPresent(path);
50625
50709
  const config2 = previous?.trim() ? JSON.parse(previous) : {};
50626
- if (config2.mcpServers?.writedock) return "present";
50627
- config2.mcpServers = { ...config2.mcpServers, writedock: serverCommand(platform) };
50710
+ const existing = config2.mcpServers?.writedock;
50711
+ if (existing && (existing.env || !isOurs(JSON.stringify(existing.args ?? [])) || JSON.stringify([existing.command, existing.args]) === JSON.stringify([server.command, server.args]))) return "present";
50712
+ config2.mcpServers = { ...config2.mcpServers, writedock: server };
50628
50713
  await save(path, `${JSON.stringify(config2, null, 2)}
50629
50714
  `, previous);
50630
- return "added";
50715
+ return existing ? "updated" : "added";
50631
50716
  }
50632
- var exists = async (path) => await readIfPresent(path) !== void 0;
50717
+ var exists = (path) => (0, import_promises6.access)(path).then(() => true, () => false);
50633
50718
  function revealFolder(path) {
50634
50719
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
50635
50720
  try {
@@ -50640,12 +50725,15 @@ function revealFolder(path) {
50640
50725
  } catch {
50641
50726
  }
50642
50727
  }
50643
- async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log, platform = process.platform) {
50644
- const claudeDesktop = platform === "win32" ? (0, import_node_path5.join)(process.env.APPDATA || (0, import_node_path5.join)(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : platform === "darwin" ? (0, import_node_path5.join)(home, "Library/Application Support/Claude/claude_desktop_config.json") : (0, import_node_path5.join)(home, ".config/Claude/claude_desktop_config.json");
50728
+ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log, platform = process.platform, nodePath = process.execPath) {
50729
+ const server = await writeLauncher(home, platform, nodePath);
50730
+ const entry = { command: server.command, args: server.args };
50731
+ const claudeDesktop = platform === "win32" ? (0, import_node_path6.join)(process.env.APPDATA || (0, import_node_path6.join)(home, "AppData/Roaming"), "Claude/claude_desktop_config.json") : platform === "darwin" ? (0, import_node_path6.join)(home, "Library/Application Support/Claude/claude_desktop_config.json") : (0, import_node_path6.join)(home, ".config/Claude/claude_desktop_config.json");
50645
50732
  const clients = [
50646
- { name: "Codex", path: (0, import_node_path5.join)(home, ".codex/config.toml"), detect: (0, import_node_path5.join)(home, ".codex/config.toml"), configure: configureCodex },
50647
- { name: "Claude Code", path: (0, import_node_path5.join)(home, ".claude.json"), detect: (0, import_node_path5.join)(home, ".claude.json"), configure: configureJsonClient },
50648
- { name: "Cursor", path: (0, import_node_path5.join)(home, ".cursor/mcp.json"), detect: (0, import_node_path5.join)(home, ".cursor/mcp.json"), configure: configureJsonClient },
50733
+ { name: "Codex", path: (0, import_node_path6.join)(home, ".codex/config.toml"), detect: (0, import_node_path6.join)(home, ".codex/config.toml"), configure: configureCodex },
50734
+ { name: "Claude Code", path: (0, import_node_path6.join)(home, ".claude.json"), detect: (0, import_node_path6.join)(home, ".claude.json"), configure: configureJsonClient },
50735
+ { name: "WorkBuddy", path: (0, import_node_path6.join)(home, ".workbuddy/mcp.json"), detect: (0, import_node_path6.join)(home, ".workbuddy"), configure: configureJsonClient },
50736
+ { name: "Cursor", path: (0, import_node_path6.join)(home, ".cursor/mcp.json"), detect: (0, import_node_path6.join)(home, ".cursor/mcp.json"), configure: configureJsonClient },
50649
50737
  { name: "Claude Desktop", path: claudeDesktop, detect: claudeDesktop, configure: configureJsonClient }
50650
50738
  ];
50651
50739
  let found = 0;
@@ -50653,15 +50741,15 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
50653
50741
  if (!await exists(client.detect)) continue;
50654
50742
  found++;
50655
50743
  try {
50656
- const state = await client.configure(client.path, platform);
50657
- log(state === "added" ? `\u2713 ${client.name}\uFF1A\u5DF2\u6DFB\u52A0 WriteDock\uFF08\u539F\u914D\u7F6E\u5907\u4EFD\u4E3A .writedock-backup\uFF09` : `\u2713 ${client.name}\uFF1A\u5DF2\u6709 WriteDock \u914D\u7F6E\uFF0C\u672A\u6539\u52A8`);
50744
+ const state = await client.configure(client.path, entry);
50745
+ log(state === "present" ? `\u2713 ${client.name}\uFF1A\u5DF2\u6709 WriteDock \u914D\u7F6E\uFF0C\u672A\u6539\u52A8` : `\u2713 ${client.name}\uFF1A\u5DF2${state === "added" ? "\u6DFB\u52A0" : "\u66F4\u65B0"} WriteDock\uFF08\u539F\u914D\u7F6E\u5907\u4EFD\u4E3A .writedock-backup\uFF09`);
50658
50746
  } catch (error2) {
50659
50747
  log(`\u2717 ${client.name}\uFF1A\u914D\u7F6E\u5931\u8D25\uFF08${error2 instanceof Error ? error2.message : String(error2)}\uFF09\uFF0C\u539F\u6587\u4EF6\u672A\u6539\u52A8`);
50660
50748
  }
50661
50749
  }
50662
50750
  if (!found) {
50663
50751
  log("\u6CA1\u6709\u627E\u5230\u5DF2\u77E5\u7684 agent \u5BA2\u6237\u7AEF\u3002\u8BF7\u5728\u4F60\u7684\u5BA2\u6237\u7AEF\u91CC\u6DFB\u52A0\u8FD9\u4E2A MCP \u670D\u52A1\uFF1A");
50664
- log(JSON.stringify({ mcpServers: { writedock: serverCommand(platform) } }, null, 2));
50752
+ log(JSON.stringify({ mcpServers: { writedock: entry } }, null, 2));
50665
50753
  }
50666
50754
  const bundle = bundledExtension();
50667
50755
  const loaded = bundle ? await findInstalledExtension().catch(() => void 0) : void 0;
@@ -50691,7 +50779,7 @@ async function runSetup(home = (0, import_node_os3.homedir)(), log = console.log
50691
50779
  var import_node_child_process2 = require("child_process");
50692
50780
  var import_node_fs2 = require("fs");
50693
50781
  var import_node_os4 = require("os");
50694
- var import_node_path6 = require("path");
50782
+ var import_node_path7 = require("path");
50695
50783
  var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
50696
50784
  async function probe(port) {
50697
50785
  try {
@@ -50713,7 +50801,7 @@ async function until(check2, timeoutMs) {
50713
50801
  function startDetached() {
50714
50802
  const directory = dataDirectory();
50715
50803
  (0, import_node_fs2.mkdirSync)(directory, { recursive: true, mode: 448 });
50716
- const log = (0, import_node_fs2.openSync)((0, import_node_path6.join)(directory, "service.log"), "w");
50804
+ const log = (0, import_node_fs2.openSync)((0, import_node_path7.join)(directory, "service.log"), "w");
50717
50805
  try {
50718
50806
  const child = (0, import_node_child_process2.spawn)(process.execPath, [__filename, "--service"], { detached: true, stdio: ["ignore", "ignore", log], windowsHide: true, cwd: (0, import_node_os4.homedir)() });
50719
50807
  child.on("error", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "writedock-mcp",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "description": "WriteDock local MCP server for the browser extension",
5
5
  "type": "commonjs",
6
6
  "license": "GPL-3.0-only",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writedock/mcp-server",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "WriteDock local MCP server for the browser extension",
@@ -0,0 +1,100 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { dirname, join } from 'node:path'
6
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
7
+ import { windowsLauncher, writeLauncher } from '../launcher'
8
+
9
+ const systemNodeElsewhere = ['/opt/homebrew/bin/node', '/usr/local/bin/node'].some(existsSync)
10
+
11
+ describe.skipIf(process.platform === 'win32')('Mac and Linux launcher, actually run', () => {
12
+ let home: string, fakeBin: string
13
+ const tool = async (path: string, body: string) => { await mkdir(dirname(path), { recursive: true }); await writeFile(path, `#!/bin/sh\n${body}\n`); await chmod(path, 0o755) }
14
+ const oldNode = (path: string) => tool(path, 'exit 1')
15
+ const goodNode = async (path: string) => { await tool(path, 'exit 0'); return path }
16
+ beforeEach(async () => {
17
+ home = await mkdtemp(join(tmpdir(), 'writedock-launcher-'))
18
+ fakeBin = join(home, 'fake-bin')
19
+ // Stands in for npx, so nothing is downloaded: it reports what it was started with.
20
+ await tool(join(fakeBin, 'npx'), 'echo "first-path=${PATH%%:*}"; echo "args=$*"')
21
+ })
22
+ afterEach(() => rm(home, { recursive: true, force: true }))
23
+ const run = async (recorded: string, pathDirs: string[] = []) => {
24
+ const { path } = await writeLauncher(home, 'darwin', recorded)
25
+ return spawnSync(path, ['--extra'], { encoding: 'utf8', env: { HOME: home, PATH: [fakeBin, ...pathDirs, '/usr/bin', '/bin'].join(':') } })
26
+ }
27
+
28
+ it('uses the Node setup ran with, ahead of an old system Node, and puts it first on PATH', async () => {
29
+ const system = join(home, 'system')
30
+ await oldNode(join(system, 'node'))
31
+ const recorded = await goodNode(join(home, 'agent/node'))
32
+ const result = await run(recorded, [system])
33
+ expect(result.status).toBe(0)
34
+ expect(result.stdout).toContain(`first-path=${dirname(recorded)}`)
35
+ expect(result.stdout).toContain('args=-y --prefer-online writedock-mcp@latest --extra')
36
+ })
37
+
38
+ it('falls back to the system Node when the recorded one is gone', async () => {
39
+ const system = await goodNode(join(home, 'system/node'))
40
+ const result = await run(join(home, 'removed-by-agent-update/node'), [dirname(system)])
41
+ expect(result.status).toBe(0)
42
+ expect(result.stdout).toContain(`first-path=${dirname(system)}`)
43
+ })
44
+
45
+ it('finds a newer WorkBuddy Node when its version folder changed, past an old system Node', async () => {
46
+ const system = join(home, 'system')
47
+ await oldNode(join(system, 'node'))
48
+ const moved = join(home, '.workbuddy/binaries/node/versions/23.0.0-1/bin')
49
+ await goodNode(join(moved, 'node'))
50
+ const result = await run(join(home, '.workbuddy/binaries/node/versions/22.22.2-6/bin/node'), [system])
51
+ expect(result.status).toBe(0)
52
+ if (!systemNodeElsewhere) expect(result.stdout).toContain(`first-path=${moved}`)
53
+ })
54
+
55
+ it.skipIf(systemNodeElsewhere)('says what to do when no usable Node exists, and starts nothing', async () => {
56
+ const system = join(home, 'system')
57
+ await oldNode(join(system, 'node'))
58
+ const result = await run(join(home, 'gone/node'), [system])
59
+ expect(result.status).toBe(1)
60
+ expect(result.stderr).toContain('nodejs.org')
61
+ expect(result.stdout).not.toContain('args=')
62
+ })
63
+
64
+ it('copes with spaces and quotes in the recorded path', async () => {
65
+ const odd = join(home, "it's a folder")
66
+ await goodNode(join(odd, 'node'))
67
+ const result = await run(join(odd, 'node'))
68
+ expect(result.status).toBe(0)
69
+ expect(result.stdout).toContain(`first-path=${odd}`)
70
+ })
71
+ })
72
+
73
+ describe('Windows launcher text (cannot run on this machine)', () => {
74
+ const script = windowsLauncher('C:\\Users\\张三\\.workbuddy\\binaries\\node\\versions\\22.22.2-6\\node.exe')
75
+ it('handles non-English user names and uses Windows line endings', () => {
76
+ expect(script.startsWith('@echo off\r\nchcp 65001 >nul\r\n')).toBe(true)
77
+ expect(script).toContain('张三')
78
+ expect(script.replace(/\r\n/g, '')).not.toContain('\n')
79
+ })
80
+ it('checks the recorded Node, then the system, then known folders, in that order', () => {
81
+ const order = ['call :check "C:\\Users', 'where node', '%USERPROFILE%', 'NVM_HOME', 'nodejs.org'].map(text => script.indexOf(text))
82
+ expect(order.every(index => index >= 0)).toBe(true)
83
+ expect([...order].sort((a, b) => a - b)).toEqual(order)
84
+ })
85
+ it('puts the chosen Node first on PATH and starts npx with call, or the script would stop there', () => {
86
+ expect(script).toContain('set "PATH=%%~dpd;%PATH%"')
87
+ expect(script).toContain('call npx -y --prefer-online writedock-mcp@latest %*')
88
+ })
89
+ it('escapes percent signs in the path', () => {
90
+ expect(windowsLauncher('C:\\50%\\node.exe')).toContain('C:\\50%%\\node.exe')
91
+ })
92
+ it('is what setup writes for Windows, started through cmd', async () => {
93
+ const home = await mkdtemp(join(tmpdir(), 'writedock-launcher-'))
94
+ const written = await writeLauncher(home, 'win32', 'C:\\node.exe')
95
+ expect(written.command).toBe('cmd')
96
+ expect(written.args).toEqual(['/c', written.path])
97
+ expect(await readFile(written.path, 'utf8')).toContain('C:\\node.exe')
98
+ await rm(home, { recursive: true, force: true })
99
+ })
100
+ })
@@ -18,11 +18,11 @@ describe('setup', () => {
18
18
  const result = await readFile(join(home, '.codex/config.toml'), 'utf8')
19
19
  expect(result.startsWith(original.trimEnd())).toBe(true)
20
20
  expect(result).toContain('[mcp_servers.writedock]')
21
- expect(result).toContain('"writedock-mcp@latest"')
21
+ expect(result).toContain('start-mcp.sh')
22
22
  expect(await readFile(join(home, '.codex/config.toml.writedock-backup'), 'utf8')).toBe(original)
23
23
  })
24
24
 
25
- it('starts npx through cmd on Windows, and finds Claude Desktop in its Windows folder', async () => {
25
+ it('points clients at the launcher on Windows, and finds Claude Desktop in its Windows folder', async () => {
26
26
  const appData = process.env.APPDATA
27
27
  delete process.env.APPDATA
28
28
  try {
@@ -31,16 +31,59 @@ describe('setup', () => {
31
31
  const desktop = join(home, 'AppData/Roaming/Claude')
32
32
  await mkdir(desktop, { recursive: true })
33
33
  await writeFile(join(desktop, 'claude_desktop_config.json'), '\uFEFF{"theme":"dark"}')
34
- expect(await runSetup(home, line => log.push(line), 'win32')).toBe(2)
34
+ expect(await runSetup(home, line => log.push(line), 'win32', 'C:\\node.exe')).toBe(2)
35
+ const launcher = join(home, 'WriteDock/start-mcp.cmd')
35
36
  const codex = await readFile(join(home, '.codex/config.toml'), 'utf8')
36
37
  expect(codex).toContain('command = "cmd"')
37
- expect(codex).toContain('args = ["/c","npx","-y","--prefer-online","writedock-mcp@latest"]')
38
+ expect(codex).toContain(JSON.stringify(['/c', launcher]))
38
39
  const config = JSON.parse(await readFile(join(desktop, 'claude_desktop_config.json'), 'utf8'))
39
40
  expect(config.theme).toBe('dark')
40
- expect(config.mcpServers.writedock).toEqual({ command: 'cmd', args: ['/c', 'npx', '-y', '--prefer-online', 'writedock-mcp@latest'] })
41
+ expect(config.mcpServers.writedock).toEqual({ command: 'cmd', args: ['/c', launcher] })
42
+ expect(await readFile(launcher, 'utf8')).toContain('C:\\node.exe')
41
43
  } finally { if (appData !== undefined) process.env.APPDATA = appData }
42
44
  })
43
45
 
46
+ it('configures WorkBuddy when it is installed, even before it has an MCP file', async () => {
47
+ await mkdir(join(home, '.workbuddy'))
48
+ expect(await runSetup(home, line => log.push(line), 'win32', 'C:\\node.exe')).toBe(1)
49
+ const config = JSON.parse(await readFile(join(home, '.workbuddy/mcp.json'), 'utf8'))
50
+ expect(config.mcpServers.writedock.command).toBe('cmd')
51
+ expect(log.join()).toContain('WorkBuddy')
52
+ })
53
+
54
+ it('moves an entry from an earlier setup to the launcher, in JSON and in Codex, with a backup', async () => {
55
+ await mkdir(join(home, '.workbuddy'))
56
+ const old = { mcpServers: { writedock: { command: 'cmd', args: ['/c', 'npx', '-y', '--prefer-online', 'writedock-mcp@latest'] }, other: { command: 'x' } } }
57
+ await writeFile(join(home, '.workbuddy/mcp.json'), JSON.stringify(old))
58
+ await mkdir(join(home, '.codex'))
59
+ const toml = 'model = "x"\n\n[mcp_servers.writedock]\ncommand = "npx"\nargs = ["-y","--prefer-online","writedock-mcp@latest"]\nstartup_timeout_sec = 120\n\n[mcp_servers.other]\ncommand = "other"\n'
60
+ await writeFile(join(home, '.codex/config.toml'), toml)
61
+ await runSetup(home, line => log.push(line), 'win32', 'C:\\node.exe')
62
+ const config = JSON.parse(await readFile(join(home, '.workbuddy/mcp.json'), 'utf8'))
63
+ expect(config.mcpServers.writedock.args[1]).toContain('start-mcp')
64
+ expect(config.mcpServers.other).toEqual({ command: 'x' })
65
+ const codex = await readFile(join(home, '.codex/config.toml'), 'utf8')
66
+ expect(codex).toContain('start-mcp')
67
+ expect(codex).not.toContain('writedock-mcp@latest')
68
+ expect(codex).toContain('[mcp_servers.other]\ncommand = "other"')
69
+ expect(codex).toContain('model = "x"')
70
+ expect(await readFile(join(home, '.codex/config.toml.writedock-backup'), 'utf8')).toBe(toml)
71
+ expect(log.join()).toContain('已更新')
72
+ // A second run changes nothing.
73
+ log.length = 0
74
+ await runSetup(home, line => log.push(line), 'win32', 'C:\\node.exe')
75
+ expect(log.join()).not.toContain('已更新')
76
+ expect(await readFile(join(home, '.codex/config.toml'), 'utf8')).toBe(codex)
77
+ })
78
+
79
+ it('leaves a JSON entry with custom settings alone', async () => {
80
+ await mkdir(join(home, '.cursor'))
81
+ const original = JSON.stringify({ mcpServers: { writedock: { command: 'npx', args: ['writedock-mcp@latest'], env: { WRITEDOCK_PORT: '9700' } } } })
82
+ await writeFile(join(home, '.cursor/mcp.json'), original)
83
+ await runSetup(home, line => log.push(line))
84
+ expect(await readFile(join(home, '.cursor/mcp.json'), 'utf8')).toBe(original)
85
+ })
86
+
44
87
  it('leaves an existing entry alone, including its custom port and token', async () => {
45
88
  await mkdir(join(home, '.codex'))
46
89
  const original = '[mcp_servers.writedock]\ncommand = "node"\n\n[mcp_servers.writedock.env]\nWRITEDOCK_PORT = "9700"\n'
@@ -62,8 +105,8 @@ describe('setup', () => {
62
105
  expect(log.join()).toContain('配置失败')
63
106
  })
64
107
 
65
- it('touches nothing when no client exists, and prints the configuration', async () => {
108
+ it('changes no client when none exists, and prints the configuration', async () => {
66
109
  expect(await runSetup(home, line => log.push(line))).toBe(0)
67
- expect(log.join()).toContain('writedock-mcp@latest')
110
+ expect(log.join()).toContain('start-mcp')
68
111
  })
69
112
  })
@@ -0,0 +1,82 @@
1
+ import { chmod, mkdir, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ const NPX_ARGS = '-y --prefer-online writedock-mcp@latest'
5
+ const VERSION_CHECK = `process.exit(Number(process.versions.node.split('.')[0])>=22?0:1)`
6
+ const NO_NODE = 'WriteDock 需要 Node.js 22 或更高版本,这台电脑上没有找到。请安装 https://nodejs.org 的 LTS 版,然后重启 agent。'
7
+
8
+ /**
9
+ * Seen live: an agent ran setup with its own Node 22, then started MCP with the system's Node 16, which cannot run it.
10
+ * So the agent's config points at this script. It looks for a usable Node in order: the one setup ran with (an agent
11
+ * update can remove that folder), the system's, then the places they are usually kept. It puts that Node first on
12
+ * PATH, because npx starts the package through whichever node comes first there.
13
+ */
14
+ export function posixLauncher(nodePath: string) {
15
+ const recorded = `'${nodePath.replace(/'/g, `'\\''`)}'`
16
+ return `#!/bin/sh
17
+ # Written by writedock-mcp setup. Running setup again replaces this file.
18
+ found=""
19
+ check() {
20
+ [ -n "$found" ] && return 0
21
+ [ -x "$1" ] || return 0
22
+ "$1" -e "${VERSION_CHECK}" >/dev/null 2>&1 && found="$1"
23
+ return 0
24
+ }
25
+ check ${recorded}
26
+ check "$(command -v node 2>/dev/null)"
27
+ check /opt/homebrew/bin/node
28
+ check /usr/local/bin/node
29
+ for candidate in "$HOME"/.workbuddy/binaries/node/versions/*/bin/node "$HOME"/.workbuddy/binaries/node/versions/*/node "$HOME"/.nvm/versions/node/*/bin/node; do
30
+ check "$candidate"
31
+ done
32
+ if [ -z "$found" ]; then
33
+ echo "${NO_NODE}" >&2
34
+ exit 1
35
+ fi
36
+ PATH="$(dirname "$found"):$PATH"
37
+ export PATH
38
+ exec npx ${NPX_ARGS} "$@"
39
+ `
40
+ }
41
+
42
+ export function windowsLauncher(nodePath: string) {
43
+ const script = `@echo off
44
+ chcp 65001 >nul
45
+ rem Written by writedock-mcp setup. Running setup again replaces this file.
46
+ set "WRITEDOCK_NODE="
47
+ call :check "${nodePath.replace(/%/g, '%%')}"
48
+ if defined WRITEDOCK_NODE goto run
49
+ for /f "delims=" %%n in ('where node 2^>nul') do if not defined WRITEDOCK_NODE call :check "%%n"
50
+ if defined WRITEDOCK_NODE goto run
51
+ for /d %%d in ("%USERPROFILE%\\.workbuddy\\binaries\\node\\versions\\*") do if not defined WRITEDOCK_NODE call :check "%%d\\node.exe"
52
+ if defined WRITEDOCK_NODE goto run
53
+ for /d %%d in ("%NVM_HOME%\\v*") do if not defined WRITEDOCK_NODE call :check "%%d\\node.exe"
54
+ if defined WRITEDOCK_NODE goto run
55
+ echo ${NO_NODE} 1>&2
56
+ exit /b 1
57
+ :run
58
+ for %%d in ("%WRITEDOCK_NODE%") do set "PATH=%%~dpd;%PATH%"
59
+ call npx ${NPX_ARGS} %*
60
+ exit /b %errorlevel%
61
+ :check
62
+ if not exist "%~1" exit /b 0
63
+ "%~1" -e "${VERSION_CHECK}" >nul 2>nul
64
+ if not errorlevel 1 set "WRITEDOCK_NODE=%~1"
65
+ exit /b 0
66
+ `
67
+ return script.replace(/\n/g, '\r\n')
68
+ }
69
+
70
+ export async function writeLauncher(home: string, platform: NodeJS.Platform, nodePath: string) {
71
+ const directory = join(home, 'WriteDock')
72
+ await mkdir(directory, { recursive: true })
73
+ if (platform === 'win32') {
74
+ const path = join(directory, 'start-mcp.cmd')
75
+ await writeFile(path, windowsLauncher(nodePath))
76
+ return { path, command: 'cmd', args: ['/c', path] }
77
+ }
78
+ const path = join(directory, 'start-mcp.sh')
79
+ await writeFile(path, posixLauncher(nodePath))
80
+ await chmod(path, 0o755)
81
+ return { path, command: path, args: [] as string[] }
82
+ }
@@ -38,7 +38,7 @@ export function createMcpServer(bridge: LocalBridge) {
38
38
  const readOnly = { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
39
39
 
40
40
  server.registerTool('get_connection_status', {
41
- description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。有 update_notice 时转告用户。extension_version 是用户正在用的扩展版本;没有这个字段表示版本未知,不得声称扩展与 MCP 版本一致。配置里的 writedock-mcp@latest 是有意的,用户靠它收到更新,不要建议固定版本,也不要修改这条配置。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
41
+ description: '检查本机 MCP 与扩展的连接、运行版本和 npm 更新提示。update_available=true 时告知用户重新启动 WriteDock MCP,保留配置;发布任务进行中不要自动重启。有 update_notice 时转告用户。extension_version 是用户正在用的扩展版本;没有这个字段表示版本未知,不得声称扩展与 MCP 版本一致。这条 MCP 配置(包括 start-mcp 启动脚本)是有意的,用户靠它收到更新并使用正确的 Node,不要建议固定版本,也不要修改这条配置。旧版服务可能不返回版本字段;latest_version 缺失或 update_checking=true 时,不得声称已是最新版。', inputSchema: {}, annotations: { ...readOnly, openWorldHint: true },
42
42
  }, () => wrap(() => bridge.status()))
43
43
  server.registerTool('list_platforms', {
44
44
  description: '列出扩展实际支持的平台、登录状态、publishModes、对应 tools 及 modeOptions 限制;小红书 article 对应 publish_article,image/video 对应 publish_post。不会返回 Cookie。',