@kwonye/mcpx 0.1.94 → 0.1.96

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.
Files changed (69) hide show
  1. package/dist/adapters/claude-desktop.d.ts.map +1 -1
  2. package/dist/adapters/claude.d.ts +2 -1
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/cline.d.ts +3 -1
  5. package/dist/adapters/cline.d.ts.map +1 -1
  6. package/dist/adapters/codex.d.ts +2 -1
  7. package/dist/adapters/codex.d.ts.map +1 -1
  8. package/dist/adapters/cursor.d.ts +2 -1
  9. package/dist/adapters/cursor.d.ts.map +1 -1
  10. package/dist/adapters/hermes.d.ts +11 -0
  11. package/dist/adapters/hermes.d.ts.map +1 -0
  12. package/dist/adapters/index.d.ts.map +1 -1
  13. package/dist/adapters/kiro.d.ts.map +1 -1
  14. package/dist/adapters/openclaw.d.ts +11 -0
  15. package/dist/adapters/openclaw.d.ts.map +1 -0
  16. package/dist/adapters/opencode.d.ts +2 -1
  17. package/dist/adapters/opencode.d.ts.map +1 -1
  18. package/dist/adapters/qwen.d.ts.map +1 -1
  19. package/dist/adapters/utils/index.d.ts +7 -1
  20. package/dist/adapters/utils/index.d.ts.map +1 -1
  21. package/dist/adapters/vscode.d.ts +2 -1
  22. package/dist/adapters/vscode.d.ts.map +1 -1
  23. package/dist/{cli-n8vt375e.js → cli-2vg0te30.js} +715 -211
  24. package/dist/{cli-154shqmt.js → cli-ap2jhwnz.js} +311 -95
  25. package/dist/{cli-tk8bggg5.js → cli-remv48t3.js} +4 -2
  26. package/dist/{cli-csfsg1yf.js → cli-v27bt25d.js} +79 -7
  27. package/dist/{cli-9fmn9y72.js → cli-z1fv3ra8.js} +105 -29
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +362 -75
  30. package/dist/compat/hermes.d.ts +7 -0
  31. package/dist/compat/hermes.d.ts.map +1 -0
  32. package/dist/compat/index.d.ts.map +1 -1
  33. package/dist/compat/openclaw.d.ts +7 -0
  34. package/dist/compat/openclaw.d.ts.map +1 -0
  35. package/dist/{config-dtc3c79c.js → config-5p08cb0c.js} +1 -1
  36. package/dist/core/add-command.d.ts.map +1 -1
  37. package/dist/core/config-store.d.ts +30 -0
  38. package/dist/core/config-store.d.ts.map +1 -1
  39. package/dist/core/config.d.ts.map +1 -1
  40. package/dist/core/daemon.d.ts +5 -1
  41. package/dist/core/daemon.d.ts.map +1 -1
  42. package/dist/core/errors.d.ts.map +1 -1
  43. package/dist/core/index.d.ts +2 -2
  44. package/dist/core/index.d.ts.map +1 -1
  45. package/dist/core/index.js +16 -67
  46. package/dist/core/managed-index-lock.d.ts +2 -0
  47. package/dist/core/managed-index-lock.d.ts.map +1 -0
  48. package/dist/core/oauth.d.ts.map +1 -1
  49. package/dist/core/plugin-host.d.ts +2 -0
  50. package/dist/core/plugin-host.d.ts.map +1 -0
  51. package/dist/core/plugin-manager.d.ts +19 -6
  52. package/dist/core/plugin-manager.d.ts.map +1 -1
  53. package/dist/core/plugin-parse.d.ts.map +1 -1
  54. package/dist/core/plugin-projections.d.ts.map +1 -1
  55. package/dist/core/skill-projections.d.ts +3 -0
  56. package/dist/core/skill-projections.d.ts.map +1 -0
  57. package/dist/core/status.d.ts.map +1 -1
  58. package/dist/core/sync.d.ts.map +1 -1
  59. package/dist/core/update-manager.d.ts.map +1 -1
  60. package/dist/gateway/server.d.ts.map +1 -1
  61. package/dist/plugin-host-73dvcen6.js +129 -0
  62. package/dist/{plugin-manager-1sjwfnah.js → plugin-manager-y5txkse1.js} +12 -4
  63. package/dist/{registry-n12thgf2.js → registry-bnjb2345.js} +1 -1
  64. package/dist/types.d.ts +3 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.d.ts.map +1 -1
  68. package/package.json +3 -2
  69. package/dist/cli-mxmgxdm2.js +0 -2257
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ensureGatewayToken
3
- } from "./cli-tk8bggg5.js";
3
+ } from "./cli-remv48t3.js";
4
4
  import {
5
5
  ensureDir,
6
6
  getManagedIndexPath,
@@ -68,6 +68,7 @@ function loadOwnership(targetDir) {
68
68
  try {
69
69
  return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
70
70
  } catch {
71
+ console.error(`[mcpx] Warning: corrupt plugin ownership manifest at ${manifestPath}, resetting`);
71
72
  return { version: 1, plugins: {} };
72
73
  }
73
74
  }
@@ -123,6 +124,13 @@ function pruneUnlistedPlugins(targetDir, keepPluginIds) {
123
124
  function nsName(pluginName, id) {
124
125
  return `${pluginName}__${id}`;
125
126
  }
127
+ function assertWithinBase(base, target) {
128
+ const resolvedBase = path2.resolve(base);
129
+ const resolvedTarget = path2.resolve(target);
130
+ if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path2.sep)) {
131
+ throw new Error(`Refusing to write outside projection base: ${target} (base: ${base})`);
132
+ }
133
+ }
126
134
  function isComponentApproved(plugin, component) {
127
135
  const approval = plugin.approvals?.[component];
128
136
  if (approval === false)
@@ -178,6 +186,7 @@ function syncCodex(plugins) {
178
186
  const targetBase = path2.join(homeDir(), ".codex", "skills");
179
187
  const projectedDirs = [];
180
188
  const unsupported = ["hooks", "agents"];
189
+ const errors = [];
181
190
  for (const plugin of plugins) {
182
191
  if (!plugin.enabled)
183
192
  continue;
@@ -185,11 +194,16 @@ function syncCodex(plugins) {
185
194
  ensureDir(targetBase);
186
195
  const owned = [];
187
196
  for (const skill of plugin.skills) {
188
- const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
189
- ensureDir(targetDir);
190
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
191
- owned.push(nsName(plugin.pluginName, skill.id));
192
- projectedDirs.push(targetDir);
197
+ try {
198
+ const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
199
+ assertWithinBase(targetBase, targetDir);
200
+ ensureDir(targetDir);
201
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
202
+ owned.push(nsName(plugin.pluginName, skill.id));
203
+ projectedDirs.push(targetDir);
204
+ } catch (error) {
205
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
206
+ }
193
207
  }
194
208
  recordOwned(targetBase, plugin.pluginId, owned);
195
209
  }
@@ -198,7 +212,8 @@ function syncCodex(plugins) {
198
212
  clientId: "codex",
199
213
  status: "SYNCED",
200
214
  projectedDirs,
201
- unsupported
215
+ unsupported,
216
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
202
217
  };
203
218
  }
204
219
  function syncCursor(plugins) {
@@ -206,28 +221,39 @@ function syncCursor(plugins) {
206
221
  const commandsBase = path2.join(homeDir(), ".cursor", "commands");
207
222
  const projectedDirs = [];
208
223
  const unsupported = ["hooks", "agents"];
209
- const ownedSkills = [];
210
- const ownedCommands = [];
224
+ const errors = [];
211
225
  for (const plugin of plugins) {
212
226
  if (!plugin.enabled)
213
227
  continue;
228
+ const ownedSkills = [];
229
+ const ownedCommands = [];
214
230
  if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
215
231
  ensureDir(skillsBase);
216
232
  for (const skill of plugin.skills) {
217
- const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
218
- ensureDir(targetDir);
219
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
220
- ownedSkills.push(nsName(plugin.pluginName, skill.id));
221
- projectedDirs.push(targetDir);
233
+ try {
234
+ const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
235
+ assertWithinBase(skillsBase, targetDir);
236
+ ensureDir(targetDir);
237
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
238
+ ownedSkills.push(nsName(plugin.pluginName, skill.id));
239
+ projectedDirs.push(targetDir);
240
+ } catch (error) {
241
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
242
+ }
222
243
  }
223
244
  }
224
245
  if (plugin.components.commands && isComponentApproved(plugin, "commands")) {
225
246
  ensureDir(commandsBase);
226
247
  for (const cmd of plugin.commands) {
227
- const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
228
- copyFileOrDir(cmd.path, targetPath);
229
- ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
230
- projectedDirs.push(targetPath);
248
+ try {
249
+ const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
250
+ assertWithinBase(commandsBase, targetPath);
251
+ copyFileOrDir(cmd.path, targetPath);
252
+ ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
253
+ projectedDirs.push(targetPath);
254
+ } catch (error) {
255
+ errors.push(`${plugin.pluginName}/${cmd.id}: ${error.message}`);
256
+ }
231
257
  }
232
258
  }
233
259
  if (ownedSkills.length > 0)
@@ -239,13 +265,15 @@ function syncCursor(plugins) {
239
265
  clientId: "cursor",
240
266
  status: "SYNCED",
241
267
  projectedDirs,
242
- unsupported
268
+ unsupported,
269
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
243
270
  };
244
271
  }
245
272
  function syncVsCode(plugins) {
246
273
  const skillsBase = path2.join(homeDir(), ".copilot", "skills");
247
274
  const projectedDirs = [];
248
275
  const unsupported = ["hooks", "agents"];
276
+ const errors = [];
249
277
  for (const plugin of plugins) {
250
278
  if (!plugin.enabled)
251
279
  continue;
@@ -253,11 +281,16 @@ function syncVsCode(plugins) {
253
281
  ensureDir(skillsBase);
254
282
  const owned = [];
255
283
  for (const skill of plugin.skills) {
256
- const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
257
- ensureDir(targetDir);
258
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
259
- owned.push(nsName(plugin.pluginName, skill.id));
260
- projectedDirs.push(targetDir);
284
+ try {
285
+ const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
286
+ assertWithinBase(skillsBase, targetDir);
287
+ ensureDir(targetDir);
288
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
289
+ owned.push(nsName(plugin.pluginName, skill.id));
290
+ projectedDirs.push(targetDir);
291
+ } catch (error) {
292
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
293
+ }
261
294
  }
262
295
  recordOwned(skillsBase, plugin.pluginId, owned);
263
296
  }
@@ -266,13 +299,15 @@ function syncVsCode(plugins) {
266
299
  clientId: "vscode",
267
300
  status: "SYNCED",
268
301
  projectedDirs,
269
- unsupported
302
+ unsupported,
303
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
270
304
  };
271
305
  }
272
306
  function syncQwen(plugins) {
273
307
  const targetBase = path2.join(homeDir(), ".qwen", "skills");
274
308
  const projectedDirs = [];
275
309
  const unsupported = ["hooks", "agents", "commands"];
310
+ const errors = [];
276
311
  for (const plugin of plugins) {
277
312
  if (!plugin.enabled)
278
313
  continue;
@@ -280,11 +315,16 @@ function syncQwen(plugins) {
280
315
  ensureDir(targetBase);
281
316
  const owned = [];
282
317
  for (const skill of plugin.skills) {
283
- const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
284
- ensureDir(targetDir);
285
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
286
- owned.push(nsName(plugin.pluginName, skill.id));
287
- projectedDirs.push(targetDir);
318
+ try {
319
+ const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
320
+ assertWithinBase(targetBase, targetDir);
321
+ ensureDir(targetDir);
322
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
323
+ owned.push(nsName(plugin.pluginName, skill.id));
324
+ projectedDirs.push(targetDir);
325
+ } catch (error) {
326
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
327
+ }
288
328
  }
289
329
  recordOwned(targetBase, plugin.pluginId, owned);
290
330
  }
@@ -293,13 +333,15 @@ function syncQwen(plugins) {
293
333
  clientId: "qwen",
294
334
  status: "SYNCED",
295
335
  projectedDirs,
296
- unsupported
336
+ unsupported,
337
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
297
338
  };
298
339
  }
299
340
  function syncCline(plugins) {
300
341
  const targetBase = path2.join(homeDir(), ".config", "cline", "skills");
301
342
  const projectedDirs = [];
302
343
  const unsupported = ["hooks", "agents", "commands"];
344
+ const errors = [];
303
345
  for (const plugin of plugins) {
304
346
  if (!plugin.enabled)
305
347
  continue;
@@ -307,11 +349,16 @@ function syncCline(plugins) {
307
349
  ensureDir(targetBase);
308
350
  const owned = [];
309
351
  for (const skill of plugin.skills) {
310
- const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
311
- ensureDir(targetDir);
312
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
313
- owned.push(nsName(plugin.pluginName, skill.id));
314
- projectedDirs.push(targetDir);
352
+ try {
353
+ const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
354
+ assertWithinBase(targetBase, targetDir);
355
+ ensureDir(targetDir);
356
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
357
+ owned.push(nsName(plugin.pluginName, skill.id));
358
+ projectedDirs.push(targetDir);
359
+ } catch (error) {
360
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
361
+ }
315
362
  }
316
363
  recordOwned(targetBase, plugin.pluginId, owned);
317
364
  }
@@ -320,13 +367,15 @@ function syncCline(plugins) {
320
367
  clientId: "cline",
321
368
  status: "SYNCED",
322
369
  projectedDirs,
323
- unsupported
370
+ unsupported,
371
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
324
372
  };
325
373
  }
326
374
  function syncKiro(plugins) {
327
375
  const targetBase = path2.join(homeDir(), ".kiro", "skills");
328
376
  const projectedDirs = [];
329
377
  const unsupported = ["hooks", "agents", "commands"];
378
+ const errors = [];
330
379
  for (const plugin of plugins) {
331
380
  if (!plugin.enabled)
332
381
  continue;
@@ -334,11 +383,16 @@ function syncKiro(plugins) {
334
383
  ensureDir(targetBase);
335
384
  const owned = [];
336
385
  for (const skill of plugin.skills) {
337
- const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
338
- ensureDir(targetDir);
339
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
340
- owned.push(nsName(plugin.pluginName, skill.id));
341
- projectedDirs.push(targetDir);
386
+ try {
387
+ const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
388
+ assertWithinBase(targetBase, targetDir);
389
+ ensureDir(targetDir);
390
+ copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
391
+ owned.push(nsName(plugin.pluginName, skill.id));
392
+ projectedDirs.push(targetDir);
393
+ } catch (error) {
394
+ errors.push(`${plugin.pluginName}/${skill.id}: ${error.message}`);
395
+ }
342
396
  }
343
397
  recordOwned(targetBase, plugin.pluginId, owned);
344
398
  }
@@ -347,6 +401,16 @@ function syncKiro(plugins) {
347
401
  clientId: "kiro",
348
402
  status: "SYNCED",
349
403
  projectedDirs,
404
+ unsupported,
405
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
406
+ };
407
+ }
408
+ function syncOpenCode(plugins) {
409
+ const unsupported = ["skills", "hooks", "agents", "commands"];
410
+ return {
411
+ clientId: "opencode",
412
+ status: plugins.length > 0 ? "SYNCED" : "SKIPPED",
413
+ projectedDirs: [],
350
414
  unsupported
351
415
  };
352
416
  }
@@ -359,7 +423,8 @@ function syncPluginsToClient(clientId, plugins) {
359
423
  vscode: syncVsCode,
360
424
  qwen: syncQwen,
361
425
  cline: syncCline,
362
- kiro: syncKiro
426
+ kiro: syncKiro,
427
+ opencode: syncOpenCode
363
428
  };
364
429
  const syncFn = syncMap[clientId];
365
430
  if (!syncFn) {
@@ -373,7 +438,8 @@ function syncPluginsToClient(clientId, plugins) {
373
438
  vscode: path2.join(homeDir(), ".copilot", "skills"),
374
439
  qwen: path2.join(homeDir(), ".qwen", "skills"),
375
440
  cline: path2.join(homeDir(), ".config", "cline", "skills"),
376
- kiro: path2.join(homeDir(), ".kiro", "skills")
441
+ kiro: path2.join(homeDir(), ".kiro", "skills"),
442
+ opencode: ""
377
443
  };
378
444
  const target = targetBaseMap[clientId];
379
445
  if (target) {
@@ -382,7 +448,7 @@ function syncPluginsToClient(clientId, plugins) {
382
448
  }
383
449
  return syncFn(plugins);
384
450
  }
385
- function copyPluginDir(src, dest, exclude = []) {
451
+ function copyPluginDir(src, dest, exclude = [], errors) {
386
452
  const excludeSet = new Set(exclude);
387
453
  ensureDir(dest);
388
454
  const entries = fs2.readdirSync(src, { withFileTypes: true });
@@ -392,29 +458,88 @@ function copyPluginDir(src, dest, exclude = []) {
392
458
  const srcPath = path2.join(src, entry.name);
393
459
  const destPath = path2.join(dest, entry.name);
394
460
  if (entry.isDirectory()) {
395
- copyPluginDir(srcPath, destPath, exclude);
461
+ copyPluginDir(srcPath, destPath, exclude, errors);
396
462
  } else if (entry.isFile()) {
397
463
  try {
398
464
  fs2.copyFileSync(srcPath, destPath);
399
- } catch {}
465
+ } catch (e) {
466
+ errors?.push(`Failed to copy ${srcPath}: ${e.message}`);
467
+ }
400
468
  }
401
469
  }
402
470
  }
403
- function copyFileOrDir(src, dest) {
471
+ function copyFileOrDir(src, dest, errors) {
404
472
  try {
405
473
  ensureDir(path2.dirname(dest));
406
474
  if (fs2.statSync(src).isDirectory()) {
407
- copyPluginDir(src, dest);
475
+ copyPluginDir(src, dest, [], errors);
408
476
  } else {
409
477
  fs2.copyFileSync(src, dest);
410
478
  }
411
- } catch {}
479
+ } catch (e) {
480
+ errors?.push(`Failed to copy ${src} -> ${dest}: ${e.message}`);
481
+ }
412
482
  }
413
483
 
484
+ // src/version.ts
485
+ var APP_VERSION = "0.1.3";
486
+
414
487
  // src/adapters/claude.ts
488
+ import fs4 from "node:fs";
489
+ import path4 from "node:path";
490
+ import { z } from "zod";
491
+
492
+ // src/core/skill-projections.ts
415
493
  import fs3 from "node:fs";
416
494
  import path3 from "node:path";
417
- import { z } from "zod";
495
+ var OWNERSHIP_MANIFEST2 = "mcpx-skills.json";
496
+ function loadOwnership2(targetDir) {
497
+ const manifestPath = path3.join(targetDir, OWNERSHIP_MANIFEST2);
498
+ if (!fs3.existsSync(manifestPath)) {
499
+ return { version: 1, paths: [] };
500
+ }
501
+ try {
502
+ return JSON.parse(fs3.readFileSync(manifestPath, "utf8"));
503
+ } catch {
504
+ console.error(`[mcpx] Warning: corrupt ownership manifest at ${manifestPath}, resetting`);
505
+ return { version: 1, paths: [] };
506
+ }
507
+ }
508
+ function saveOwnership2(targetDir, manifest) {
509
+ ensureDir(targetDir);
510
+ fs3.writeFileSync(path3.join(targetDir, OWNERSHIP_MANIFEST2), JSON.stringify(manifest, null, 2));
511
+ }
512
+ function projectSkillsToDir(targetDir, skills, layout) {
513
+ const owned = loadOwnership2(targetDir);
514
+ const newPaths = new Set(skills.map((s) => layout === "dir" ? `${s.id}/SKILL.md` : `${s.id}.md`));
515
+ for (const ownedPath of owned.paths) {
516
+ if (!newPaths.has(ownedPath)) {
517
+ const fullPath = path3.join(targetDir, ownedPath);
518
+ try {
519
+ if (fs3.existsSync(fullPath)) {
520
+ fs3.rmSync(fullPath, { recursive: true, force: true });
521
+ }
522
+ if (layout === "dir") {
523
+ const dirPath = path3.join(targetDir, path3.dirname(ownedPath));
524
+ try {
525
+ const remaining = fs3.readdirSync(dirPath);
526
+ if (remaining.length === 0) {
527
+ fs3.rmSync(dirPath, { recursive: true, force: true });
528
+ }
529
+ } catch {}
530
+ }
531
+ } catch {
532
+ console.error(`[mcpx] Warning: could not remove stale skill projection: ${fullPath}`);
533
+ }
534
+ }
535
+ }
536
+ for (const skill of skills) {
537
+ const filePath = layout === "dir" ? path3.join(targetDir, skill.id, "SKILL.md") : path3.join(targetDir, `${skill.id}.md`);
538
+ ensureDir(path3.dirname(filePath));
539
+ fs3.writeFileSync(filePath, skill.content, "utf-8");
540
+ }
541
+ saveOwnership2(targetDir, { version: 1, paths: Array.from(newPaths) });
542
+ }
418
543
 
419
544
  // src/adapters/utils/index.ts
420
545
  function getManagedEntryNames(managedIndex, clientId) {
@@ -437,15 +562,35 @@ function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryName
437
562
  }
438
563
  }
439
564
  }
440
- function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
565
+ function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue, expectedEntry) {
441
566
  if (existingValue === undefined || existingValue === null) {
442
567
  return null;
443
568
  }
444
569
  if (isManagedEntry(managedIndex, clientId, entryName)) {
445
570
  return null;
446
571
  }
572
+ if (expectedEntry && isManagedGatewayProjection(entryName) && matchesExpectedShape(existingValue, expectedEntry)) {
573
+ if (!managedIndex.managed[clientId]) {
574
+ managedIndex.managed[clientId] = { configPath: "", entries: {} };
575
+ }
576
+ managedIndex.managed[clientId].entries[entryName] = {
577
+ fingerprint: sha256(JSON.stringify(existingValue)),
578
+ lastSyncedAt: new Date().toISOString()
579
+ };
580
+ return null;
581
+ }
447
582
  return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
448
583
  }
584
+ function matchesExpectedShape(value, expected) {
585
+ if (!value || typeof value !== "object")
586
+ return false;
587
+ const v = value;
588
+ if (expected.url && v.url !== expected.url)
589
+ return false;
590
+ if (expected.command && v.command !== expected.command)
591
+ return false;
592
+ return true;
593
+ }
449
594
  function setManagedEntries(managedIndex, clientId, configPath, entries) {
450
595
  if (!managedIndex.managed[clientId]) {
451
596
  managedIndex.managed[clientId] = {
@@ -547,13 +692,20 @@ function purgeManagedFromExcludedArray(raw, managedNames) {
547
692
  }
548
693
  raw.mcp = Object.keys(mcp).length > 0 ? mcp : undefined;
549
694
  }
695
+ function detectManagedEntryDrift(managedIndex, clientId, entryName, existingValue) {
696
+ const recorded = managedIndex.managed[clientId]?.entries?.[entryName];
697
+ if (!recorded || existingValue === undefined)
698
+ return false;
699
+ const liveFingerprint = sha256(JSON.stringify(existingValue));
700
+ return liveFingerprint !== recorded.fingerprint;
701
+ }
550
702
 
551
703
  // src/adapters/claude.ts
552
704
  function normalizeProjectKey(p) {
553
705
  try {
554
- return fs3.realpathSync(path3.resolve(p));
706
+ return fs4.realpathSync(path4.resolve(p));
555
707
  } catch {
556
- return path3.resolve(p);
708
+ return path4.resolve(p);
557
709
  }
558
710
  }
559
711
  function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
@@ -580,7 +732,7 @@ var claudeEntrySchema = z.object({
580
732
  class ClaudeAdapter {
581
733
  id = "claude";
582
734
  detectConfigPath() {
583
- return path3.join(homeDir(), ".claude.json");
735
+ return path4.join(homeDir(), ".claude.json");
584
736
  }
585
737
  supportsHttp() {
586
738
  return true;
@@ -588,6 +740,9 @@ class ClaudeAdapter {
588
740
  syncPlugins(plugins) {
589
741
  return syncPluginsToClient(this.id, plugins);
590
742
  }
743
+ syncSkills(skills) {
744
+ projectSkillsToDir(path4.join(homeDir(), ".claude", "skills"), skills, "dir");
745
+ }
591
746
  scanForImports(_config, managedIndex) {
592
747
  const configPath = this.detectConfigPath();
593
748
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -668,6 +823,7 @@ class ClaudeAdapter {
668
823
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
669
824
  const topLevelServers = raw.mcpServers ?? {};
670
825
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
826
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
671
827
  for (const name of enabledManagedNames) {
672
828
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
673
829
  if (topLevelConflict) {
@@ -703,7 +859,7 @@ class ClaudeAdapter {
703
859
  raw.projects = projects;
704
860
  writeJsonAtomic(configPath, raw);
705
861
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
706
- return okResult(this.id, configPath);
862
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
707
863
  } catch (error) {
708
864
  return errorResult(this.id, configPath, error.message);
709
865
  }
@@ -712,7 +868,7 @@ class ClaudeAdapter {
712
868
 
713
869
  // src/adapters/claude-desktop.ts
714
870
  import { execFileSync } from "node:child_process";
715
- import path4 from "node:path";
871
+ import path5 from "node:path";
716
872
  import { z as z2 } from "zod";
717
873
  var stringMapSchema2 = z2.record(z2.string(), z2.string());
718
874
  var claudeDesktopEntrySchema = z2.object({
@@ -732,19 +888,19 @@ function buildProxyEntry(entry) {
732
888
  if (mcpxPath)
733
889
  return { command: mcpxPath, args: ["proxy", upstreamName] };
734
890
  } catch {}
735
- return { command: "npx", args: ["-y", "@kwonye/mcpx@latest", "proxy", upstreamName] };
891
+ return { command: "npx", args: ["-y", `@kwonye/mcpx@${APP_VERSION}`, "proxy", upstreamName] };
736
892
  }
737
893
 
738
894
  class ClaudeDesktopAdapter {
739
895
  id = "claude-desktop";
740
896
  detectConfigPath() {
741
897
  if (process.platform === "linux") {
742
- return path4.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
898
+ return path5.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
743
899
  }
744
900
  if (process.platform === "win32") {
745
- return path4.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
901
+ return path5.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
746
902
  }
747
- return path4.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
903
+ return path5.join(homeDir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
748
904
  }
749
905
  supportsHttp() {
750
906
  return true;
@@ -821,6 +977,7 @@ class ClaudeDesktopAdapter {
821
977
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
822
978
  const topLevelServers = raw.mcpServers ?? {};
823
979
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
980
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
824
981
  for (const name of enabledManagedNames) {
825
982
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
826
983
  if (topLevelConflict) {
@@ -837,7 +994,7 @@ class ClaudeDesktopAdapter {
837
994
  const { command, args } = buildProxyEntry(entry);
838
995
  return [entry.name, JSON.stringify({ command, args })];
839
996
  })));
840
- return okResult(this.id, configPath);
997
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
841
998
  } catch (error) {
842
999
  return errorResult(this.id, configPath, error.message);
843
1000
  }
@@ -845,8 +1002,8 @@ class ClaudeDesktopAdapter {
845
1002
  }
846
1003
 
847
1004
  // src/adapters/codex.ts
848
- import path5 from "node:path";
849
- import fs4 from "node:fs";
1005
+ import path6 from "node:path";
1006
+ import fs5 from "node:fs";
850
1007
  import { parse, stringify } from "@iarna/toml";
851
1008
  import { z as z3 } from "zod";
852
1009
  var stringMapSchema3 = z3.preprocess((value) => {
@@ -869,7 +1026,7 @@ var codexEntrySchema = z3.object({
869
1026
  class CodexAdapter {
870
1027
  id = "codex";
871
1028
  detectConfigPath() {
872
- return path5.join(homeDir(), ".codex", "config.toml");
1029
+ return path6.join(homeDir(), ".codex", "config.toml");
873
1030
  }
874
1031
  supportsHttp() {
875
1032
  return true;
@@ -877,13 +1034,16 @@ class CodexAdapter {
877
1034
  syncPlugins(plugins) {
878
1035
  return syncPluginsToClient(this.id, plugins);
879
1036
  }
1037
+ syncSkills(skills) {
1038
+ projectSkillsToDir(path6.join(homeDir(), ".codex", "skills"), skills, "dir");
1039
+ }
880
1040
  scanForImports(_config, managedIndex) {
881
1041
  const configPath = this.detectConfigPath();
882
1042
  const result = emptyImportScan(this.id, configPath ?? undefined);
883
- if (!configPath || !fs4.existsSync(configPath)) {
1043
+ if (!configPath || !fs5.existsSync(configPath)) {
884
1044
  return result;
885
1045
  }
886
- const existing = fs4.readFileSync(configPath, "utf8");
1046
+ const existing = fs5.readFileSync(configPath, "utf8");
887
1047
  const doc = existing.trim().length > 0 ? parse(existing) : {};
888
1048
  const mcpServers = doc.mcp_servers ?? {};
889
1049
  for (const [name, rawEntry] of Object.entries(mcpServers)) {
@@ -938,32 +1098,35 @@ class CodexAdapter {
938
1098
  return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
939
1099
  }
940
1100
  try {
941
- const existing = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf8") : "";
1101
+ const existing = fs5.existsSync(configPath) ? fs5.readFileSync(configPath, "utf8") : "";
942
1102
  const doc = existing.trim().length > 0 ? parse(existing) : {};
943
1103
  const mcpServers = doc.mcp_servers ?? {};
944
1104
  removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
945
1105
  const managedNames = options.managedEntries.map((entry) => entry.name);
946
- const serverEntries = Object.fromEntries(options.managedEntries.filter((entry) => entry.enabled).map((entry) => [entry.name, {
1106
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
1107
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1108
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
947
1109
  url: entry.url,
948
1110
  http_headers: entry.headers
949
1111
  }]));
950
- for (const name of managedNames) {
1112
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcpServers[name]));
1113
+ for (const name of enabledManagedNames) {
951
1114
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
952
1115
  if (conflict) {
953
1116
  return errorResult(this.id, configPath, conflict);
954
1117
  }
955
1118
  }
956
- pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
1119
+ pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, enabledManagedNames);
957
1120
  for (const [name, entry] of Object.entries(serverEntries)) {
958
1121
  mcpServers[name] = entry;
959
1122
  }
960
1123
  doc.mcp_servers = mcpServers;
961
- fs4.mkdirSync(path5.dirname(configPath), { recursive: true });
1124
+ fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
962
1125
  const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
963
- fs4.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
964
- fs4.renameSync(tmpPath, configPath);
1126
+ fs5.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
1127
+ fs5.renameSync(tmpPath, configPath);
965
1128
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
966
- return okResult(this.id, configPath);
1129
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
967
1130
  } catch (error) {
968
1131
  return errorResult(this.id, configPath, error.message);
969
1132
  }
@@ -971,7 +1134,7 @@ class CodexAdapter {
971
1134
  }
972
1135
 
973
1136
  // src/adapters/cursor.ts
974
- import path6 from "node:path";
1137
+ import path7 from "node:path";
975
1138
  import { z as z4 } from "zod";
976
1139
  var stringMapSchema4 = z4.record(z4.string(), z4.string());
977
1140
  var cursorEntrySchema = z4.object({
@@ -988,7 +1151,7 @@ var cursorEntrySchema = z4.object({
988
1151
  class CursorAdapter {
989
1152
  id = "cursor";
990
1153
  detectConfigPath() {
991
- return path6.join(homeDir(), ".cursor", "mcp.json");
1154
+ return path7.join(homeDir(), ".cursor", "mcp.json");
992
1155
  }
993
1156
  supportsHttp() {
994
1157
  return true;
@@ -996,6 +1159,9 @@ class CursorAdapter {
996
1159
  syncPlugins(plugins) {
997
1160
  return syncPluginsToClient(this.id, plugins);
998
1161
  }
1162
+ syncSkills(skills) {
1163
+ projectSkillsToDir(path7.join(homeDir(), ".cursor", "skills"), skills, "dir");
1164
+ }
999
1165
  scanForImports(_config, managedIndex) {
1000
1166
  const configPath = this.detectConfigPath();
1001
1167
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1070,6 +1236,7 @@ class CursorAdapter {
1070
1236
  headers: entry.headers
1071
1237
  }]));
1072
1238
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1239
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1073
1240
  for (const name of enabledManagedNames) {
1074
1241
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1075
1242
  if (conflict) {
@@ -1089,7 +1256,7 @@ class CursorAdapter {
1089
1256
  entry.name,
1090
1257
  JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
1091
1258
  ])));
1092
- return okResult(this.id, configPath);
1259
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1093
1260
  } catch (error) {
1094
1261
  return errorResult(this.id, configPath, error.message);
1095
1262
  }
@@ -1097,8 +1264,8 @@ class CursorAdapter {
1097
1264
  }
1098
1265
 
1099
1266
  // src/adapters/cline.ts
1100
- import path7 from "node:path";
1101
- import fs5 from "node:fs";
1267
+ import path8 from "node:path";
1268
+ import fs6 from "node:fs";
1102
1269
  import { z as z5 } from "zod";
1103
1270
  var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
1104
1271
  var stringMapSchema5 = z5.record(z5.string(), z5.string());
@@ -1116,22 +1283,22 @@ var clineEntrySchema = z5.object({
1116
1283
  function getCandidatePaths() {
1117
1284
  if (process.platform === "linux") {
1118
1285
  return [
1119
- path7.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1120
- path7.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1121
- path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1286
+ path8.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1287
+ path8.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1288
+ path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1122
1289
  ];
1123
1290
  }
1124
1291
  if (process.platform === "win32") {
1125
1292
  return [
1126
- path7.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1127
- path7.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1128
- path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1293
+ path8.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1294
+ path8.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1295
+ path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1129
1296
  ];
1130
1297
  }
1131
1298
  return [
1132
- path7.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1133
- path7.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1134
- path7.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1299
+ path8.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1300
+ path8.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1301
+ path8.join(homeDir(), ".cline", "settings", "cline_mcp_settings.json")
1135
1302
  ];
1136
1303
  }
1137
1304
 
@@ -1140,7 +1307,7 @@ class ClineAdapter {
1140
1307
  detectConfigPath() {
1141
1308
  const candidates = getCandidatePaths();
1142
1309
  for (const candidate of candidates) {
1143
- if (fs5.existsSync(candidate)) {
1310
+ if (fs6.existsSync(candidate)) {
1144
1311
  return candidate;
1145
1312
  }
1146
1313
  }
@@ -1149,10 +1316,16 @@ class ClineAdapter {
1149
1316
  supportsHttp() {
1150
1317
  return true;
1151
1318
  }
1319
+ syncPlugins(plugins) {
1320
+ return syncPluginsToClient(this.id, plugins);
1321
+ }
1322
+ syncSkills(skills) {
1323
+ projectSkillsToDir(path8.join(homeDir(), ".config", "cline", "skills"), skills, "dir");
1324
+ }
1152
1325
  scanForImports(_config, managedIndex) {
1153
1326
  const configPath = this.detectConfigPath();
1154
1327
  const result = emptyImportScan(this.id, configPath ?? undefined);
1155
- if (!configPath || !fs5.existsSync(configPath)) {
1328
+ if (!configPath || !fs6.existsSync(configPath)) {
1156
1329
  return result;
1157
1330
  }
1158
1331
  const raw = readJsonFile(configPath, {});
@@ -1211,7 +1384,7 @@ class ClineAdapter {
1211
1384
  if (!configPath) {
1212
1385
  return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
1213
1386
  }
1214
- if (!fs5.existsSync(configPath)) {
1387
+ if (!fs6.existsSync(configPath)) {
1215
1388
  return okResult(this.id, configPath, "Cline config not found; skipping sync.");
1216
1389
  }
1217
1390
  try {
@@ -1221,18 +1394,21 @@ class ClineAdapter {
1221
1394
  };
1222
1395
  removeSourceEntries(servers, options.sourceEntriesToRemove);
1223
1396
  const managedNames = options.managedEntries.map((entry) => entry.name);
1224
- const serverEntries = Object.fromEntries(options.managedEntries.filter((entry) => entry.enabled).map((entry) => [entry.name, {
1397
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
1398
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1399
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
1225
1400
  type: "streamableHttp",
1226
1401
  url: entry.url,
1227
1402
  headers: entry.headers
1228
1403
  }]));
1229
- for (const name of managedNames) {
1404
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1405
+ for (const name of enabledManagedNames) {
1230
1406
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1231
1407
  if (conflict) {
1232
1408
  return errorResult(this.id, configPath, conflict);
1233
1409
  }
1234
1410
  }
1235
- pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
1411
+ pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
1236
1412
  for (const [name, entry] of Object.entries(serverEntries)) {
1237
1413
  servers[name] = entry;
1238
1414
  }
@@ -1242,7 +1418,7 @@ class ClineAdapter {
1242
1418
  };
1243
1419
  writeJsonAtomic(configPath, next);
1244
1420
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1245
- return okResult(this.id, configPath);
1421
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1246
1422
  } catch (error) {
1247
1423
  return errorResult(this.id, configPath, error.message);
1248
1424
  }
@@ -1250,8 +1426,7 @@ class ClineAdapter {
1250
1426
  }
1251
1427
 
1252
1428
  // src/adapters/kiro.ts
1253
- import fs6 from "node:fs";
1254
- import path8 from "node:path";
1429
+ import path9 from "node:path";
1255
1430
  import { z as z6 } from "zod";
1256
1431
  var stringMapSchema6 = z6.record(z6.string(), z6.string());
1257
1432
  var kiroEntrySchema = z6.object({
@@ -1268,7 +1443,7 @@ var kiroEntrySchema = z6.object({
1268
1443
  class KiroAdapter {
1269
1444
  id = "kiro";
1270
1445
  detectConfigPath() {
1271
- return path8.join(homeDir(), ".kiro", "settings", "mcp.json");
1446
+ return path9.join(homeDir(), ".kiro", "settings", "mcp.json");
1272
1447
  }
1273
1448
  supportsHttp() {
1274
1449
  return true;
@@ -1344,17 +1519,20 @@ class KiroAdapter {
1344
1519
  };
1345
1520
  removeSourceEntries(servers, options.sourceEntriesToRemove);
1346
1521
  const managedNames = options.managedEntries.map((entry) => entry.name);
1347
- const serverEntries = Object.fromEntries(options.managedEntries.filter((entry) => entry.enabled).map((entry) => [entry.name, {
1522
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
1523
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1524
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
1348
1525
  url: entry.url,
1349
1526
  headers: entry.headers
1350
1527
  }]));
1351
- for (const name of managedNames) {
1528
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1529
+ for (const name of enabledManagedNames) {
1352
1530
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1353
1531
  if (conflict) {
1354
1532
  return errorResult(this.id, configPath, conflict);
1355
1533
  }
1356
1534
  }
1357
- pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
1535
+ pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
1358
1536
  for (const [name, entry] of Object.entries(serverEntries)) {
1359
1537
  servers[name] = entry;
1360
1538
  }
@@ -1364,35 +1542,18 @@ class KiroAdapter {
1364
1542
  };
1365
1543
  writeJsonAtomic(configPath, next);
1366
1544
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1367
- return okResult(this.id, configPath);
1545
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1368
1546
  } catch (error) {
1369
1547
  return errorResult(this.id, configPath, error.message);
1370
1548
  }
1371
1549
  }
1372
1550
  syncSkills(skills) {
1373
- const skillsDir = path8.join(homeDir(), ".kiro", "skills");
1374
- if (!fs6.existsSync(skillsDir)) {
1375
- fs6.mkdirSync(skillsDir, { recursive: true });
1376
- }
1377
- const existingFiles = fs6.readdirSync(skillsDir);
1378
- const managedIds = new Set(skills.map((s) => s.id));
1379
- for (const file of existingFiles) {
1380
- if (file.endsWith(".md")) {
1381
- const id = path8.basename(file, ".md");
1382
- if (!managedIds.has(id)) {
1383
- fs6.unlinkSync(path8.join(skillsDir, file));
1384
- }
1385
- }
1386
- }
1387
- for (const skill of skills) {
1388
- const targetPath = path8.join(skillsDir, `${skill.id}.md`);
1389
- fs6.writeFileSync(targetPath, skill.content, "utf-8");
1390
- }
1551
+ projectSkillsToDir(path9.join(homeDir(), ".kiro", "skills"), skills, "flat");
1391
1552
  }
1392
1553
  }
1393
1554
 
1394
1555
  // src/adapters/opencode.ts
1395
- import path9 from "node:path";
1556
+ import path10 from "node:path";
1396
1557
  import { z as z7 } from "zod";
1397
1558
  var stringMapSchema7 = z7.record(z7.string(), z7.string());
1398
1559
  var openCodeEntrySchema = z7.object({
@@ -1410,11 +1571,14 @@ var openCodeEntrySchema = z7.object({
1410
1571
  class OpenCodeAdapter {
1411
1572
  id = "opencode";
1412
1573
  detectConfigPath() {
1413
- return path9.join(homeDir(), ".config", "opencode", "opencode.json");
1574
+ return path10.join(homeDir(), ".config", "opencode", "opencode.json");
1414
1575
  }
1415
1576
  supportsHttp() {
1416
1577
  return true;
1417
1578
  }
1579
+ syncPlugins(plugins) {
1580
+ return syncPluginsToClient(this.id, plugins);
1581
+ }
1418
1582
  scanForImports(_config, managedIndex) {
1419
1583
  const configPath = this.detectConfigPath();
1420
1584
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1488,18 +1652,21 @@ class OpenCodeAdapter {
1488
1652
  };
1489
1653
  removeSourceEntries(mcp, options.sourceEntriesToRemove);
1490
1654
  const managedNames = options.managedEntries.map((entry) => entry.name);
1491
- const serverEntries = Object.fromEntries(options.managedEntries.filter((entry) => entry.enabled).map((entry) => [entry.name, {
1655
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
1656
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1657
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
1492
1658
  type: "remote",
1493
1659
  url: entry.url,
1494
1660
  headers: entry.headers
1495
1661
  }]));
1496
- for (const name of managedNames) {
1662
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, mcp[name]));
1663
+ for (const name of enabledManagedNames) {
1497
1664
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
1498
1665
  if (conflict) {
1499
1666
  return errorResult(this.id, configPath, conflict);
1500
1667
  }
1501
1668
  }
1502
- pruneStaleManagedEntries(options.managedIndex, this.id, mcp, managedNames);
1669
+ pruneStaleManagedEntries(options.managedIndex, this.id, mcp, enabledManagedNames);
1503
1670
  for (const [name, entry] of Object.entries(serverEntries)) {
1504
1671
  mcp[name] = entry;
1505
1672
  }
@@ -1509,7 +1676,7 @@ class OpenCodeAdapter {
1509
1676
  };
1510
1677
  writeJsonAtomic(configPath, next);
1511
1678
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1512
- return okResult(this.id, configPath);
1679
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1513
1680
  } catch (error) {
1514
1681
  return errorResult(this.id, configPath, error.message);
1515
1682
  }
@@ -1517,7 +1684,7 @@ class OpenCodeAdapter {
1517
1684
  }
1518
1685
 
1519
1686
  // src/adapters/vscode.ts
1520
- import path10 from "node:path";
1687
+ import path11 from "node:path";
1521
1688
  import { z as z8 } from "zod";
1522
1689
  var stringMapSchema8 = z8.record(z8.string(), z8.string());
1523
1690
  var vscodeEntrySchema = z8.object({
@@ -1535,12 +1702,12 @@ class VsCodeAdapter {
1535
1702
  id = "vscode";
1536
1703
  detectConfigPath() {
1537
1704
  if (process.platform === "linux") {
1538
- return path10.join(homeDir(), ".config", "Code", "User", "mcp.json");
1705
+ return path11.join(homeDir(), ".config", "Code", "User", "mcp.json");
1539
1706
  }
1540
1707
  if (process.platform === "win32") {
1541
- return path10.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1708
+ return path11.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1542
1709
  }
1543
- return path10.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1710
+ return path11.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1544
1711
  }
1545
1712
  supportsHttp() {
1546
1713
  return true;
@@ -1548,6 +1715,9 @@ class VsCodeAdapter {
1548
1715
  syncPlugins(plugins) {
1549
1716
  return syncPluginsToClient(this.id, plugins);
1550
1717
  }
1718
+ syncSkills(skills) {
1719
+ projectSkillsToDir(path11.join(homeDir(), ".copilot", "skills"), skills, "dir");
1720
+ }
1551
1721
  scanForImports(_config, managedIndex) {
1552
1722
  const configPath = this.detectConfigPath();
1553
1723
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1622,6 +1792,7 @@ class VsCodeAdapter {
1622
1792
  headers: entry.headers
1623
1793
  }]));
1624
1794
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1795
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1625
1796
  for (const name of enabledManagedNames) {
1626
1797
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1627
1798
  if (conflict) {
@@ -1641,7 +1812,7 @@ class VsCodeAdapter {
1641
1812
  entry.name,
1642
1813
  JSON.stringify({ type: "http", url: entry.url, headers: entry.headers })
1643
1814
  ])));
1644
- return okResult(this.id, configPath);
1815
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1645
1816
  } catch (error) {
1646
1817
  return errorResult(this.id, configPath, error.message);
1647
1818
  }
@@ -1649,8 +1820,7 @@ class VsCodeAdapter {
1649
1820
  }
1650
1821
 
1651
1822
  // src/adapters/qwen.ts
1652
- import fs7 from "node:fs";
1653
- import path11 from "node:path";
1823
+ import path12 from "node:path";
1654
1824
  import { z as z9 } from "zod";
1655
1825
  var stringMapSchema9 = z9.record(z9.string(), z9.string());
1656
1826
  var qwenEntrySchema = z9.object({
@@ -1667,7 +1837,7 @@ var qwenEntrySchema = z9.object({
1667
1837
  class QwenAdapter {
1668
1838
  id = "qwen";
1669
1839
  detectConfigPath() {
1670
- return path11.join(homeDir(), ".qwen", "settings.json");
1840
+ return path12.join(homeDir(), ".qwen", "settings.json");
1671
1841
  }
1672
1842
  supportsHttp() {
1673
1843
  return true;
@@ -1753,19 +1923,22 @@ class QwenAdapter {
1753
1923
  try {
1754
1924
  const raw = readJsonFile(configPath, {});
1755
1925
  const managedNames = options.managedEntries.map((entry) => entry.name);
1756
- const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
1926
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
1927
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1928
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
1757
1929
  httpUrl: entry.url,
1758
1930
  headers: entry.headers
1759
1931
  }]));
1760
1932
  const topLevelServers = raw.mcpServers ?? {};
1761
1933
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
1762
- for (const name of managedNames) {
1934
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
1935
+ for (const name of enabledManagedNames) {
1763
1936
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
1764
1937
  if (topLevelConflict) {
1765
1938
  return errorResult(this.id, configPath, topLevelConflict);
1766
1939
  }
1767
1940
  }
1768
- pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, managedNames);
1941
+ pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
1769
1942
  for (const [name, entry] of Object.entries(serverEntries)) {
1770
1943
  topLevelServers[name] = entry;
1771
1944
  }
@@ -1776,29 +1949,291 @@ class QwenAdapter {
1776
1949
  entry.name,
1777
1950
  JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
1778
1951
  ])));
1779
- return okResult(this.id, configPath);
1952
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1780
1953
  } catch (error) {
1781
1954
  return errorResult(this.id, configPath, error.message);
1782
1955
  }
1783
1956
  }
1784
1957
  syncSkills(skills) {
1785
- const skillsDir = path11.join(homeDir(), ".qwen", "skills");
1786
- if (!fs7.existsSync(skillsDir)) {
1787
- fs7.mkdirSync(skillsDir, { recursive: true });
1788
- }
1789
- const existingFiles = fs7.readdirSync(skillsDir);
1790
- const managedIds = new Set(skills.map((s) => s.id));
1791
- for (const file of existingFiles) {
1792
- if (file.endsWith(".md")) {
1793
- const id = path11.basename(file, ".md");
1794
- if (!managedIds.has(id)) {
1795
- fs7.unlinkSync(path11.join(skillsDir, file));
1958
+ projectSkillsToDir(path12.join(homeDir(), ".qwen", "skills"), skills, "flat");
1959
+ }
1960
+ }
1961
+
1962
+ // src/adapters/openclaw.ts
1963
+ import fs7 from "node:fs";
1964
+ import path13 from "node:path";
1965
+ import { z as z10 } from "zod";
1966
+ var stringMapSchema10 = z10.record(z10.string(), z10.string());
1967
+ var openclawEntrySchema = z10.object({
1968
+ url: z10.string().min(1).optional(),
1969
+ transport: z10.string().optional(),
1970
+ headers: stringMapSchema10.optional(),
1971
+ command: z10.string().min(1).optional(),
1972
+ args: z10.array(z10.string()).optional(),
1973
+ env: stringMapSchema10.optional(),
1974
+ cwd: z10.string().min(1).optional(),
1975
+ workingDirectory: z10.string().min(1).optional(),
1976
+ auth: z10.unknown().optional()
1977
+ }).passthrough();
1978
+
1979
+ class OpenClawAdapter {
1980
+ id = "openclaw";
1981
+ detectConfigPath() {
1982
+ return process.env.OPENCLAW_CONFIG_PATH ?? path13.join(homeDir(), ".openclaw", "openclaw.json");
1983
+ }
1984
+ supportsHttp() {
1985
+ return true;
1986
+ }
1987
+ syncPlugins(plugins) {
1988
+ return syncPluginsToClient(this.id, plugins);
1989
+ }
1990
+ syncSkills(skills) {
1991
+ projectSkillsToDir(path13.join(homeDir(), ".openclaw", "skills"), skills, "dir");
1992
+ }
1993
+ scanForImports(_config, managedIndex) {
1994
+ const configPath = this.detectConfigPath();
1995
+ const result = emptyImportScan(this.id, configPath ?? undefined);
1996
+ if (!configPath) {
1997
+ return result;
1998
+ }
1999
+ let raw;
2000
+ try {
2001
+ raw = JSON.parse(fs7.readFileSync(configPath, "utf8"));
2002
+ } catch {
2003
+ return result;
2004
+ }
2005
+ const mcpServers = {};
2006
+ const rootMcp = raw.mcp;
2007
+ if (rootMcp?.servers) {
2008
+ Object.assign(mcpServers, rootMcp.servers);
2009
+ }
2010
+ if (raw.mcpServers) {
2011
+ Object.assign(mcpServers, raw.mcpServers);
2012
+ }
2013
+ for (const [name, rawEntry] of Object.entries(mcpServers)) {
2014
+ if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
2015
+ continue;
2016
+ }
2017
+ const parsed = openclawEntrySchema.safeParse(rawEntry);
2018
+ if (!parsed.success) {
2019
+ result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
2020
+ continue;
2021
+ }
2022
+ const entry = parsed.data;
2023
+ if (entry.auth === "oauth") {
2024
+ result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
2025
+ continue;
2026
+ }
2027
+ if (entry.url && !entry.command) {
2028
+ result.candidates.push({
2029
+ clientId: this.id,
2030
+ configPath,
2031
+ sourceEntryName: name,
2032
+ serverName: name,
2033
+ spec: {
2034
+ transport: "http",
2035
+ url: entry.url,
2036
+ headers: entry.headers
2037
+ }
2038
+ });
2039
+ continue;
2040
+ }
2041
+ if (entry.command && !entry.url) {
2042
+ result.candidates.push({
2043
+ clientId: this.id,
2044
+ configPath,
2045
+ sourceEntryName: name,
2046
+ serverName: name,
2047
+ spec: {
2048
+ transport: "stdio",
2049
+ command: entry.command,
2050
+ args: entry.args,
2051
+ env: entry.env,
2052
+ cwd: entry.workingDirectory ?? entry.cwd
2053
+ }
2054
+ });
2055
+ continue;
2056
+ }
2057
+ result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
2058
+ }
2059
+ return result;
2060
+ }
2061
+ syncGateway(_config, options) {
2062
+ const configPath = this.detectConfigPath();
2063
+ if (!configPath) {
2064
+ return errorResult(this.id, undefined, "Unable to resolve OpenClaw config path.");
2065
+ }
2066
+ try {
2067
+ let raw;
2068
+ if (fs7.existsSync(configPath)) {
2069
+ try {
2070
+ raw = JSON.parse(fs7.readFileSync(configPath, "utf8"));
2071
+ } catch {
2072
+ return errorResult(this.id, configPath, "OpenClaw config contains invalid JSON (possibly JSON5). Cannot safely write.");
2073
+ }
2074
+ } else {
2075
+ raw = {};
2076
+ }
2077
+ const rootMcp = raw.mcp ?? {};
2078
+ const servers = rootMcp.servers ?? {};
2079
+ removeSourceEntries(servers, options.sourceEntriesToRemove);
2080
+ const enabledEntries = options.managedEntries.filter((entry) => entry.enabled);
2081
+ const serverEntries = Object.fromEntries(enabledEntries.map((entry) => [entry.name, {
2082
+ url: entry.url,
2083
+ transport: "streamable-http",
2084
+ headers: entry.headers
2085
+ }]));
2086
+ const enabledManagedNames = enabledEntries.map((entry) => entry.name);
2087
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
2088
+ for (const name of enabledManagedNames) {
2089
+ const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
2090
+ if (conflict) {
2091
+ return errorResult(this.id, configPath, conflict);
1796
2092
  }
1797
2093
  }
2094
+ pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
2095
+ for (const [name, entry] of Object.entries(serverEntries)) {
2096
+ servers[name] = entry;
2097
+ }
2098
+ rootMcp.servers = servers;
2099
+ raw.mcp = rootMcp;
2100
+ writeJsonAtomic(configPath, raw);
2101
+ setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
2102
+ entry.name,
2103
+ JSON.stringify({ url: entry.url, transport: "streamable-http", headers: entry.headers, enabled: entry.enabled })
2104
+ ])));
2105
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
2106
+ } catch (error) {
2107
+ return errorResult(this.id, configPath, error.message);
2108
+ }
2109
+ }
2110
+ }
2111
+
2112
+ // src/adapters/hermes.ts
2113
+ import fs8 from "node:fs";
2114
+ import path14 from "node:path";
2115
+ import YAML from "yaml";
2116
+ import { z as z11 } from "zod";
2117
+ var stringMapSchema11 = z11.record(z11.string(), z11.string());
2118
+ var hermesEntrySchema = z11.object({
2119
+ url: z11.string().min(1).optional(),
2120
+ headers: stringMapSchema11.optional(),
2121
+ command: z11.string().min(1).optional(),
2122
+ args: z11.array(z11.string()).optional(),
2123
+ env: stringMapSchema11.optional(),
2124
+ auth: z11.unknown().optional(),
2125
+ enabled: z11.boolean().optional(),
2126
+ timeout: z11.number().optional(),
2127
+ tools: z11.array(z11.string()).optional()
2128
+ }).passthrough();
2129
+
2130
+ class HermesAdapter {
2131
+ id = "hermes";
2132
+ detectConfigPath() {
2133
+ return path14.join(homeDir(), ".hermes", "config.yaml");
2134
+ }
2135
+ supportsHttp() {
2136
+ return true;
2137
+ }
2138
+ syncPlugins(plugins) {
2139
+ return syncPluginsToClient(this.id, plugins);
2140
+ }
2141
+ syncSkills(skills) {
2142
+ projectSkillsToDir(path14.join(homeDir(), ".hermes", "skills"), skills, "dir");
2143
+ }
2144
+ scanForImports(_config, managedIndex) {
2145
+ const configPath = this.detectConfigPath();
2146
+ const result = emptyImportScan(this.id, configPath ?? undefined);
2147
+ if (!configPath || !fs8.existsSync(configPath)) {
2148
+ return result;
2149
+ }
2150
+ const existing = fs8.readFileSync(configPath, "utf8");
2151
+ const doc = YAML.parse(existing) ?? {};
2152
+ const mcpServers = doc.mcp_servers ?? {};
2153
+ for (const [name, rawEntry] of Object.entries(mcpServers)) {
2154
+ if (isManagedGatewayProjection(name) || managedIndex.managed[this.id]?.entries?.[name]) {
2155
+ continue;
2156
+ }
2157
+ const parsed = hermesEntrySchema.safeParse(rawEntry);
2158
+ if (!parsed.success) {
2159
+ result.skipped.push(buildImportSkip(this.id, name, "Entry could not be mapped to an mcpx server.", configPath));
2160
+ continue;
2161
+ }
2162
+ const entry = parsed.data;
2163
+ if (entry.auth === "oauth") {
2164
+ result.skipped.push(buildImportSkip(this.id, name, "OAuth-authenticated servers cannot be imported via mcpx.", configPath));
2165
+ continue;
2166
+ }
2167
+ if (entry.url && !entry.command) {
2168
+ result.candidates.push({
2169
+ clientId: this.id,
2170
+ configPath,
2171
+ sourceEntryName: name,
2172
+ serverName: name,
2173
+ spec: {
2174
+ transport: "http",
2175
+ url: entry.url,
2176
+ headers: entry.headers,
2177
+ enabled: entry.enabled !== false
2178
+ }
2179
+ });
2180
+ continue;
2181
+ }
2182
+ if (entry.command && !entry.url) {
2183
+ result.candidates.push({
2184
+ clientId: this.id,
2185
+ configPath,
2186
+ sourceEntryName: name,
2187
+ serverName: name,
2188
+ spec: {
2189
+ transport: "stdio",
2190
+ command: entry.command,
2191
+ args: entry.args,
2192
+ env: entry.env,
2193
+ enabled: entry.enabled !== false
2194
+ }
2195
+ });
2196
+ continue;
2197
+ }
2198
+ result.skipped.push(buildImportSkip(this.id, name, "Entry must define exactly one transport that mcpx supports.", configPath));
1798
2199
  }
1799
- for (const skill of skills) {
1800
- const targetPath = path11.join(skillsDir, `${skill.id}.md`);
1801
- fs7.writeFileSync(targetPath, skill.content, "utf-8");
2200
+ return result;
2201
+ }
2202
+ syncGateway(_config, options) {
2203
+ const configPath = this.detectConfigPath();
2204
+ if (!configPath) {
2205
+ return errorResult(this.id, undefined, "Unable to resolve Hermes config.yaml path.");
2206
+ }
2207
+ try {
2208
+ const existing = fs8.existsSync(configPath) ? fs8.readFileSync(configPath, "utf8") : "";
2209
+ const doc = existing.trim().length > 0 ? YAML.parse(existing) ?? {} : {};
2210
+ const mcpServers = doc.mcp_servers ?? {};
2211
+ removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
2212
+ const managedNames = options.managedEntries.map((entry) => entry.name);
2213
+ const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
2214
+ url: entry.url,
2215
+ headers: entry.headers,
2216
+ enabled: entry.enabled
2217
+ }]));
2218
+ for (const name of managedNames) {
2219
+ const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
2220
+ if (conflict) {
2221
+ return errorResult(this.id, configPath, conflict);
2222
+ }
2223
+ }
2224
+ pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
2225
+ for (const [name, entry] of Object.entries(serverEntries)) {
2226
+ mcpServers[name] = entry;
2227
+ }
2228
+ doc.mcp_servers = mcpServers;
2229
+ fs8.mkdirSync(path14.dirname(configPath), { recursive: true });
2230
+ const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
2231
+ fs8.writeFileSync(tmpPath, YAML.stringify(doc), { mode: 384 });
2232
+ fs8.renameSync(tmpPath, configPath);
2233
+ setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
2234
+ return okResult(this.id, configPath);
2235
+ } catch (error) {
2236
+ return errorResult(this.id, configPath, error.message);
1802
2237
  }
1803
2238
  }
1804
2239
  }
@@ -1814,7 +2249,9 @@ function getAdapters() {
1814
2249
  new OpenCodeAdapter,
1815
2250
  new KiroAdapter,
1816
2251
  new VsCodeAdapter,
1817
- new QwenAdapter
2252
+ new QwenAdapter,
2253
+ new OpenClawAdapter,
2254
+ new HermesAdapter
1818
2255
  ];
1819
2256
  }
1820
2257
 
@@ -1839,15 +2276,80 @@ function saveManagedIndex(index, filePath = getManagedIndexPath()) {
1839
2276
  writeJsonAtomic(filePath, index);
1840
2277
  }
1841
2278
 
2279
+ // src/core/managed-index-lock.ts
2280
+ import fs9 from "node:fs";
2281
+ import path15 from "node:path";
2282
+ function isStale(lockPath, maxAgeMs = 5000) {
2283
+ try {
2284
+ const stat = fs9.statSync(lockPath);
2285
+ return Date.now() - stat.mtimeMs > maxAgeMs;
2286
+ } catch {
2287
+ return true;
2288
+ }
2289
+ }
2290
+ function withManagedIndexLock(lockPath, fn) {
2291
+ fs9.mkdirSync(path15.dirname(lockPath), { recursive: true });
2292
+ const deadline = Date.now() + 5000;
2293
+ while (true) {
2294
+ try {
2295
+ fs9.writeFileSync(lockPath, `${process.pid}
2296
+ `, { flag: "wx" });
2297
+ break;
2298
+ } catch {
2299
+ if (isStale(lockPath)) {
2300
+ try {
2301
+ fs9.unlinkSync(lockPath);
2302
+ } catch {}
2303
+ continue;
2304
+ }
2305
+ if (Date.now() > deadline) {
2306
+ throw new Error(`Timed out waiting for managed-index lock: ${lockPath}`);
2307
+ }
2308
+ }
2309
+ }
2310
+ try {
2311
+ return fn();
2312
+ } finally {
2313
+ try {
2314
+ fs9.unlinkSync(lockPath);
2315
+ } catch {}
2316
+ }
2317
+ }
2318
+
1842
2319
  // src/core/sync.ts
1843
2320
  function getGatewayUrl(config) {
1844
2321
  return `http://127.0.0.1:${config.gateway.port}/mcp`;
1845
2322
  }
1846
2323
  function buildProjectScopes(config) {
1847
2324
  const globallyEnabled = new Set(Object.entries(config.servers).filter(([, spec]) => isServerEnabled(spec)).map(([name]) => name));
2325
+ const pluginDisabledByProject = new Map;
2326
+ for (const [, plugin] of Object.entries(config.plugins ?? {})) {
2327
+ if (!plugin.projectOverrides)
2328
+ continue;
2329
+ for (const [overridePath, override] of Object.entries(plugin.projectOverrides)) {
2330
+ const disabled = override.enabled === false || override.components?.mcpServers === false;
2331
+ if (!disabled)
2332
+ continue;
2333
+ if (!pluginDisabledByProject.has(overridePath)) {
2334
+ pluginDisabledByProject.set(overridePath, new Set);
2335
+ }
2336
+ for (const serverName of plugin.serverNames) {
2337
+ pluginDisabledByProject.get(overridePath).add(managedGatewayEntryName(serverName));
2338
+ }
2339
+ }
2340
+ }
1848
2341
  return Object.values(config.projects ?? {}).map((project) => {
2342
+ const projectPath = project.path;
1849
2343
  const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
1850
- return { path: project.path, disabledServerNames };
2344
+ const pluginDisabled = pluginDisabledByProject.get(projectPath);
2345
+ if (pluginDisabled) {
2346
+ for (const name of pluginDisabled) {
2347
+ if (!disabledServerNames.includes(name)) {
2348
+ disabledServerNames.push(name);
2349
+ }
2350
+ }
2351
+ }
2352
+ return { path: projectPath, disabledServerNames };
1851
2353
  });
1852
2354
  }
1853
2355
  function buildManagedEntries(config, gatewayUrl, localToken) {
@@ -2014,62 +2516,64 @@ function runGatewayPhase(adapters, config, managedEntries, managedIndex, managed
2014
2516
  function syncAllClients(config, secrets, options) {
2015
2517
  const opts = Array.isArray(options) ? { targetClients: options, importScan: false } : { importScan: false, ...options };
2016
2518
  const managedIndexPath = getManagedIndexPath();
2017
- const managedIndex = loadManagedIndex(managedIndexPath);
2018
- const adapters = getFilteredAdapters(opts.targetClients);
2019
- const allPhaseErrors = [];
2020
- let imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
2021
- let sourceEntriesToRemove = new Map;
2022
- if (opts.importScan) {
2023
- const importResult = runImportPhase(adapters, config, managedIndex);
2024
- imports = importResult.imports;
2025
- sourceEntriesToRemove = importResult.sourceEntriesToRemove;
2026
- allPhaseErrors.push(...importResult.phaseErrors ?? []);
2027
- }
2028
- const localToken = ensureGatewayToken(config, secrets);
2029
- const gatewayUrl = getGatewayUrl(config);
2030
- const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
2031
- const gatewayResult = runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, buildProjectScopes(config));
2032
- const results = gatewayResult.results;
2033
- allPhaseErrors.push(...gatewayResult.phaseErrors ?? []);
2034
- const skills = listSkills();
2035
- const skillsErrors = runSkillsPhase(adapters, skills);
2036
- allPhaseErrors.push(...skillsErrors ?? []);
2037
- const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
2038
- pluginId: p.id,
2039
- pluginName: p.name,
2040
- pluginRoot: p.root,
2041
- components: p.components,
2042
- approvals: p.approvals ?? {},
2043
- enabled: p.enabled,
2044
- serverNames: p.serverNames,
2045
- skills: p.discovered.skills,
2046
- commands: p.discovered.commands,
2047
- agents: p.discovered.agents,
2048
- hooks: p.discovered.hooks
2049
- }));
2050
- const pluginResult = runPluginsPhase(adapters, pluginInputs);
2051
- const pluginResults = pluginResult.pluginResults;
2052
- allPhaseErrors.push(...pluginResult.phaseErrors ?? []);
2053
- saveManagedIndex(managedIndex, managedIndexPath);
2054
- const clientStates = {};
2055
- for (const result of results) {
2056
- clientStates[result.clientId] = {
2057
- status: result.status,
2058
- configPath: result.configPath,
2059
- message: result.message,
2060
- lastSyncAt: new Date().toISOString()
2519
+ return withManagedIndexLock(`${managedIndexPath}.lock`, () => {
2520
+ const managedIndex = loadManagedIndex(managedIndexPath);
2521
+ const adapters = getFilteredAdapters(opts.targetClients);
2522
+ const allPhaseErrors = [];
2523
+ let imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
2524
+ let sourceEntriesToRemove = new Map;
2525
+ if (opts.importScan) {
2526
+ const importResult = runImportPhase(adapters, config, managedIndex);
2527
+ imports = importResult.imports;
2528
+ sourceEntriesToRemove = importResult.sourceEntriesToRemove;
2529
+ allPhaseErrors.push(...importResult.phaseErrors ?? []);
2530
+ }
2531
+ const localToken = ensureGatewayToken(config, secrets);
2532
+ const gatewayUrl = getGatewayUrl(config);
2533
+ const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
2534
+ const gatewayResult = runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, buildProjectScopes(config));
2535
+ const results = gatewayResult.results;
2536
+ allPhaseErrors.push(...gatewayResult.phaseErrors ?? []);
2537
+ const skills = listSkills();
2538
+ const skillsErrors = runSkillsPhase(adapters, skills);
2539
+ allPhaseErrors.push(...skillsErrors ?? []);
2540
+ const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
2541
+ pluginId: p.id,
2542
+ pluginName: p.name,
2543
+ pluginRoot: p.root,
2544
+ components: p.components,
2545
+ approvals: p.approvals ?? {},
2546
+ enabled: p.enabled,
2547
+ serverNames: p.serverNames,
2548
+ skills: p.discovered.skills,
2549
+ commands: p.discovered.commands,
2550
+ agents: p.discovered.agents,
2551
+ hooks: p.discovered.hooks
2552
+ }));
2553
+ const pluginResult = runPluginsPhase(adapters, pluginInputs);
2554
+ const pluginResults = pluginResult.pluginResults;
2555
+ allPhaseErrors.push(...pluginResult.phaseErrors ?? []);
2556
+ saveManagedIndex(managedIndex, managedIndexPath);
2557
+ const clientStates = {};
2558
+ for (const result of results) {
2559
+ clientStates[result.clientId] = {
2560
+ status: result.status,
2561
+ configPath: result.configPath,
2562
+ message: result.message,
2563
+ lastSyncAt: new Date().toISOString()
2564
+ };
2565
+ }
2566
+ const hasErrors = results.some((r) => r.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
2567
+ return {
2568
+ gatewayUrl,
2569
+ imports,
2570
+ results,
2571
+ hasErrors,
2572
+ clientStates,
2573
+ phaseErrors: allPhaseErrors.length > 0 ? allPhaseErrors : undefined,
2574
+ pluginResults: pluginResults.length > 0 ? pluginResults : undefined
2061
2575
  };
2062
- }
2063
- const hasErrors = results.some((r) => r.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
2064
- return {
2065
- gatewayUrl,
2066
- imports,
2067
- results,
2068
- hasErrors,
2069
- clientStates,
2070
- phaseErrors: allPhaseErrors.length > 0 ? allPhaseErrors : undefined,
2071
- pluginResults: pluginResults.length > 0 ? pluginResults : undefined
2072
- };
2576
+ });
2073
2577
  }
2074
2578
  function persistSyncState(summary, config) {
2075
2579
  if (!summary.clientStates)
@@ -2079,4 +2583,4 @@ function persistSyncState(summary, config) {
2079
2583
  }
2080
2584
  }
2081
2585
 
2082
- export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };
2586
+ export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, APP_VERSION, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };