@kwonye/mcpx 0.1.93 → 0.1.95

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 (80) 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 +18 -4
  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-y7ba5qvr.js → cli-28fsxhe3.js} +430 -156
  24. package/dist/{cli-qvkpwy9w.js → cli-njtbb11m.js} +138 -43
  25. package/dist/{cli-8vxva0cb.js → cli-tk8bggg5.js} +81 -8
  26. package/dist/{cli-np0wwh6m.js → cli-v27bt25d.js} +157 -11
  27. package/dist/{cli-kwyp9ywh.js → cli-wxw1t3rr.js} +941 -303
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +388 -77
  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-2s5nsm2g.js → config-5p08cb0c.js} +6 -2
  36. package/dist/core/add-command.d.ts.map +1 -1
  37. package/dist/core/config-repair.d.ts.map +1 -1
  38. package/dist/core/config-store.d.ts +33 -0
  39. package/dist/core/config-store.d.ts.map +1 -0
  40. package/dist/core/config.d.ts +13 -1
  41. package/dist/core/config.d.ts.map +1 -1
  42. package/dist/core/daemon.d.ts +5 -1
  43. package/dist/core/daemon.d.ts.map +1 -1
  44. package/dist/core/errors.d.ts +18 -0
  45. package/dist/core/errors.d.ts.map +1 -0
  46. package/dist/core/index.d.ts +6 -3
  47. package/dist/core/index.d.ts.map +1 -1
  48. package/dist/core/index.js +30 -6
  49. package/dist/core/managed-index-lock.d.ts +2 -0
  50. package/dist/core/managed-index-lock.d.ts.map +1 -0
  51. package/dist/core/oauth.d.ts.map +1 -1
  52. package/dist/core/plugin-host.d.ts +2 -0
  53. package/dist/core/plugin-host.d.ts.map +1 -0
  54. package/dist/core/plugin-manager.d.ts +19 -6
  55. package/dist/core/plugin-manager.d.ts.map +1 -1
  56. package/dist/core/plugin-parse.d.ts.map +1 -1
  57. package/dist/core/plugin-projections.d.ts +6 -2
  58. package/dist/core/plugin-projections.d.ts.map +1 -1
  59. package/dist/core/registry.d.ts +4 -1
  60. package/dist/core/registry.d.ts.map +1 -1
  61. package/dist/core/secrets.d.ts.map +1 -1
  62. package/dist/core/server-auth.d.ts.map +1 -1
  63. package/dist/core/skill-projections.d.ts +3 -0
  64. package/dist/core/skill-projections.d.ts.map +1 -0
  65. package/dist/core/status.d.ts +1 -0
  66. package/dist/core/status.d.ts.map +1 -1
  67. package/dist/core/sync.d.ts +19 -2
  68. package/dist/core/sync.d.ts.map +1 -1
  69. package/dist/core/update-manager.d.ts.map +1 -1
  70. package/dist/gateway/server.d.ts +4 -1
  71. package/dist/gateway/server.d.ts.map +1 -1
  72. package/dist/plugin-host-954a7tf7.js +129 -0
  73. package/dist/{plugin-manager-1sc4b6am.js → plugin-manager-m8ar2271.js} +12 -4
  74. package/dist/{registry-mztfy0cz.js → registry-n12thgf2.js} +1 -1
  75. package/dist/types.d.ts +5 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/dist/version.d.ts +1 -1
  78. package/dist/version.d.ts.map +1 -1
  79. package/package.json +3 -2
  80. package/dist/cli-m2645syd.js +0 -2199
