vibe-config-sync 0.2.4 → 0.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 CHANGED
@@ -21,6 +21,7 @@ When using AI coding tools on multiple machines, configurations like skills, com
21
21
  | `agents/` | Agent definitions |
22
22
  | `plugins/installed_plugins.json` | Plugin registry (machine paths stripped) |
23
23
  | `plugins/known_marketplaces.json` | Plugin marketplace sources (machine paths stripped) |
24
+ | `mcp-servers.json` | MCP server configs (extracted from `~/.claude.json`) |
24
25
 
25
26
  **Not synced** (machine-specific / ephemeral / large): `plugins/cache/`, `projects/`, `telemetry/`, `session-env/`, `plans/`, `todos/`, `debug/`, `history.jsonl`, etc.
26
27
 
@@ -82,11 +83,11 @@ vibe-sync pull
82
83
 
83
84
  ## How It Works
84
85
 
85
- - **Export** copies config files into `~/.vibe-sync/data/`, stripping machine-specific paths from plugin JSON files. Skills that are symlinks are resolved and their actual contents are copied.
86
+ - **Export** copies config files into `~/.vibe-sync/data/`, stripping machine-specific paths from plugin JSON files. Skills that are symlinks are resolved and their actual contents are copied. MCP server configurations are extracted from `~/.claude.json` and saved as `mcp-servers.json` (prompts for confirmation if `env` fields containing potential secrets are detected).
86
87
 
87
- - **Import** restores files from `~/.vibe-sync/data/` to `~/.claude/`, validating JSON structure before overwriting. Plugin JSON files are used as a manifest only (never copied directly) — the `claude` CLI installs plugins and writes correct registry files with local paths. Already-installed plugins are detected and skipped.
88
+ - **Import** restores files from `~/.vibe-sync/data/` to `~/.claude/`, validating JSON structure before overwriting. Plugin JSON files are used as a manifest only (never copied directly) — the `claude` CLI installs plugins and writes correct registry files with local paths. Already-installed plugins are detected and skipped. MCP servers are merged into `~/.claude.json` — only new servers are added; existing servers are never overwritten.
88
89
 
89
- - **Backup** is created automatically at `~/.vibe-sync/backups/claude/<timestamp>/` before every import. Use `vibe-sync restore` to list or recover from backups.
90
+ - **Backup** is created automatically at `~/.vibe-sync/backups/claude/<timestamp>/` before every import. Both `~/.claude/` contents and `~/.claude.json` are backed up. Use `vibe-sync restore` to list or recover from backups.
90
91
 
91
92
  ## Sync Strategy
92
93
 
@@ -118,6 +119,12 @@ In short: new content is added, existing content is overwritten, but nothing is
118
119
 
119
120
  Plugin JSON files (`installed_plugins.json`, `known_marketplaces.json`) are exported with machine-specific paths stripped. On import, they are **not** copied to `~/.claude/plugins/` — instead they serve as a manifest. The `claude` CLI is invoked to install each plugin, and it writes correct registry files with local paths as a side effect. Already-installed plugins (detected by `installPath`) are skipped to avoid redundant network operations.
120
121
 
122
+ ### MCP Servers
123
+
124
+ MCP server configurations live in `~/.claude.json` under the `mcpServers` key. On export, this section is extracted and saved as `mcp-servers.json` in the sync directory. If any server config contains `env` fields (which may hold secrets like API keys), the user is prompted before exporting.
125
+
126
+ On import, MCP servers are **additive only** — new servers from the sync repo are added to `~/.claude.json`, but existing servers with the same name are never overwritten. This preserves any machine-specific customizations (e.g., local paths, environment variables).
127
+
121
128
  ### What This Means in Practice
122
129
 
123
130
  - If both machines modify different configs and then sync, the machine that runs `import` last will lose its local changes (a timestamped backup exists at `~/.vibe-sync/backups/claude/` for manual recovery)
@@ -129,6 +136,7 @@ Plugin JSON files (`installed_plugins.json`, `known_marketplaces.json`) are expo
129
136
  | Variable | Description | Default |
130
137
  |----------|-------------|---------|
131
138
  | `CLAUDE_HOME` | Override Claude config directory | `~/.claude` |
139
+ | `CLAUDE_JSON` | Override Claude global config file (MCP servers) | `~/.claude.json` |
132
140
 
133
141
  ## License
134
142
 
package/README.zh-CN.md CHANGED
@@ -21,6 +21,7 @@
21
21
  | `agents/` | 代理定义 |
22
22
  | `plugins/installed_plugins.json` | 插件注册表(已剥离机器路径) |
23
23
  | `plugins/known_marketplaces.json` | 插件市场来源(已剥离机器路径) |
24
+ | `mcp-servers.json` | MCP 服务器配置(从 `~/.claude.json` 提取) |
24
25
 
25
26
  **不同步**(机器特定 / 临时 / 大文件):`plugins/cache/`、`projects/`、`telemetry/`、`session-env/`、`plans/`、`todos/`、`debug/`、`history.jsonl` 等。
26
27
 
@@ -82,11 +83,11 @@ vibe-sync pull
82
83
 
83
84
  ## 工作原理
84
85
 
85
- - **导出**:将配置文件复制到 `~/.vibe-sync/data/`,同时从插件 JSON 文件中剥离机器特定路径。符号链接的技能会被解析并复制其实际内容。
86
+ - **导出**:将配置文件复制到 `~/.vibe-sync/data/`,同时从插件 JSON 文件中剥离机器特定路径。符号链接的技能会被解析并复制其实际内容。MCP 服务器配置从 `~/.claude.json` 中提取并保存为 `mcp-servers.json`(如果检测到包含潜在密钥的 `env` 字段,会提示用户确认)。
86
87
 
87
- - **导入**:从 `~/.vibe-sync/data/` 恢复文件到 `~/.claude/`,在覆盖前验证 JSON 结构。插件 JSON 文件仅作为清单使用(不会直接复制)—— `claude` CLI 负责安装插件并写入包含本地路径的正确注册文件。已安装的插件会被检测并跳过。
88
+ - **导入**:从 `~/.vibe-sync/data/` 恢复文件到 `~/.claude/`,在覆盖前验证 JSON 结构。插件 JSON 文件仅作为清单使用(不会直接复制)—— `claude` CLI 负责安装插件并写入包含本地路径的正确注册文件。已安装的插件会被检测并跳过。MCP 服务器以增量方式合并到 `~/.claude.json`——仅添加新服务器,已存在的服务器不会被覆盖。
88
89
 
89
- - **备份**:每次导入前自动在 `~/.vibe-sync/backups/claude/<timestamp>/` 创建备份。使用 `vibe-sync restore` 列出或恢复备份。
90
+ - **备份**:每次导入前自动在 `~/.vibe-sync/backups/claude/<timestamp>/` 创建备份,同时备份 `~/.claude/` 内容和 `~/.claude.json`。使用 `vibe-sync restore` 列出或恢复备份。
90
91
 
91
92
  ## 同步策略
92
93
 
@@ -118,6 +119,12 @@ vibe-sync pull
118
119
 
119
120
  插件 JSON 文件(`installed_plugins.json`、`known_marketplaces.json`)在导出时会剥离机器特定路径。导入时**不会**复制到 `~/.claude/plugins/`——而是作为清单使用。`claude` CLI 被调用来安装每个插件,并在安装过程中写入包含本地路径的正确注册文件。已安装的插件(通过 `installPath` 检测)会被跳过,以避免冗余的网络操作。
120
121
 
122
+ ### MCP 服务器
123
+
124
+ MCP 服务器配置存储在 `~/.claude.json` 的 `mcpServers` 键下。导出时,该部分被提取并保存为同步目录中的 `mcp-servers.json`。如果任何服务器配置包含 `env` 字段(可能包含 API 密钥等密钥),导出前会提示用户确认。
125
+
126
+ 导入时,MCP 服务器采用**仅增量**策略——同步仓库中的新服务器会被添加到 `~/.claude.json`,但同名的已有服务器不会被覆盖。这保留了机器特定的自定义配置(如本地路径、环境变量)。
127
+
121
128
  ### 实际影响
122
129
 
123
130
  - 如果两台机器修改了不同的配置然后同步,最后执行 `import` 的机器会丢失其本地更改(可在 `~/.vibe-sync/backups/claude/` 找到带时间戳的备份进行手动恢复)
@@ -129,6 +136,7 @@ vibe-sync pull
129
136
  | 变量 | 说明 | 默认值 |
130
137
  |------|------|--------|
131
138
  | `CLAUDE_HOME` | 覆盖 Claude 配置目录 | `~/.claude` |
139
+ | `CLAUDE_JSON` | 覆盖 Claude 全局配置文件(MCP 服务器) | `~/.claude.json` |
132
140
 
133
141
  ## 许可证
134
142
 
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import fs from "fs-extra";
8
8
  import os from "os";
9
9
  import path from "path";
10
10
  var CLAUDE_HOME = process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude");
11
+ var CLAUDE_JSON = process.env.CLAUDE_JSON ?? path.join(os.homedir(), ".claude.json");
11
12
  var SYNC_DIR = path.join(os.homedir(), ".vibe-sync");
12
13
  var BACKUP_BASE = path.join(SYNC_DIR, "backups", "claude");
13
14
  function getConfigDir() {
@@ -19,10 +20,12 @@ function isInitialized() {
19
20
  var SYNC_FILES = ["settings.json", "CLAUDE.md"];
20
21
  var SYNC_DIRS = ["commands", "agents"];
21
22
  var PLUGIN_FILES = ["installed_plugins.json", "known_marketplaces.json"];
23
+ var MCP_SYNC_FILE = "mcp-servers.json";
22
24
 
23
25
  // src/commands/export.ts
24
26
  import fs4 from "fs-extra";
25
27
  import path4 from "path";
28
+ import readline from "readline/promises";
26
29
 
27
30
  // src/core/logger.ts
28
31
  import pc from "picocolors";
@@ -79,6 +82,11 @@ function writeJsonSafe(filePath, data) {
79
82
  }
80
83
 
81
84
  // src/core/sanitize.ts
85
+ function mcpServersHaveEnv(servers) {
86
+ return Object.values(servers).some(
87
+ (config) => typeof config === "object" && config !== null && "env" in config
88
+ );
89
+ }
82
90
  function sanitizePlugins(data) {
83
91
  const result = structuredClone(data);
84
92
  for (const entries of Object.values(result.plugins ?? {})) {
@@ -144,7 +152,21 @@ function importSkills(srcDir, destDir) {
144
152
  }
145
153
 
146
154
  // src/commands/export.ts
147
- function cmdExport() {
155
+ async function confirmMcpExport() {
156
+ const rl = readline.createInterface({
157
+ input: process.stdin,
158
+ output: process.stdout
159
+ });
160
+ try {
161
+ const answer = await rl.question(
162
+ '[WARN] MCP server configs contain "env" fields that may include secrets (API keys, tokens).\n Export anyway? (y/N): '
163
+ );
164
+ return answer.trim().toLowerCase() === "y";
165
+ } finally {
166
+ rl.close();
167
+ }
168
+ }
169
+ async function cmdExport() {
148
170
  if (!fs4.existsSync(CLAUDE_HOME)) {
149
171
  throw new Error(
150
172
  `Claude config directory not found: ${CLAUDE_HOME}
@@ -196,6 +218,21 @@ Make sure Claude Code has been run at least once.`
196
218
  );
197
219
  logInfo("Exported: plugins/known_marketplaces.json (sanitized)");
198
220
  }
221
+ const claudeJson = readJsonSafe(CLAUDE_JSON);
222
+ const mcpServers = claudeJson?.mcpServers;
223
+ if (typeof mcpServers === "object" && mcpServers !== null) {
224
+ const servers = mcpServers;
225
+ let shouldExport = true;
226
+ if (mcpServersHaveEnv(servers)) {
227
+ shouldExport = await confirmMcpExport();
228
+ }
229
+ if (shouldExport) {
230
+ writeJsonSafe(path4.join(configDir, MCP_SYNC_FILE), servers);
231
+ logInfo(`Exported: ${MCP_SYNC_FILE} (from ~/.claude.json)`);
232
+ } else {
233
+ logWarn(`Skipped: ${MCP_SYNC_FILE} (user declined due to env secrets)`);
234
+ }
235
+ }
199
236
  logOk("Export complete");
200
237
  }
201
238
 
@@ -234,6 +271,9 @@ function backupExisting() {
234
271
  fs5.copySync(src, path5.join(backupDir, "plugins", file));
235
272
  }
236
273
  }
274
+ if (fs5.existsSync(CLAUDE_JSON)) {
275
+ fs5.copySync(CLAUDE_JSON, path5.join(backupDir, ".claude.json"));
276
+ }
237
277
  logOk(`Backup created: ${backupDir}`);
238
278
  }
239
279
  function listBackups() {
@@ -283,6 +323,11 @@ function restoreFromBackup(backupName) {
283
323
  }
284
324
  }
285
325
  }
326
+ const claudeJsonSrc = path5.join(backupDir, ".claude.json");
327
+ if (fs5.existsSync(claudeJsonSrc)) {
328
+ fs5.copySync(claudeJsonSrc, CLAUDE_JSON);
329
+ logInfo("Restored: .claude.json");
330
+ }
286
331
  logOk(`Restored from backup: ${backupName}`);
287
332
  }
288
333
 
@@ -508,6 +553,49 @@ function importSyncSkills(configDir, dryRun) {
508
553
  importSkills(skillsSrc, path6.join(CLAUDE_HOME, "skills"));
509
554
  }
510
555
  }
556
+ function readLocalMcpServers() {
557
+ const json = readJsonSafe(CLAUDE_JSON) ?? {};
558
+ const mcpServers = json.mcpServers;
559
+ const servers = typeof mcpServers === "object" && mcpServers !== null ? mcpServers : {};
560
+ return { json, servers };
561
+ }
562
+ function importMcpServers(configDir, dryRun) {
563
+ const src = path6.join(configDir, MCP_SYNC_FILE);
564
+ if (!fs6.existsSync(src)) return;
565
+ const result = validateJsonFile(src);
566
+ if (!result.valid) {
567
+ logWarn(dryRun ? ` INVALID: ${result.errors.join(", ")}` : `Skipping ${MCP_SYNC_FILE}: ${result.errors.join(", ")}`);
568
+ return;
569
+ }
570
+ if (dryRun) logOk(` Validated: ${MCP_SYNC_FILE}`);
571
+ const syncedServers = readJsonSafe(src);
572
+ if (!syncedServers) return;
573
+ const { json: existing, servers: currentServers } = readLocalMcpServers();
574
+ const syncedNames = Object.keys(syncedServers);
575
+ const newNames = syncedNames.filter((n) => !(n in currentServers));
576
+ if (dryRun) {
577
+ const skipNames = syncedNames.filter((n) => n in currentServers);
578
+ if (newNames.length > 0) logInfo(`Would import MCP servers: ${newNames.join(", ")}`);
579
+ if (skipNames.length > 0) logWarn(`Would skip (already exist): ${skipNames.join(", ")}`);
580
+ if (newNames.length === 0) logInfo("No new MCP servers to import");
581
+ return;
582
+ }
583
+ if (newNames.length === 0) {
584
+ logInfo("No new MCP servers to import (all already exist locally)");
585
+ return;
586
+ }
587
+ for (const name of syncedNames) {
588
+ if (name in currentServers) {
589
+ logWarn(`MCP server already exists locally, skipping: ${name}`);
590
+ }
591
+ }
592
+ const newServers = Object.fromEntries(
593
+ newNames.map((name) => [name, syncedServers[name]])
594
+ );
595
+ existing.mcpServers = { ...currentServers, ...newServers };
596
+ fs6.writeJsonSync(CLAUDE_JSON, existing, { spaces: 2 });
597
+ logInfo(`Imported: ${MCP_SYNC_FILE} \u2192 ~/.claude.json (added ${newNames.length} new server(s))`);
598
+ }
511
599
  function syncPlugins(configDir, existingState, dryRun) {
512
600
  const pluginsSrc = path6.join(configDir, "plugins");
513
601
  const hasManifest = PLUGIN_FILES.some(
@@ -559,6 +647,7 @@ Run "vibe-sync export" first or "vibe-sync pull"`
559
647
  importSyncFiles(configDir, options);
560
648
  importSyncDirs(configDir, dryRun);
561
649
  importSyncSkills(configDir, dryRun);
650
+ importMcpServers(configDir, dryRun);
562
651
  if (options.reinstallPlugins) {
563
652
  syncPlugins(configDir, existingState, dryRun);
564
653
  } else {
@@ -568,6 +657,8 @@ Run "vibe-sync export" first or "vibe-sync pull"`
568
657
  }
569
658
 
570
659
  // src/commands/status.ts
660
+ import fs8 from "fs-extra";
661
+ import os2 from "os";
571
662
  import path8 from "path";
572
663
 
573
664
  // src/core/diff.ts
@@ -665,6 +756,23 @@ function cmdStatus() {
665
756
  hasDiff = true;
666
757
  }
667
758
  }
759
+ const repoMcp = path8.join(configDir, MCP_SYNC_FILE);
760
+ const claudeJson = readJsonSafe(CLAUDE_JSON);
761
+ const localMcp = typeof claudeJson?.mcpServers === "object" && claudeJson.mcpServers !== null ? claudeJson.mcpServers : void 0;
762
+ if (localMcp || fs8.existsSync(repoMcp)) {
763
+ const tmpFile = path8.join(os2.tmpdir(), `vibe-sync-mcp-${Date.now()}.json`);
764
+ try {
765
+ if (localMcp) {
766
+ fs8.writeJsonSync(tmpFile, localMcp, { spaces: 2 });
767
+ }
768
+ const localPath = localMcp ? tmpFile : "";
769
+ if (showDiff(localPath, repoMcp, MCP_SYNC_FILE)) {
770
+ hasDiff = true;
771
+ }
772
+ } finally {
773
+ fs8.removeSync(tmpFile);
774
+ }
775
+ }
668
776
  if (!hasDiff) {
669
777
  logOk("No differences found");
670
778
  }
@@ -776,7 +884,7 @@ async function pullFromRemote(git) {
776
884
 
777
885
  // src/commands/push.ts
778
886
  async function cmdPush() {
779
- cmdExport();
887
+ await cmdExport();
780
888
  const git = createGit(SYNC_DIR);
781
889
  await commitAndPush(git);
782
890
  }
@@ -810,12 +918,12 @@ async function cmdPull(options = {}) {
810
918
  }
811
919
 
812
920
  // src/commands/init.ts
813
- import fs8 from "fs-extra";
921
+ import fs9 from "fs-extra";
814
922
  import path9 from "path";
815
- import readline from "readline/promises";
923
+ import readline2 from "readline/promises";
816
924
  var GIT_URL_PATTERN = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/).+/;
817
925
  function createReadline() {
818
- return readline.createInterface({
926
+ return readline2.createInterface({
819
927
  input: process.stdin,
820
928
  output: process.stdout
821
929
  });
@@ -867,7 +975,7 @@ async function freshInit() {
867
975
  logError("URL cannot be empty");
868
976
  return;
869
977
  }
870
- fs8.ensureDirSync(SYNC_DIR);
978
+ fs9.ensureDirSync(SYNC_DIR);
871
979
  const git = createGit(SYNC_DIR);
872
980
  await git.init();
873
981
  logOk("Initialized git repository");
@@ -879,8 +987,8 @@ async function freshInit() {
879
987
  logInfo("No existing data on remote (new repository)");
880
988
  }
881
989
  const gitignorePath = path9.join(SYNC_DIR, ".gitignore");
882
- if (!fs8.existsSync(gitignorePath)) {
883
- fs8.writeFileSync(gitignorePath, ".DS_Store\nThumbs.db\n");
990
+ if (!fs9.existsSync(gitignorePath)) {
991
+ fs9.writeFileSync(gitignorePath, ".DS_Store\nThumbs.db\n");
884
992
  }
885
993
  console.log("");
886
994
  logOk("Setup complete!");
@@ -926,7 +1034,7 @@ function cmdRestore(timestamp) {
926
1034
 
927
1035
  // src/index.ts
928
1036
  var program = new Command();
929
- program.name("vibe-sync").description("Sync AI coding tool configurations across machines via git").version("0.2.4").action(async () => {
1037
+ program.name("vibe-sync").description("Sync AI coding tool configurations across machines via git").version("0.3.0").action(async () => {
930
1038
  if (!isInitialized()) {
931
1039
  await cmdInit();
932
1040
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-config-sync",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Cross-platform CLI tool to sync AI coding tool configurations across machines via git",
5
5
  "type": "module",
6
6
  "bin": {