@@ -1,9 +1,6 @@
1
- import {
2
- saveConfig
3
- } from "./cli-np0wwh6m.js";
4
1
  import {
5
2
  ensureGatewayToken
6
- } from "./cli-8vxva0cb.js";
3
+ } from "./cli-tk8bggg5.js";
7
4
  import {
8
5
  ensureDir,
9
6
  getManagedIndexPath,
@@ -71,14 +68,15 @@ function loadOwnership(targetDir) {
71
68
  try {
72
69
  return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
73
70
  } catch {
71
+ console.error(`[mcpx] Warning: corrupt plugin ownership manifest at ${manifestPath}, resetting`);
74
72
  return { version: 1, plugins: {} };
75
73
  }
76
74
  }
77
75
  function saveOwnership(targetDir, manifest) {
76
+ ensureDir(targetDir);
78
77
  fs2.writeFileSync(path2.join(targetDir, OWNERSHIP_MANIFEST), JSON.stringify(manifest, null, 2));
79
78
  }
80
79
  function recordOwned(targetDir, pluginId, paths) {
81
- ensureDir(targetDir);
82
80
  const manifest = loadOwnership(targetDir);
83
81
  manifest.plugins[pluginId] = {
84
82
  pluginId,
@@ -114,16 +112,49 @@ function pruneAllPluginProjections(targetDir) {
114
112
  prunePluginProjections(targetDir, pluginId);
115
113
  }
116
114
  }
115
+ function pruneUnlistedPlugins(targetDir, keepPluginIds) {
116
+ const manifest = loadOwnership(targetDir);
117
+ const keep = new Set(keepPluginIds);
118
+ for (const [pluginId] of Object.entries(manifest.plugins)) {
119
+ if (!keep.has(pluginId)) {
120
+ prunePluginProjections(targetDir, pluginId);
121
+ }
122
+ }
123
+ }
117
124
  function nsName(pluginName, id) {
118
125
  return `${pluginName}__${id}`;
119
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
+ }
134
+ function isComponentApproved(plugin, component) {
135
+ const approval = plugin.approvals?.[component];
136
+ if (approval === false)
137
+ return false;
138
+ if (approval === true)
139
+ return true;
140
+ if (component === "hooks")
141
+ return false;
142
+ if (component === "mcpServers")
143
+ return plugin.components.mcpServers;
144
+ return plugin.components[component] ?? false;
145
+ }
120
146
  function syncClaude(plugins) {
121
147
  const targetBase = path2.join(homeDir(), ".claude", "skills");
122
148
  const projectedDirs = [];
123
149
  const unsupported = [];
150
+ const skippedUnapproved = [];
124
151
  for (const plugin of plugins) {
125
152
  if (!plugin.enabled)
126
153
  continue;
154
+ if (!isComponentApproved(plugin, "skills")) {
155
+ skippedUnapproved.push(`${plugin.pluginName}:skills`);
156
+ continue;
157
+ }
127
158
  const targetDir = path2.join(targetBase, plugin.pluginName);
128
159
  ensureDir(targetDir);
129
160
  const mcpJsonSrc = path2.join(plugin.pluginRoot, ".mcp.json");
@@ -155,25 +186,34 @@ function syncCodex(plugins) {
155
186
  const targetBase = path2.join(homeDir(), ".codex", "skills");
156
187
  const projectedDirs = [];
157
188
  const unsupported = ["hooks", "agents"];
189
+ const errors = [];
158
190
  for (const plugin of plugins) {
159
- if (!plugin.enabled || !plugin.components.skills)
191
+ if (!plugin.enabled)
160
192
  continue;
161
- ensureDir(targetBase);
162
- const owned = [];
163
- for (const skill of plugin.skills) {
164
- const targetDir = path2.join(targetBase, nsName(plugin.pluginName, skill.id));
165
- ensureDir(targetDir);
166
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
167
- owned.push(nsName(plugin.pluginName, skill.id));
168
- projectedDirs.push(targetDir);
193
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
194
+ ensureDir(targetBase);
195
+ const owned = [];
196
+ for (const skill of plugin.skills) {
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
+ }
207
+ }
208
+ recordOwned(targetBase, plugin.pluginId, owned);
169
209
  }
170
- recordOwned(targetBase, plugin.pluginId, owned);
171
210
  }
172
211
  return {
173
212
  clientId: "codex",
174
213
  status: "SYNCED",
175
214
  projectedDirs,
176
- unsupported
215
+ unsupported,
216
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
177
217
  };
178
218
  }
179
219
  function syncCursor(plugins) {
@@ -181,28 +221,39 @@ function syncCursor(plugins) {
181
221
  const commandsBase = path2.join(homeDir(), ".cursor", "commands");
182
222
  const projectedDirs = [];
183
223
  const unsupported = ["hooks", "agents"];
184
- const ownedSkills = [];
185
- const ownedCommands = [];
224
+ const errors = [];
186
225
  for (const plugin of plugins) {
187
226
  if (!plugin.enabled)
188
227
  continue;
189
- if (plugin.components.skills) {
228
+ const ownedSkills = [];
229
+ const ownedCommands = [];
230
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
190
231
  ensureDir(skillsBase);
191
232
  for (const skill of plugin.skills) {
192
- const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
193
- ensureDir(targetDir);
194
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
195
- ownedSkills.push(nsName(plugin.pluginName, skill.id));
196
- 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
+ }
197
243
  }
198
244
  }
199
- if (plugin.components.commands) {
245
+ if (plugin.components.commands && isComponentApproved(plugin, "commands")) {
200
246
  ensureDir(commandsBase);
201
247
  for (const cmd of plugin.commands) {
202
- const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
203
- copyFileOrDir(cmd.path, targetPath);
204
- ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
205
- 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
+ }
206
257
  }
207
258
  }
208
259
  if (ownedSkills.length > 0)
@@ -214,151 +265,281 @@ function syncCursor(plugins) {
214
265
  clientId: "cursor",
215
266
  status: "SYNCED",
216
267
  projectedDirs,
217
- unsupported
268
+ unsupported,
269
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
218
270
  };
219
271
  }
220
272
  function syncVsCode(plugins) {
221
273
  const skillsBase = path2.join(homeDir(), ".copilot", "skills");
222
274
  const projectedDirs = [];
223
275
  const unsupported = ["hooks", "agents"];
276
+ const errors = [];
224
277
  for (const plugin of plugins) {
225
- if (!plugin.enabled || !plugin.components.skills)
278
+ if (!plugin.enabled)
226
279
  continue;
227
- ensureDir(skillsBase);
228
- const owned = [];
229
- for (const skill of plugin.skills) {
230
- const targetDir = path2.join(skillsBase, nsName(plugin.pluginName, skill.id));
231
- ensureDir(targetDir);
232
- copyFileOrDir(skill.path, path2.join(targetDir, "SKILL.md"));
233
- owned.push(nsName(plugin.pluginName, skill.id));
234
- projectedDirs.push(targetDir);
280
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
281
+ ensureDir(skillsBase);
282
+ const owned = [];
283
+ for (const skill of plugin.skills) {
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
+ }
294
+ }
295
+ recordOwned(skillsBase, plugin.pluginId, owned);
235
296
  }
236
- recordOwned(skillsBase, plugin.pluginId, owned);
237
297
  }
238
298
  return {
239
299
  clientId: "vscode",
240
300
  status: "SYNCED",
241
301
  projectedDirs,
242
- unsupported
302
+ unsupported,
303
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
243
304
  };
244
305
  }
245
- function syncKiro(plugins) {
246
- const skillsBase = path2.join(homeDir(), ".kiro", "skills");
306
+ function syncQwen(plugins) {
307
+ const targetBase = path2.join(homeDir(), ".qwen", "skills");
247
308
  const projectedDirs = [];
248
309
  const unsupported = ["hooks", "agents", "commands"];
310
+ const errors = [];
249
311
  for (const plugin of plugins) {
250
- if (!plugin.enabled || !plugin.components.skills)
312
+ if (!plugin.enabled)
251
313
  continue;
252
- ensureDir(skillsBase);
253
- const owned = [];
254
- for (const skill of plugin.skills) {
255
- const targetPath = path2.join(skillsBase, `${nsName(plugin.pluginName, skill.id)}.md`);
256
- copyFileOrDir(skill.path, targetPath);
257
- owned.push(`${nsName(plugin.pluginName, skill.id)}.md`);
258
- projectedDirs.push(targetPath);
314
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
315
+ ensureDir(targetBase);
316
+ const owned = [];
317
+ for (const skill of plugin.skills) {
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
+ }
328
+ }
329
+ recordOwned(targetBase, plugin.pluginId, owned);
259
330
  }
260
- recordOwned(skillsBase, plugin.pluginId, owned);
261
331
  }
262
332
  return {
263
- clientId: "kiro",
333
+ clientId: "qwen",
264
334
  status: "SYNCED",
265
335
  projectedDirs,
266
- unsupported
336
+ unsupported,
337
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
267
338
  };
268
339
  }
269
- function syncQwen(plugins) {
270
- const skillsBase = path2.join(homeDir(), ".qwen", "skills");
271
- const commandsBase = path2.join(homeDir(), ".qwen", "commands");
340
+ function syncCline(plugins) {
341
+ const targetBase = path2.join(homeDir(), ".config", "cline", "skills");
272
342
  const projectedDirs = [];
273
- const unsupported = ["hooks", "agents"];
274
- const ownedSkills = [];
275
- const ownedCommands = [];
343
+ const unsupported = ["hooks", "agents", "commands"];
344
+ const errors = [];
276
345
  for (const plugin of plugins) {
277
346
  if (!plugin.enabled)
278
347
  continue;
279
- if (plugin.components.skills) {
280
- ensureDir(skillsBase);
348
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
349
+ ensureDir(targetBase);
350
+ const owned = [];
281
351
  for (const skill of plugin.skills) {
282
- const targetPath = path2.join(skillsBase, `${nsName(plugin.pluginName, skill.id)}.md`);
283
- copyFileOrDir(skill.path, targetPath);
284
- ownedSkills.push(`${nsName(plugin.pluginName, skill.id)}.md`);
285
- projectedDirs.push(targetPath);
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
+ }
286
362
  }
363
+ recordOwned(targetBase, plugin.pluginId, owned);
287
364
  }
288
- if (plugin.components.commands) {
289
- ensureDir(commandsBase);
290
- for (const cmd of plugin.commands) {
291
- const targetPath = path2.join(commandsBase, `${nsName(plugin.pluginName, cmd.id)}.md`);
292
- copyFileOrDir(cmd.path, targetPath);
293
- ownedCommands.push(`${nsName(plugin.pluginName, cmd.id)}.md`);
294
- projectedDirs.push(targetPath);
365
+ }
366
+ return {
367
+ clientId: "cline",
368
+ status: "SYNCED",
369
+ projectedDirs,
370
+ unsupported,
371
+ ...errors.length > 0 ? { error: errors.join("; ") } : {}
372
+ };
373
+ }
374
+ function syncKiro(plugins) {
375
+ const targetBase = path2.join(homeDir(), ".kiro", "skills");
376
+ const projectedDirs = [];
377
+ const unsupported = ["hooks", "agents", "commands"];
378
+ const errors = [];
379
+ for (const plugin of plugins) {
380
+ if (!plugin.enabled)
381
+ continue;
382
+ if (plugin.components.skills && isComponentApproved(plugin, "skills")) {
383
+ ensureDir(targetBase);
384
+ const owned = [];
385
+ for (const skill of plugin.skills) {
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
+ }
295
396
  }
397
+ recordOwned(targetBase, plugin.pluginId, owned);
296
398
  }
297
- if (ownedSkills.length > 0)
298
- recordOwned(skillsBase, plugin.pluginId, ownedSkills);
299
- if (ownedCommands.length > 0)
300
- recordOwned(commandsBase, plugin.pluginId, ownedCommands);
301
399
  }
302
400
  return {
303
- clientId: "qwen",
401
+ clientId: "kiro",
304
402
  status: "SYNCED",
305
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: [],
306
414
  unsupported
307
415
  };
308
416
  }
309
- var CLIENT_SYNCERS = {
310
- claude: syncClaude,
311
- "claude-desktop": syncClaudeDesktop,
312
- codex: syncCodex,
313
- cursor: syncCursor,
314
- vscode: syncVsCode,
315
- kiro: syncKiro,
316
- qwen: syncQwen
317
- };
318
417
  function syncPluginsToClient(clientId, plugins) {
319
- const syncer = CLIENT_SYNCERS[clientId];
320
- if (!syncer) {
321
- return {
322
- clientId,
323
- status: "SKIPPED",
324
- projectedDirs: [],
325
- unsupported: ["mcpServers", "skills", "hooks", "agents", "commands"],
326
- error: `No plugin sync implementation for ${clientId}`
327
- };
418
+ const syncMap = {
419
+ claude: syncClaude,
420
+ "claude-desktop": syncClaudeDesktop,
421
+ codex: syncCodex,
422
+ cursor: syncCursor,
423
+ vscode: syncVsCode,
424
+ qwen: syncQwen,
425
+ cline: syncCline,
426
+ kiro: syncKiro,
427
+ opencode: syncOpenCode
428
+ };
429
+ const syncFn = syncMap[clientId];
430
+ if (!syncFn) {
431
+ return { clientId, status: "SKIPPED", projectedDirs: [], unsupported: [] };
432
+ }
433
+ const targetBaseMap = {
434
+ claude: path2.join(homeDir(), ".claude", "skills"),
435
+ "claude-desktop": "",
436
+ codex: path2.join(homeDir(), ".codex", "skills"),
437
+ cursor: path2.join(homeDir(), ".cursor"),
438
+ vscode: path2.join(homeDir(), ".copilot", "skills"),
439
+ qwen: path2.join(homeDir(), ".qwen", "skills"),
440
+ cline: path2.join(homeDir(), ".config", "cline", "skills"),
441
+ kiro: path2.join(homeDir(), ".kiro", "skills"),
442
+ opencode: ""
443
+ };
444
+ const target = targetBaseMap[clientId];
445
+ if (target) {
446
+ const enabledIds = plugins.filter((p) => p.enabled).map((p) => p.pluginId);
447
+ pruneUnlistedPlugins(target, enabledIds);
328
448
  }
329
- return syncer(plugins);
449
+ return syncFn(plugins);
330
450
  }
331
- function copyPluginDir(src, dest, exclude = []) {
451
+ function copyPluginDir(src, dest, exclude = [], errors) {
452
+ const excludeSet = new Set(exclude);
332
453
  ensureDir(dest);
333
454
  const entries = fs2.readdirSync(src, { withFileTypes: true });
334
455
  for (const entry of entries) {
335
- if (exclude.includes(entry.name))
336
- continue;
337
- if (entry.name.startsWith(".") && entry.name !== ".claude-plugin")
456
+ if (excludeSet.has(entry.name))
338
457
  continue;
339
458
  const srcPath = path2.join(src, entry.name);
340
459
  const destPath = path2.join(dest, entry.name);
341
460
  if (entry.isDirectory()) {
342
- copyPluginDir(srcPath, destPath, exclude);
343
- } else {
344
- fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
345
- fs2.copyFileSync(srcPath, destPath);
461
+ copyPluginDir(srcPath, destPath, exclude, errors);
462
+ } else if (entry.isFile()) {
463
+ try {
464
+ fs2.copyFileSync(srcPath, destPath);
465
+ } catch (e) {
466
+ errors?.push(`Failed to copy ${srcPath}: ${e.message}`);
467
+ }
346
468
  }
347
469
  }
348
470
  }
349
- function copyFileOrDir(src, dest) {
350
- ensureDir(path2.dirname(dest));
351
- if (fs2.statSync(src).isDirectory()) {
352
- copyPluginDir(src, dest);
353
- } else {
354
- fs2.copyFileSync(src, dest);
471
+ function copyFileOrDir(src, dest, errors) {
472
+ try {
473
+ ensureDir(path2.dirname(dest));
474
+ if (fs2.statSync(src).isDirectory()) {
475
+ copyPluginDir(src, dest, [], errors);
476
+ } else {
477
+ fs2.copyFileSync(src, dest);
478
+ }
479
+ } catch (e) {
480
+ errors?.push(`Failed to copy ${src} -> ${dest}: ${e.message}`);
355
481
  }
356
482
  }
357
483
 
484
+ // src/version.ts
485
+ var APP_VERSION = "0.1.3";
486
+
358
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
359
493
  import fs3 from "node:fs";
360
494
  import path3 from "node:path";
361
- 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
+ }
362
543
 
363
544
  // src/adapters/utils/index.ts
364
545
  function getManagedEntryNames(managedIndex, clientId) {
@@ -381,15 +562,35 @@ function pruneStaleManagedEntries(managedIndex, clientId, servers, keepEntryName
381
562
  }
382
563
  }
383
564
  }
384
- function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue) {
565
+ function ensureManagedEntryWritable(managedIndex, clientId, entryName, existingValue, expectedEntry) {
385
566
  if (existingValue === undefined || existingValue === null) {
386
567
  return null;
387
568
  }
388
569
  if (isManagedEntry(managedIndex, clientId, entryName)) {
389
570
  return null;
390
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
+ }
391
582
  return `Cannot sync managed entry "${entryName}" because an unmanaged entry already exists.`;
392
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
+ }
393
594
  function setManagedEntries(managedIndex, clientId, configPath, entries) {
394
595
  if (!managedIndex.managed[clientId]) {
395
596
  managedIndex.managed[clientId] = {
@@ -475,25 +676,36 @@ function removeSourceEntries(entries, names) {
475
676
  delete entries[name];
476
677
  }
477
678
  }
478
- function syncDisabledMcpServersArray(raw, managedNames, managedEntries) {
479
- const existingDisabled = (raw.disabledMcpServers ?? []).filter((name) => !managedNames.includes(name));
480
- const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
481
- raw.disabledMcpServers = [...existingDisabled, ...disabledManaged];
679
+ function purgeManagedFromDisabledArray(raw, managedNames) {
680
+ const existingDisabled = raw.disabledMcpServers ?? [];
681
+ raw.disabledMcpServers = existingDisabled.filter((name) => !managedNames.includes(name));
682
+ if (Array.isArray(raw.disabledMcpServers) && raw.disabledMcpServers.length === 0) {
683
+ delete raw.disabledMcpServers;
684
+ }
482
685
  }
483
- function syncMcpExcludedArray(raw, managedNames, managedEntries) {
686
+ function purgeManagedFromExcludedArray(raw, managedNames) {
484
687
  const mcp = raw.mcp ?? {};
485
- const existingExcluded = (mcp.excluded ?? []).filter((name) => !managedNames.includes(name));
486
- const disabledManaged = managedEntries.filter((entry) => !entry.enabled).map((entry) => entry.name);
487
- mcp.excluded = [...existingExcluded, ...disabledManaged];
488
- raw.mcp = mcp;
688
+ const existingExcluded = mcp.excluded ?? [];
689
+ mcp.excluded = existingExcluded.filter((name) => !managedNames.includes(name));
690
+ if (Array.isArray(mcp.excluded) && mcp.excluded.length === 0) {
691
+ delete mcp.excluded;
692
+ }
693
+ raw.mcp = Object.keys(mcp).length > 0 ? mcp : undefined;
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;
489
701
  }
490
702
 
491
703
  // src/adapters/claude.ts
492
704
  function normalizeProjectKey(p) {
493
705
  try {
494
- return fs3.realpathSync(path3.resolve(p));
706
+ return fs4.realpathSync(path4.resolve(p));
495
707
  } catch {
496
- return path3.resolve(p);
708
+ return path4.resolve(p);
497
709
  }
498
710
  }
499
711
  function syncProjectDisabledArray(project, managedNames, disabledForThisProject) {
@@ -520,7 +732,7 @@ var claudeEntrySchema = z.object({
520
732
  class ClaudeAdapter {
521
733
  id = "claude";
522
734
  detectConfigPath() {
523
- return path3.join(homeDir(), ".claude.json");
735
+ return path4.join(homeDir(), ".claude.json");
524
736
  }
525
737
  supportsHttp() {
526
738
  return true;
@@ -528,6 +740,9 @@ class ClaudeAdapter {
528
740
  syncPlugins(plugins) {
529
741
  return syncPluginsToClient(this.id, plugins);
530
742
  }
743
+ syncSkills(skills) {
744
+ projectSkillsToDir(path4.join(homeDir(), ".claude", "skills"), skills, "dir");
745
+ }
531
746
  scanForImports(_config, managedIndex) {
532
747
  const configPath = this.detectConfigPath();
533
748
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -608,6 +823,7 @@ class ClaudeAdapter {
608
823
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
609
824
  const topLevelServers = raw.mcpServers ?? {};
610
825
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
826
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
611
827
  for (const name of enabledManagedNames) {
612
828
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
613
829
  if (topLevelConflict) {
@@ -619,7 +835,7 @@ class ClaudeAdapter {
619
835
  topLevelServers[name] = entry;
620
836
  }
621
837
  raw.mcpServers = topLevelServers;
622
- syncDisabledMcpServersArray(raw, managedNames, options.managedEntries);
838
+ purgeManagedFromDisabledArray(raw, managedNames);
623
839
  const scopeByNormalizedPath = new Map;
624
840
  for (const scope of options.projectScopes ?? []) {
625
841
  scopeByNormalizedPath.set(normalizeProjectKey(scope.path), scope.disabledServerNames);
@@ -643,7 +859,7 @@ class ClaudeAdapter {
643
859
  raw.projects = projects;
644
860
  writeJsonAtomic(configPath, raw);
645
861
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
646
- return okResult(this.id, configPath);
862
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
647
863
  } catch (error) {
648
864
  return errorResult(this.id, configPath, error.message);
649
865
  }
@@ -652,7 +868,7 @@ class ClaudeAdapter {
652
868
 
653
869
  // src/adapters/claude-desktop.ts
654
870
  import { execFileSync } from "node:child_process";
655
- import path4 from "node:path";
871
+ import path5 from "node:path";
656
872
  import { z as z2 } from "zod";
657
873
  var stringMapSchema2 = z2.record(z2.string(), z2.string());
658
874
  var claudeDesktopEntrySchema = z2.object({
@@ -672,19 +888,19 @@ function buildProxyEntry(entry) {
672
888
  if (mcpxPath)
673
889
  return { command: mcpxPath, args: ["proxy", upstreamName] };
674
890
  } catch {}
675
- return { command: "npx", args: ["-y", "@kwonye/mcpx@latest", "proxy", upstreamName] };
891
+ return { command: "npx", args: ["-y", `@kwonye/mcpx@${APP_VERSION}`, "proxy", upstreamName] };
676
892
  }
677
893
 
678
894
  class ClaudeDesktopAdapter {
679
895
  id = "claude-desktop";
680
896
  detectConfigPath() {
681
897
  if (process.platform === "linux") {
682
- return path4.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
898
+ return path5.join(homeDir(), ".config", "Claude", "claude_desktop_config.json");
683
899
  }
684
900
  if (process.platform === "win32") {
685
- return path4.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
901
+ return path5.join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json");
686
902
  }
687
- 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");
688
904
  }
689
905
  supportsHttp() {
690
906
  return true;
@@ -761,6 +977,7 @@ class ClaudeDesktopAdapter {
761
977
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
762
978
  const topLevelServers = raw.mcpServers ?? {};
763
979
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
980
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, topLevelServers[name]));
764
981
  for (const name of enabledManagedNames) {
765
982
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
766
983
  if (topLevelConflict) {
@@ -777,7 +994,7 @@ class ClaudeDesktopAdapter {
777
994
  const { command, args } = buildProxyEntry(entry);
778
995
  return [entry.name, JSON.stringify({ command, args })];
779
996
  })));
780
- return okResult(this.id, configPath);
997
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
781
998
  } catch (error) {
782
999
  return errorResult(this.id, configPath, error.message);
783
1000
  }
@@ -785,8 +1002,8 @@ class ClaudeDesktopAdapter {
785
1002
  }
786
1003
 
787
1004
  // src/adapters/codex.ts
788
- import path5 from "node:path";
789
- import fs4 from "node:fs";
1005
+ import path6 from "node:path";
1006
+ import fs5 from "node:fs";
790
1007
  import { parse, stringify } from "@iarna/toml";
791
1008
  import { z as z3 } from "zod";
792
1009
  var stringMapSchema3 = z3.preprocess((value) => {
@@ -809,7 +1026,7 @@ var codexEntrySchema = z3.object({
809
1026
  class CodexAdapter {
810
1027
  id = "codex";
811
1028
  detectConfigPath() {
812
- return path5.join(homeDir(), ".codex", "config.toml");
1029
+ return path6.join(homeDir(), ".codex", "config.toml");
813
1030
  }
814
1031
  supportsHttp() {
815
1032
  return true;
@@ -817,13 +1034,16 @@ class CodexAdapter {
817
1034
  syncPlugins(plugins) {
818
1035
  return syncPluginsToClient(this.id, plugins);
819
1036
  }
1037
+ syncSkills(skills) {
1038
+ projectSkillsToDir(path6.join(homeDir(), ".codex", "skills"), skills, "dir");
1039
+ }
820
1040
  scanForImports(_config, managedIndex) {
821
1041
  const configPath = this.detectConfigPath();
822
1042
  const result = emptyImportScan(this.id, configPath ?? undefined);
823
- if (!configPath || !fs4.existsSync(configPath)) {
1043
+ if (!configPath || !fs5.existsSync(configPath)) {
824
1044
  return result;
825
1045
  }
826
- const existing = fs4.readFileSync(configPath, "utf8");
1046
+ const existing = fs5.readFileSync(configPath, "utf8");
827
1047
  const doc = existing.trim().length > 0 ? parse(existing) : {};
828
1048
  const mcpServers = doc.mcp_servers ?? {};
829
1049
  for (const [name, rawEntry] of Object.entries(mcpServers)) {
@@ -878,33 +1098,35 @@ class CodexAdapter {
878
1098
  return errorResult(this.id, undefined, "Unable to resolve Codex config.toml path.");
879
1099
  }
880
1100
  try {
881
- const existing = fs4.existsSync(configPath) ? fs4.readFileSync(configPath, "utf8") : "";
1101
+ const existing = fs5.existsSync(configPath) ? fs5.readFileSync(configPath, "utf8") : "";
882
1102
  const doc = existing.trim().length > 0 ? parse(existing) : {};
883
1103
  const mcpServers = doc.mcp_servers ?? {};
884
1104
  removeSourceEntries(mcpServers, options.sourceEntriesToRemove);
885
1105
  const managedNames = options.managedEntries.map((entry) => entry.name);
886
- const serverEntries = Object.fromEntries(options.managedEntries.map((entry) => [entry.name, {
887
- enabled: entry.enabled,
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, {
888
1109
  url: entry.url,
889
1110
  http_headers: entry.headers
890
1111
  }]));
891
- 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) {
892
1114
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcpServers[name]);
893
1115
  if (conflict) {
894
1116
  return errorResult(this.id, configPath, conflict);
895
1117
  }
896
1118
  }
897
- pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, managedNames);
1119
+ pruneStaleManagedEntries(options.managedIndex, this.id, mcpServers, enabledManagedNames);
898
1120
  for (const [name, entry] of Object.entries(serverEntries)) {
899
1121
  mcpServers[name] = entry;
900
1122
  }
901
1123
  doc.mcp_servers = mcpServers;
902
- fs4.mkdirSync(path5.dirname(configPath), { recursive: true });
1124
+ fs5.mkdirSync(path6.dirname(configPath), { recursive: true });
903
1125
  const tmpPath = `${configPath}.tmp-${process.pid}-${Date.now()}`;
904
- fs4.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
905
- fs4.renameSync(tmpPath, configPath);
1126
+ fs5.writeFileSync(tmpPath, stringify(doc), { mode: 384 });
1127
+ fs5.renameSync(tmpPath, configPath);
906
1128
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
907
- return okResult(this.id, configPath);
1129
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
908
1130
  } catch (error) {
909
1131
  return errorResult(this.id, configPath, error.message);
910
1132
  }
@@ -912,7 +1134,7 @@ class CodexAdapter {
912
1134
  }
913
1135
 
914
1136
  // src/adapters/cursor.ts
915
- import path6 from "node:path";
1137
+ import path7 from "node:path";
916
1138
  import { z as z4 } from "zod";
917
1139
  var stringMapSchema4 = z4.record(z4.string(), z4.string());
918
1140
  var cursorEntrySchema = z4.object({
@@ -929,7 +1151,7 @@ var cursorEntrySchema = z4.object({
929
1151
  class CursorAdapter {
930
1152
  id = "cursor";
931
1153
  detectConfigPath() {
932
- return path6.join(homeDir(), ".cursor", "mcp.json");
1154
+ return path7.join(homeDir(), ".cursor", "mcp.json");
933
1155
  }
934
1156
  supportsHttp() {
935
1157
  return true;
@@ -937,6 +1159,9 @@ class CursorAdapter {
937
1159
  syncPlugins(plugins) {
938
1160
  return syncPluginsToClient(this.id, plugins);
939
1161
  }
1162
+ syncSkills(skills) {
1163
+ projectSkillsToDir(path7.join(homeDir(), ".cursor", "skills"), skills, "dir");
1164
+ }
940
1165
  scanForImports(_config, managedIndex) {
941
1166
  const configPath = this.detectConfigPath();
942
1167
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1011,6 +1236,7 @@ class CursorAdapter {
1011
1236
  headers: entry.headers
1012
1237
  }]));
1013
1238
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1239
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1014
1240
  for (const name of enabledManagedNames) {
1015
1241
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1016
1242
  if (conflict) {
@@ -1030,7 +1256,7 @@ class CursorAdapter {
1030
1256
  entry.name,
1031
1257
  JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
1032
1258
  ])));
1033
- return okResult(this.id, configPath);
1259
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1034
1260
  } catch (error) {
1035
1261
  return errorResult(this.id, configPath, error.message);
1036
1262
  }
@@ -1038,8 +1264,8 @@ class CursorAdapter {
1038
1264
  }
1039
1265
 
1040
1266
  // src/adapters/cline.ts
1041
- import path7 from "node:path";
1042
- import fs5 from "node:fs";
1267
+ import path8 from "node:path";
1268
+ import fs6 from "node:fs";
1043
1269
  import { z as z5 } from "zod";
1044
1270
  var HTTP_TRANSPORT_TYPES = new Set(["http", "sse", "streamableHttp"]);
1045
1271
  var stringMapSchema5 = z5.record(z5.string(), z5.string());
@@ -1057,22 +1283,22 @@ var clineEntrySchema = z5.object({
1057
1283
  function getCandidatePaths() {
1058
1284
  if (process.platform === "linux") {
1059
1285
  return [
1060
- path7.join(homeDir(), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1061
- path7.join(homeDir(), ".config", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1062
- 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")
1063
1289
  ];
1064
1290
  }
1065
1291
  if (process.platform === "win32") {
1066
1292
  return [
1067
- path7.join(process.env.APPDATA ?? "", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1068
- path7.join(process.env.APPDATA ?? "", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1069
- 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")
1070
1296
  ];
1071
1297
  }
1072
1298
  return [
1073
- path7.join(homeDir(), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1074
- path7.join(homeDir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
1075
- 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")
1076
1302
  ];
1077
1303
  }
1078
1304
 
@@ -1081,7 +1307,7 @@ class ClineAdapter {
1081
1307
  detectConfigPath() {
1082
1308
  const candidates = getCandidatePaths();
1083
1309
  for (const candidate of candidates) {
1084
- if (fs5.existsSync(candidate)) {
1310
+ if (fs6.existsSync(candidate)) {
1085
1311
  return candidate;
1086
1312
  }
1087
1313
  }
@@ -1090,10 +1316,16 @@ class ClineAdapter {
1090
1316
  supportsHttp() {
1091
1317
  return true;
1092
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
+ }
1093
1325
  scanForImports(_config, managedIndex) {
1094
1326
  const configPath = this.detectConfigPath();
1095
1327
  const result = emptyImportScan(this.id, configPath ?? undefined);
1096
- if (!configPath || !fs5.existsSync(configPath)) {
1328
+ if (!configPath || !fs6.existsSync(configPath)) {
1097
1329
  return result;
1098
1330
  }
1099
1331
  const raw = readJsonFile(configPath, {});
@@ -1152,7 +1384,7 @@ class ClineAdapter {
1152
1384
  if (!configPath) {
1153
1385
  return errorResult(this.id, undefined, "Unable to resolve Cline MCP config path.");
1154
1386
  }
1155
- if (!fs5.existsSync(configPath)) {
1387
+ if (!fs6.existsSync(configPath)) {
1156
1388
  return okResult(this.id, configPath, "Cline config not found; skipping sync.");
1157
1389
  }
1158
1390
  try {
@@ -1162,19 +1394,21 @@ class ClineAdapter {
1162
1394
  };
1163
1395
  removeSourceEntries(servers, options.sourceEntriesToRemove);
1164
1396
  const managedNames = options.managedEntries.map((entry) => entry.name);
1165
- const serverEntries = Object.fromEntries(options.managedEntries.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, {
1166
1400
  type: "streamableHttp",
1167
1401
  url: entry.url,
1168
- headers: entry.headers,
1169
- disabled: !entry.enabled
1402
+ headers: entry.headers
1170
1403
  }]));
1171
- 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) {
1172
1406
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1173
1407
  if (conflict) {
1174
1408
  return errorResult(this.id, configPath, conflict);
1175
1409
  }
1176
1410
  }
1177
- pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
1411
+ pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
1178
1412
  for (const [name, entry] of Object.entries(serverEntries)) {
1179
1413
  servers[name] = entry;
1180
1414
  }
@@ -1184,7 +1418,7 @@ class ClineAdapter {
1184
1418
  };
1185
1419
  writeJsonAtomic(configPath, next);
1186
1420
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1187
- return okResult(this.id, configPath);
1421
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1188
1422
  } catch (error) {
1189
1423
  return errorResult(this.id, configPath, error.message);
1190
1424
  }
@@ -1192,8 +1426,7 @@ class ClineAdapter {
1192
1426
  }
1193
1427
 
1194
1428
  // src/adapters/kiro.ts
1195
- import fs6 from "node:fs";
1196
- import path8 from "node:path";
1429
+ import path9 from "node:path";
1197
1430
  import { z as z6 } from "zod";
1198
1431
  var stringMapSchema6 = z6.record(z6.string(), z6.string());
1199
1432
  var kiroEntrySchema = z6.object({
@@ -1210,7 +1443,7 @@ var kiroEntrySchema = z6.object({
1210
1443
  class KiroAdapter {
1211
1444
  id = "kiro";
1212
1445
  detectConfigPath() {
1213
- return path8.join(homeDir(), ".kiro", "settings", "mcp.json");
1446
+ return path9.join(homeDir(), ".kiro", "settings", "mcp.json");
1214
1447
  }
1215
1448
  supportsHttp() {
1216
1449
  return true;
@@ -1286,18 +1519,20 @@ class KiroAdapter {
1286
1519
  };
1287
1520
  removeSourceEntries(servers, options.sourceEntriesToRemove);
1288
1521
  const managedNames = options.managedEntries.map((entry) => entry.name);
1289
- const serverEntries = Object.fromEntries(options.managedEntries.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, {
1290
1525
  url: entry.url,
1291
- headers: entry.headers,
1292
- disabled: !entry.enabled
1526
+ headers: entry.headers
1293
1527
  }]));
1294
- 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) {
1295
1530
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1296
1531
  if (conflict) {
1297
1532
  return errorResult(this.id, configPath, conflict);
1298
1533
  }
1299
1534
  }
1300
- pruneStaleManagedEntries(options.managedIndex, this.id, servers, managedNames);
1535
+ pruneStaleManagedEntries(options.managedIndex, this.id, servers, enabledManagedNames);
1301
1536
  for (const [name, entry] of Object.entries(serverEntries)) {
1302
1537
  servers[name] = entry;
1303
1538
  }
@@ -1307,35 +1542,18 @@ class KiroAdapter {
1307
1542
  };
1308
1543
  writeJsonAtomic(configPath, next);
1309
1544
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1310
- return okResult(this.id, configPath);
1545
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1311
1546
  } catch (error) {
1312
1547
  return errorResult(this.id, configPath, error.message);
1313
1548
  }
1314
1549
  }
1315
1550
  syncSkills(skills) {
1316
- const skillsDir = path8.join(homeDir(), ".kiro", "skills");
1317
- if (!fs6.existsSync(skillsDir)) {
1318
- fs6.mkdirSync(skillsDir, { recursive: true });
1319
- }
1320
- const existingFiles = fs6.readdirSync(skillsDir);
1321
- const managedIds = new Set(skills.map((s) => s.id));
1322
- for (const file of existingFiles) {
1323
- if (file.endsWith(".md")) {
1324
- const id = path8.basename(file, ".md");
1325
- if (!managedIds.has(id)) {
1326
- fs6.unlinkSync(path8.join(skillsDir, file));
1327
- }
1328
- }
1329
- }
1330
- for (const skill of skills) {
1331
- const targetPath = path8.join(skillsDir, `${skill.id}.md`);
1332
- fs6.writeFileSync(targetPath, skill.content, "utf-8");
1333
- }
1551
+ projectSkillsToDir(path9.join(homeDir(), ".kiro", "skills"), skills, "flat");
1334
1552
  }
1335
1553
  }
1336
1554
 
1337
1555
  // src/adapters/opencode.ts
1338
- import path9 from "node:path";
1556
+ import path10 from "node:path";
1339
1557
  import { z as z7 } from "zod";
1340
1558
  var stringMapSchema7 = z7.record(z7.string(), z7.string());
1341
1559
  var openCodeEntrySchema = z7.object({
@@ -1353,11 +1571,14 @@ var openCodeEntrySchema = z7.object({
1353
1571
  class OpenCodeAdapter {
1354
1572
  id = "opencode";
1355
1573
  detectConfigPath() {
1356
- return path9.join(homeDir(), ".config", "opencode", "opencode.json");
1574
+ return path10.join(homeDir(), ".config", "opencode", "opencode.json");
1357
1575
  }
1358
1576
  supportsHttp() {
1359
1577
  return true;
1360
1578
  }
1579
+ syncPlugins(plugins) {
1580
+ return syncPluginsToClient(this.id, plugins);
1581
+ }
1361
1582
  scanForImports(_config, managedIndex) {
1362
1583
  const configPath = this.detectConfigPath();
1363
1584
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1431,19 +1652,21 @@ class OpenCodeAdapter {
1431
1652
  };
1432
1653
  removeSourceEntries(mcp, options.sourceEntriesToRemove);
1433
1654
  const managedNames = options.managedEntries.map((entry) => entry.name);
1434
- const serverEntries = Object.fromEntries(options.managedEntries.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, {
1435
1658
  type: "remote",
1436
1659
  url: entry.url,
1437
- headers: entry.headers,
1438
- enabled: entry.enabled
1660
+ headers: entry.headers
1439
1661
  }]));
1440
- 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) {
1441
1664
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, mcp[name]);
1442
1665
  if (conflict) {
1443
1666
  return errorResult(this.id, configPath, conflict);
1444
1667
  }
1445
1668
  }
1446
- pruneStaleManagedEntries(options.managedIndex, this.id, mcp, managedNames);
1669
+ pruneStaleManagedEntries(options.managedIndex, this.id, mcp, enabledManagedNames);
1447
1670
  for (const [name, entry] of Object.entries(serverEntries)) {
1448
1671
  mcp[name] = entry;
1449
1672
  }
@@ -1453,7 +1676,7 @@ class OpenCodeAdapter {
1453
1676
  };
1454
1677
  writeJsonAtomic(configPath, next);
1455
1678
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(Object.entries(serverEntries).map(([name, entry]) => [name, JSON.stringify(entry)])));
1456
- return okResult(this.id, configPath);
1679
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1457
1680
  } catch (error) {
1458
1681
  return errorResult(this.id, configPath, error.message);
1459
1682
  }
@@ -1461,7 +1684,7 @@ class OpenCodeAdapter {
1461
1684
  }
1462
1685
 
1463
1686
  // src/adapters/vscode.ts
1464
- import path10 from "node:path";
1687
+ import path11 from "node:path";
1465
1688
  import { z as z8 } from "zod";
1466
1689
  var stringMapSchema8 = z8.record(z8.string(), z8.string());
1467
1690
  var vscodeEntrySchema = z8.object({
@@ -1479,12 +1702,12 @@ class VsCodeAdapter {
1479
1702
  id = "vscode";
1480
1703
  detectConfigPath() {
1481
1704
  if (process.platform === "linux") {
1482
- return path10.join(homeDir(), ".config", "Code", "User", "mcp.json");
1705
+ return path11.join(homeDir(), ".config", "Code", "User", "mcp.json");
1483
1706
  }
1484
1707
  if (process.platform === "win32") {
1485
- return path10.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1708
+ return path11.join(process.env.APPDATA ?? "", "Code", "User", "mcp.json");
1486
1709
  }
1487
- return path10.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1710
+ return path11.join(homeDir(), "Library", "Application Support", "Code", "User", "mcp.json");
1488
1711
  }
1489
1712
  supportsHttp() {
1490
1713
  return true;
@@ -1492,6 +1715,9 @@ class VsCodeAdapter {
1492
1715
  syncPlugins(plugins) {
1493
1716
  return syncPluginsToClient(this.id, plugins);
1494
1717
  }
1718
+ syncSkills(skills) {
1719
+ projectSkillsToDir(path11.join(homeDir(), ".copilot", "skills"), skills, "dir");
1720
+ }
1495
1721
  scanForImports(_config, managedIndex) {
1496
1722
  const configPath = this.detectConfigPath();
1497
1723
  const result = emptyImportScan(this.id, configPath ?? undefined);
@@ -1566,6 +1792,7 @@ class VsCodeAdapter {
1566
1792
  headers: entry.headers
1567
1793
  }]));
1568
1794
  const enabledManagedNames = enabledEntries.map((entry) => entry.name);
1795
+ const driftedNames = enabledManagedNames.filter((name) => detectManagedEntryDrift(options.managedIndex, this.id, name, servers[name]));
1569
1796
  for (const name of enabledManagedNames) {
1570
1797
  const conflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, servers[name]);
1571
1798
  if (conflict) {
@@ -1581,11 +1808,11 @@ class VsCodeAdapter {
1581
1808
  servers
1582
1809
  };
1583
1810
  writeJsonAtomic(configPath, next);
1584
- setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
1811
+ setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(enabledEntries.map((entry) => [
1585
1812
  entry.name,
1586
- JSON.stringify({ type: "http", url: entry.url, headers: entry.headers, enabled: entry.enabled })
1813
+ JSON.stringify({ type: "http", url: entry.url, headers: entry.headers })
1587
1814
  ])));
1588
- return okResult(this.id, configPath);
1815
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1589
1816
  } catch (error) {
1590
1817
  return errorResult(this.id, configPath, error.message);
1591
1818
  }
@@ -1593,8 +1820,7 @@ class VsCodeAdapter {
1593
1820
  }
1594
1821
 
1595
1822
  // src/adapters/qwen.ts
1596
- import fs7 from "node:fs";
1597
- import path11 from "node:path";
1823
+ import path12 from "node:path";
1598
1824
  import { z as z9 } from "zod";
1599
1825
  var stringMapSchema9 = z9.record(z9.string(), z9.string());
1600
1826
  var qwenEntrySchema = z9.object({
@@ -1611,7 +1837,7 @@ var qwenEntrySchema = z9.object({
1611
1837
  class QwenAdapter {
1612
1838
  id = "qwen";
1613
1839
  detectConfigPath() {
1614
- return path11.join(homeDir(), ".qwen", "settings.json");
1840
+ return path12.join(homeDir(), ".qwen", "settings.json");
1615
1841
  }
1616
1842
  supportsHttp() {
1617
1843
  return true;
@@ -1697,52 +1923,317 @@ class QwenAdapter {
1697
1923
  try {
1698
1924
  const raw = readJsonFile(configPath, {});
1699
1925
  const managedNames = options.managedEntries.map((entry) => entry.name);
1700
- 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, {
1701
1929
  httpUrl: entry.url,
1702
1930
  headers: entry.headers
1703
1931
  }]));
1704
1932
  const topLevelServers = raw.mcpServers ?? {};
1705
1933
  removeSourceEntries(topLevelServers, options.sourceEntriesToRemove);
1706
- 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) {
1707
1936
  const topLevelConflict = ensureManagedEntryWritable(options.managedIndex, this.id, name, topLevelServers[name]);
1708
1937
  if (topLevelConflict) {
1709
1938
  return errorResult(this.id, configPath, topLevelConflict);
1710
1939
  }
1711
1940
  }
1712
- pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, managedNames);
1941
+ pruneStaleManagedEntries(options.managedIndex, this.id, topLevelServers, enabledManagedNames);
1713
1942
  for (const [name, entry] of Object.entries(serverEntries)) {
1714
1943
  topLevelServers[name] = entry;
1715
1944
  }
1716
1945
  raw.mcpServers = topLevelServers;
1717
- syncMcpExcludedArray(raw, managedNames, options.managedEntries);
1946
+ purgeManagedFromExcludedArray(raw, managedNames);
1718
1947
  writeJsonAtomic(configPath, raw);
1719
1948
  setManagedEntries(options.managedIndex, this.id, configPath, Object.fromEntries(options.managedEntries.map((entry) => [
1720
1949
  entry.name,
1721
1950
  JSON.stringify({ httpUrl: entry.url, headers: entry.headers, enabled: entry.enabled })
1722
1951
  ])));
1723
- return okResult(this.id, configPath);
1952
+ return { ...okResult(this.id, configPath), driftedEntries: driftedNames.length > 0 ? driftedNames : undefined };
1724
1953
  } catch (error) {
1725
1954
  return errorResult(this.id, configPath, error.message);
1726
1955
  }
1727
1956
  }
1728
1957
  syncSkills(skills) {
1729
- const skillsDir = path11.join(homeDir(), ".qwen", "skills");
1730
- if (!fs7.existsSync(skillsDir)) {
1731
- fs7.mkdirSync(skillsDir, { recursive: true });
1732
- }
1733
- const existingFiles = fs7.readdirSync(skillsDir);
1734
- const managedIds = new Set(skills.map((s) => s.id));
1735
- for (const file of existingFiles) {
1736
- if (file.endsWith(".md")) {
1737
- const id = path11.basename(file, ".md");
1738
- if (!managedIds.has(id)) {
1739
- 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.");
1740
2073
  }
2074
+ } else {
2075
+ raw = {};
1741
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);
2092
+ }
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);
1742
2108
  }
1743
- for (const skill of skills) {
1744
- const targetPath = path11.join(skillsDir, `${skill.id}.md`);
1745
- fs7.writeFileSync(targetPath, skill.content, "utf-8");
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));
2199
+ }
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);
1746
2237
  }
1747
2238
  }
1748
2239
  }
@@ -1758,7 +2249,9 @@ function getAdapters() {
1758
2249
  new OpenCodeAdapter,
1759
2250
  new KiroAdapter,
1760
2251
  new VsCodeAdapter,
1761
- new QwenAdapter
2252
+ new QwenAdapter,
2253
+ new OpenClawAdapter,
2254
+ new HermesAdapter
1762
2255
  ];
1763
2256
  }
1764
2257
 
@@ -1783,45 +2276,105 @@ function saveManagedIndex(index, filePath = getManagedIndexPath()) {
1783
2276
  writeJsonAtomic(filePath, index);
1784
2277
  }
1785
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
+
1786
2319
  // src/core/sync.ts
1787
2320
  function getGatewayUrl(config) {
1788
2321
  return `http://127.0.0.1:${config.gateway.port}/mcp`;
1789
2322
  }
1790
2323
  function buildProjectScopes(config) {
1791
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
+ }
1792
2341
  return Object.values(config.projects ?? {}).map((project) => {
2342
+ const projectPath = project.path;
1793
2343
  const disabledServerNames = (project.disabledServers ?? []).filter((name) => globallyEnabled.has(name)).map((name) => managedGatewayEntryName(name));
1794
- 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 };
1795
2353
  });
1796
2354
  }
1797
2355
  function buildManagedEntries(config, gatewayUrl, localToken) {
1798
- const entries = Object.entries(config.servers);
1799
- return entries.map(([name, spec]) => ({
2356
+ return Object.entries(config.servers).map(([name, spec]) => ({
1800
2357
  name: `${name} (mcpx)`,
1801
2358
  url: `${gatewayUrl}?upstream=${encodeURIComponent(name)}`,
1802
- headers: {
1803
- Authorization: `Bearer ${localToken}`
1804
- },
2359
+ headers: { Authorization: `Bearer ${localToken}` },
1805
2360
  enabled: isServerEnabled(spec)
1806
2361
  }));
1807
2362
  }
1808
2363
  function isClientInternalServerCommand(candidate) {
1809
2364
  return candidate.spec.transport === "stdio" && /\.app\/Contents\//.test(candidate.spec.command ?? "");
1810
2365
  }
1811
- function syncAllClients(config, secrets, targetClients) {
2366
+ function getFilteredAdapters(targetClients) {
1812
2367
  const adapters = getAdapters();
1813
- const managedIndexPath = getManagedIndexPath();
1814
- const managedIndex = loadManagedIndex(managedIndexPath);
1815
- const filteredAdapters = targetClients && targetClients.length > 0 ? adapters.filter((adapter) => targetClients.includes(adapter.id)) : adapters;
1816
- const imports = {
1817
- imported: [],
1818
- duplicates: [],
1819
- skipped: [],
1820
- conflicts: [],
1821
- errors: []
1822
- };
2368
+ if (targetClients && targetClients.length > 0) {
2369
+ return adapters.filter((a) => targetClients.includes(a.id));
2370
+ }
2371
+ return adapters;
2372
+ }
2373
+ function runImportPhase(adapters, config, managedIndex) {
2374
+ const imports = { imported: [], duplicates: [], skipped: [], conflicts: [], errors: [] };
1823
2375
  const sourceEntriesToRemove = new Map;
1824
- for (const adapter of filteredAdapters) {
2376
+ const phaseErrors = [];
2377
+ for (const adapter of adapters) {
1825
2378
  try {
1826
2379
  const scan = adapter.scanForImports(config, managedIndex);
1827
2380
  imports.skipped.push(...scan.skipped);
@@ -1848,7 +2401,7 @@ function syncAllClients(config, secrets, targetClients) {
1848
2401
  if (!sourceEntriesToRemove.has(candidate.clientId)) {
1849
2402
  sourceEntriesToRemove.set(candidate.clientId, new Set);
1850
2403
  }
1851
- sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
2404
+ sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
1852
2405
  continue;
1853
2406
  }
1854
2407
  if (serverSpecsEqual(existing, candidate.spec)) {
@@ -1861,7 +2414,7 @@ function syncAllClients(config, secrets, targetClients) {
1861
2414
  if (!sourceEntriesToRemove.has(candidate.clientId)) {
1862
2415
  sourceEntriesToRemove.set(candidate.clientId, new Set);
1863
2416
  }
1864
- sourceEntriesToRemove.get(candidate.clientId)?.add(candidate.sourceEntryName);
2417
+ sourceEntriesToRemove.get(candidate.clientId).add(candidate.sourceEntryName);
1865
2418
  continue;
1866
2419
  }
1867
2420
  imports.conflicts.push({
@@ -1878,37 +2431,56 @@ function syncAllClients(config, secrets, targetClients) {
1878
2431
  configPath: adapter.detectConfigPath() ?? undefined,
1879
2432
  message: error instanceof Error ? error.message : String(error)
1880
2433
  });
2434
+ phaseErrors.push({
2435
+ clientId: adapter.id,
2436
+ phase: "import",
2437
+ message: error instanceof Error ? error.message : String(error)
2438
+ });
1881
2439
  }
1882
2440
  }
1883
- const localToken = ensureGatewayToken(config, secrets);
1884
- const gatewayUrl = getGatewayUrl(config);
1885
- const managedEntries = buildManagedEntries(config, gatewayUrl, localToken);
1886
- const skills = listSkills();
1887
- const results = [];
1888
- const pluginInputs = Object.values(config.plugins ?? {}).map((p) => ({
1889
- pluginId: p.id,
1890
- pluginName: p.name,
1891
- pluginRoot: p.root,
1892
- components: p.components,
1893
- approvals: p.approvals ?? {},
1894
- enabled: p.enabled,
1895
- serverNames: p.serverNames,
1896
- skills: p.discovered.skills,
1897
- commands: p.discovered.commands,
1898
- agents: p.discovered.agents,
1899
- hooks: p.discovered.hooks
1900
- }));
1901
- for (const adapter of filteredAdapters) {
2441
+ return { imports, sourceEntriesToRemove, phaseErrors };
2442
+ }
2443
+ function runSkillsPhase(adapters, skills) {
2444
+ const phaseErrors = [];
2445
+ for (const adapter of adapters) {
1902
2446
  if (adapter.syncSkills) {
1903
2447
  try {
1904
2448
  adapter.syncSkills(skills);
1905
- } catch (error) {}
2449
+ } catch (error) {
2450
+ phaseErrors.push({
2451
+ clientId: adapter.id,
2452
+ phase: "skills",
2453
+ message: error instanceof Error ? error.message : String(error)
2454
+ });
2455
+ }
1906
2456
  }
1907
- if (adapter.syncPlugins && pluginInputs.length > 0) {
2457
+ }
2458
+ return phaseErrors;
2459
+ }
2460
+ function runPluginsPhase(adapters, pluginInputs) {
2461
+ const pluginResults = [];
2462
+ const phaseErrors = [];
2463
+ for (const adapter of adapters) {
2464
+ if (adapter.syncPlugins) {
1908
2465
  try {
1909
- adapter.syncPlugins(pluginInputs);
1910
- } catch (error) {}
2466
+ const result = adapter.syncPlugins(pluginInputs);
2467
+ if (result)
2468
+ pluginResults.push(result);
2469
+ } catch (error) {
2470
+ phaseErrors.push({
2471
+ clientId: adapter.id,
2472
+ phase: "plugins",
2473
+ message: error instanceof Error ? error.message : String(error)
2474
+ });
2475
+ }
1911
2476
  }
2477
+ }
2478
+ return { pluginResults, phaseErrors };
2479
+ }
2480
+ function runGatewayPhase(adapters, config, managedEntries, managedIndex, managedIndexPath, sourceEntriesToRemove, projectScopes) {
2481
+ const results = [];
2482
+ const phaseErrors = [];
2483
+ for (const adapter of adapters) {
1912
2484
  if (!adapter.supportsHttp()) {
1913
2485
  results.push({
1914
2486
  clientId: adapter.id,
@@ -1917,32 +2489,98 @@ function syncAllClients(config, secrets, targetClients) {
1917
2489
  });
1918
2490
  continue;
1919
2491
  }
1920
- const result = adapter.syncGateway(config, {
1921
- managedEntries,
1922
- managedIndex,
1923
- managedIndexPath,
1924
- sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
1925
- projectScopes: buildProjectScopes(config)
1926
- });
1927
- results.push(result);
1928
- }
1929
- for (const result of results) {
1930
- config.clients[result.clientId] = {
1931
- status: result.status,
1932
- configPath: result.configPath,
1933
- message: result.message,
1934
- lastSyncAt: new Date().toISOString()
2492
+ try {
2493
+ const result = adapter.syncGateway(config, {
2494
+ managedEntries,
2495
+ managedIndex,
2496
+ managedIndexPath,
2497
+ sourceEntriesToRemove: Array.from(sourceEntriesToRemove.get(adapter.id) ?? []),
2498
+ projectScopes
2499
+ });
2500
+ results.push(result);
2501
+ } catch (error) {
2502
+ results.push({
2503
+ clientId: adapter.id,
2504
+ status: "ERROR",
2505
+ message: error instanceof Error ? error.message : String(error)
2506
+ });
2507
+ phaseErrors.push({
2508
+ clientId: adapter.id,
2509
+ phase: "gateway",
2510
+ message: error instanceof Error ? error.message : String(error)
2511
+ });
2512
+ }
2513
+ }
2514
+ return { results, phaseErrors };
2515
+ }
2516
+ function syncAllClients(config, secrets, options) {
2517
+ const opts = Array.isArray(options) ? { targetClients: options, importScan: false } : { importScan: false, ...options };
2518
+ const managedIndexPath = getManagedIndexPath();
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
1935
2575
  };
2576
+ });
2577
+ }
2578
+ function persistSyncState(summary, config) {
2579
+ if (!summary.clientStates)
2580
+ return;
2581
+ for (const [clientId, state] of Object.entries(summary.clientStates)) {
2582
+ config.clients[clientId] = state;
1936
2583
  }
1937
- saveManagedIndex(managedIndex, managedIndexPath);
1938
- saveConfig(config);
1939
- const hasErrors = results.some((result) => result.status === "ERROR") || imports.conflicts.length > 0 || imports.errors.length > 0;
1940
- return {
1941
- gatewayUrl,
1942
- imports,
1943
- results,
1944
- hasErrors
1945
- };
1946
2584
  }
1947
2585
 
1948
- export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients };
2586
+ export { listSkills, getSkill, saveSkill, deleteSkill, prunePluginProjections, pruneAllPluginProjections, syncPluginsToClient, APP_VERSION, getAdapters, loadManagedIndex, getGatewayUrl, syncAllClients, persistSyncState };