sync-agents-settings 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +121 -22
  3. package/commands/report-schema.md +25 -0
  4. package/commands/sync-diff.md +5 -1
  5. package/commands/sync-doctor.md +43 -0
  6. package/commands/sync-instructions.md +6 -2
  7. package/commands/sync-reconcile.md +41 -0
  8. package/commands/sync-validate.md +42 -0
  9. package/commands/sync.md +6 -2
  10. package/dist/backup.d.ts +1 -1
  11. package/dist/backup.js +4 -1
  12. package/dist/backup.js.map +1 -1
  13. package/dist/cli.js +541 -54
  14. package/dist/cli.js.map +1 -1
  15. package/dist/diff.d.ts +6 -0
  16. package/dist/diff.js +7 -0
  17. package/dist/diff.js.map +1 -0
  18. package/dist/doctor.d.ts +23 -0
  19. package/dist/doctor.js +140 -0
  20. package/dist/doctor.js.map +1 -0
  21. package/dist/fix.d.ts +19 -0
  22. package/dist/fix.js +71 -0
  23. package/dist/fix.js.map +1 -0
  24. package/dist/instructions.d.ts +8 -1
  25. package/dist/instructions.js +370 -11
  26. package/dist/instructions.js.map +1 -1
  27. package/dist/oauth.d.ts +2 -0
  28. package/dist/oauth.js +7 -0
  29. package/dist/oauth.js.map +1 -0
  30. package/dist/paths.d.ts +4 -0
  31. package/dist/paths.js +5 -0
  32. package/dist/paths.js.map +1 -1
  33. package/dist/reconcile.d.ts +27 -0
  34. package/dist/reconcile.js +122 -0
  35. package/dist/reconcile.js.map +1 -0
  36. package/dist/report-parser.d.ts +81 -0
  37. package/dist/report-parser.js +129 -0
  38. package/dist/report-parser.js.map +1 -0
  39. package/dist/report-schema-renderer.d.ts +2 -0
  40. package/dist/report-schema-renderer.js +129 -0
  41. package/dist/report-schema-renderer.js.map +1 -0
  42. package/dist/report-schema-sync.d.ts +9 -0
  43. package/dist/report-schema-sync.js +28 -0
  44. package/dist/report-schema-sync.js.map +1 -0
  45. package/dist/report.d.ts +47 -0
  46. package/dist/report.js +55 -0
  47. package/dist/report.js.map +1 -0
  48. package/dist/types.d.ts +1 -1
  49. package/dist/validation.d.ts +18 -0
  50. package/dist/validation.js +62 -0
  51. package/dist/validation.js.map +1 -0
  52. package/dist/writers/codex.js +12 -2
  53. package/dist/writers/codex.js.map +1 -1
  54. package/dist/writers/kimi.d.ts +7 -0
  55. package/dist/writers/kimi.js +25 -0
  56. package/dist/writers/kimi.js.map +1 -0
  57. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync } from "node:fs";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
3
4
  import { Command } from "commander";
4
5
  import { readClaudeMcpServers } from "./reader.js";
5
6
  import { writeToGemini } from "./writers/gemini.js";
@@ -7,8 +8,18 @@ import { writeToCodex, resolveCodexConfigPath } from "./writers/codex.js";
7
8
  import { writeToOpenCode } from "./writers/opencode.js";
8
9
  import { writeToKiro } from "./writers/kiro.js";
9
10
  import { writeToCursor } from "./writers/cursor.js";
11
+ import { writeToKimi, resolveKimiMcpConfigPath } from "./writers/kimi.js";
10
12
  import { createBackup, getFilesToBackup } from "./backup.js";
11
13
  import { PATHS } from "./paths.js";
14
+ import { runDoctor } from "./doctor.js";
15
+ import { validateServersForTargets } from "./validation.js";
16
+ import { isOAuthOnlyServer } from "./oauth.js";
17
+ import { reconcileTargets, groupValidationByTarget } from "./reconcile.js";
18
+ import { runAutoFix } from "./fix.js";
19
+ import { compareNameSets } from "./diff.js";
20
+ import { formatReconcileReport, formatDoctorReport, formatValidationReport, formatSyncReport, formatSyncInstructionsReport, formatDiffReport, } from "./report.js";
21
+ import { generateReportSchemaDocument } from "./report-schema-renderer.js";
22
+ import { checkReportSchemaUpToDate } from "./report-schema-sync.js";
12
23
  import { syncInstructions, getGlobalSyncPairs, getLocalSyncPairs, getUnsupportedGlobalTargets, } from "./instructions.js";
13
24
  const program = new Command();
14
25
  program
@@ -18,17 +29,13 @@ program
18
29
  program
19
30
  .command("sync")
20
31
  .description("Sync MCP settings from Claude Code to other CLIs")
21
- .option("-t, --target <targets...>", "sync targets (gemini, codex, opencode, kiro, cursor)", [
22
- "gemini",
23
- "codex",
24
- "opencode",
25
- "kiro",
26
- "cursor",
27
- ])
32
+ .option("-t, --target <targets...>", "sync targets (gemini, codex, opencode, kiro, cursor, kimi, aider)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi", "aider"])
28
33
  .option("--dry-run", "preview mode, no files will be written", false)
29
34
  .option("--no-backup", "skip backup")
30
35
  .option("--skip-oauth", "skip MCP servers that require OAuth", false)
31
36
  .option("--codex-home <path>", "Codex config directory (default: ~/.codex, or specify project-level .codex/)")
37
+ .option("--kimi-home <path>", "Kimi config directory (default: ~/.kimi, or specify project-level .kimi/)")
38
+ .option("--report <format>", "output format: text or json", "text")
32
39
  .option("-v, --verbose", "show detailed output", false)
33
40
  .action(async (opts) => {
34
41
  const targets = opts.target;
@@ -37,57 +44,131 @@ program
37
44
  const verbose = opts.verbose;
38
45
  const skipOAuth = opts.skipOauth;
39
46
  const codexHome = opts.codexHome;
40
- if (dryRun) {
47
+ const kimiHome = opts.kimiHome;
48
+ const reportFormat = opts.report;
49
+ const jsonReport = reportFormat === "json";
50
+ if (reportFormat !== "text" && reportFormat !== "json") {
51
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
52
+ process.exit(1);
53
+ }
54
+ if (!jsonReport && dryRun) {
41
55
  console.log("🔍 Dry-run mode — no files will be written\n");
42
56
  }
43
57
  // 1. Read Claude MCP servers
44
- console.log("📖 Reading Claude Code MCP settings...");
58
+ if (!jsonReport) {
59
+ console.log("📖 Reading Claude Code MCP settings...");
60
+ }
45
61
  let servers = readClaudeMcpServers();
46
62
  if (skipOAuth) {
47
- servers = servers.filter((s) => !s.oauth);
63
+ servers = servers.filter((s) => !isOAuthOnlyServer(s));
48
64
  }
49
- console.log(` Found ${servers.length} MCP server(s)\n`);
50
- if (verbose) {
65
+ if (!jsonReport) {
66
+ console.log(` Found ${servers.length} MCP server(s)\n`);
67
+ }
68
+ if (!jsonReport && verbose) {
51
69
  printServers(servers);
52
70
  }
53
71
  if (servers.length === 0) {
54
- console.log("No MCP servers found, exiting.");
72
+ if (jsonReport) {
73
+ console.log(formatSyncReport({
74
+ sourceCount: 0,
75
+ dryRun,
76
+ skipOAuth,
77
+ targets: [],
78
+ }));
79
+ }
80
+ else {
81
+ console.log("No MCP servers found, exiting.");
82
+ }
55
83
  return;
56
84
  }
57
85
  // 2. Backup
58
86
  const codexConfigPath = resolveCodexConfigPath(codexHome);
87
+ const kimiConfigPath = resolveKimiMcpConfigPath(kimiHome);
59
88
  if (!skipBackup && !dryRun) {
60
- console.log("💾 Backing up config files...");
61
- const backupDir = createBackup(getFilesToBackup(targets, codexConfigPath));
62
- console.log(` Backup directory: ${backupDir}\n`);
89
+ if (!jsonReport) {
90
+ console.log("💾 Backing up config files...");
91
+ }
92
+ const backupDir = createBackup(getFilesToBackup(targets, codexConfigPath, kimiConfigPath));
93
+ if (!jsonReport) {
94
+ console.log(` Backup directory: ${backupDir}\n`);
95
+ }
63
96
  }
64
97
  // 3. Sync to targets
98
+ const targetReports = [];
65
99
  for (const target of targets) {
66
- console.log(`🔄 Syncing to ${target.toUpperCase()}...`);
100
+ if (!jsonReport) {
101
+ console.log(`🔄 Syncing to ${target.toUpperCase()}...`);
102
+ }
67
103
  if (target === "gemini") {
68
104
  const result = writeToGemini(servers, dryRun);
69
- printResult(result.added, result.skipped);
105
+ targetReports.push({ target, added: result.added, skipped: result.skipped });
106
+ if (!jsonReport) {
107
+ printResult(result.added, result.skipped);
108
+ }
70
109
  }
71
110
  else if (target === "codex") {
72
111
  const result = writeToCodex(servers, dryRun, codexHome);
73
- console.log(` Target: ${result.configPath}`);
74
- printResult(result.added, result.skipped);
112
+ targetReports.push({
113
+ target,
114
+ added: result.added,
115
+ skipped: result.skipped,
116
+ configPath: result.configPath,
117
+ });
118
+ if (!jsonReport) {
119
+ console.log(` Target: ${result.configPath}`);
120
+ printResult(result.added, result.skipped);
121
+ }
75
122
  }
76
123
  else if (target === "opencode") {
77
124
  const result = writeToOpenCode(servers, dryRun);
78
- printResult(result.added, result.skipped);
125
+ targetReports.push({ target, added: result.added, skipped: result.skipped });
126
+ if (!jsonReport) {
127
+ printResult(result.added, result.skipped);
128
+ }
79
129
  }
80
130
  else if (target === "kiro") {
81
131
  const result = writeToKiro(servers, dryRun);
82
- printResult(result.added, result.skipped);
132
+ targetReports.push({ target, added: result.added, skipped: result.skipped });
133
+ if (!jsonReport) {
134
+ printResult(result.added, result.skipped);
135
+ }
83
136
  }
84
137
  else if (target === "cursor") {
85
138
  const result = writeToCursor(servers, dryRun);
86
- printResult(result.added, result.skipped);
139
+ targetReports.push({ target, added: result.added, skipped: result.skipped });
140
+ if (!jsonReport) {
141
+ printResult(result.added, result.skipped);
142
+ }
143
+ }
144
+ else if (target === "kimi") {
145
+ const result = writeToKimi(servers, dryRun, kimiHome);
146
+ targetReports.push({
147
+ target,
148
+ added: result.added,
149
+ skipped: result.skipped,
150
+ configPath: result.configPath,
151
+ });
152
+ if (!jsonReport) {
153
+ console.log(` Target: ${result.configPath}`);
154
+ printResult(result.added, result.skipped);
155
+ }
156
+ }
157
+ if (!jsonReport) {
158
+ console.log();
87
159
  }
88
- console.log();
89
160
  }
90
- console.log("✅ Sync complete!");
161
+ if (jsonReport) {
162
+ console.log(formatSyncReport({
163
+ sourceCount: servers.length,
164
+ dryRun,
165
+ skipOAuth,
166
+ targets: targetReports,
167
+ }));
168
+ }
169
+ else {
170
+ console.log("✅ Sync complete!");
171
+ }
91
172
  });
92
173
  program
93
174
  .command("list")
@@ -98,46 +179,395 @@ program
98
179
  printServers(servers);
99
180
  console.log(`\nTotal: ${servers.length} MCP server(s)`);
100
181
  });
182
+ program
183
+ .command("report-schema")
184
+ .description("Print report JSON schema markdown (or write to a file)")
185
+ .option("--check", "exit non-zero if target schema file is missing or outdated", false)
186
+ .option("--write <path>", "write markdown to a file path instead of stdout")
187
+ .action((opts) => {
188
+ const content = generateReportSchemaDocument();
189
+ const checkOnly = opts.check;
190
+ const outputPath = opts.write;
191
+ if (checkOnly) {
192
+ const targetPath = outputPath ?? "docs/report-schema.md";
193
+ const check = checkReportSchemaUpToDate(targetPath);
194
+ if (check.upToDate) {
195
+ console.log(`✅ Report schema is up to date: ${targetPath}`);
196
+ return;
197
+ }
198
+ if (check.reason === "missing") {
199
+ console.error(`❌ Report schema file is missing: ${targetPath}`);
200
+ }
201
+ else {
202
+ console.error(`❌ Report schema file is outdated: ${targetPath}`);
203
+ }
204
+ process.exit(1);
205
+ }
206
+ if (!outputPath) {
207
+ console.log(content);
208
+ return;
209
+ }
210
+ mkdirSync(dirname(outputPath), { recursive: true });
211
+ writeFileSync(outputPath, content);
212
+ console.log(`✅ Report schema written: ${outputPath}`);
213
+ });
101
214
  program
102
215
  .command("diff")
103
216
  .description("Compare MCP settings between Claude Code and other CLIs")
104
- .option("-t, --target <targets...>", "comparison targets (gemini, codex, opencode, kiro, cursor)", ["gemini", "codex", "opencode", "kiro", "cursor"])
217
+ .option("-t, --target <targets...>", "comparison targets (gemini, codex, opencode, kiro, cursor, kimi)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi"])
218
+ .option("--kimi-home <path>", "Kimi config directory (default: ~/.kimi, or specify project-level .kimi/)")
219
+ .option("--report <format>", "output format: text or json", "text")
105
220
  .action((opts) => {
106
221
  const targets = opts.target;
222
+ const kimiHome = opts.kimiHome;
223
+ const reportFormat = opts.report;
224
+ const jsonReport = reportFormat === "json";
225
+ if (reportFormat !== "text" && reportFormat !== "json") {
226
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
227
+ process.exit(1);
228
+ }
107
229
  const servers = readClaudeMcpServers();
108
230
  const claudeNames = new Set(servers.map((s) => s.name));
109
- console.log(`Claude Code: ${servers.length} MCP server(s)\n`);
231
+ const sourceNames = [...claudeNames].sort();
232
+ if (!jsonReport) {
233
+ console.log(`Claude Code: ${servers.length} MCP server(s)\n`);
234
+ }
110
235
  const diffConfigs = {
111
236
  gemini: { path: PATHS.geminiSettings },
112
237
  opencode: { path: PATHS.openCodeConfig, key: "mcp" },
113
238
  kiro: { path: PATHS.kiroMcpConfig },
114
239
  cursor: { path: PATHS.cursorMcpConfig },
240
+ kimi: { path: resolveKimiMcpConfigPath(kimiHome) },
115
241
  };
242
+ const targetReports = [];
116
243
  for (const target of targets) {
117
244
  if (target === "codex") {
118
- console.log(` Codex: use 'codex mcp list' to view`);
245
+ const codexNote = "use 'codex mcp list' to view";
246
+ if (!jsonReport) {
247
+ console.log(` Codex: ${codexNote}`);
248
+ }
249
+ targetReports.push({
250
+ target: "codex",
251
+ shared: [],
252
+ onlyInSource: [],
253
+ onlyInTarget: [],
254
+ note: codexNote,
255
+ });
119
256
  }
120
257
  else if (Object.hasOwn(diffConfigs, target)) {
121
258
  const { path, key } = diffConfigs[target];
122
259
  const names = readExistingServerNames(path, key);
123
- printDiff(target.charAt(0).toUpperCase() + target.slice(1), claudeNames, names);
260
+ const compared = compareNameSets(claudeNames, names);
261
+ targetReports.push({
262
+ target,
263
+ shared: compared.shared,
264
+ onlyInSource: compared.onlyInSource,
265
+ onlyInTarget: compared.onlyInTarget,
266
+ });
267
+ if (!jsonReport) {
268
+ printDiff(target.charAt(0).toUpperCase() + target.slice(1), compared.shared, compared.onlyInSource, compared.onlyInTarget);
269
+ }
124
270
  }
125
271
  }
272
+ if (jsonReport) {
273
+ console.log(formatDiffReport({
274
+ sourceCount: servers.length,
275
+ sourceNames,
276
+ targets: targetReports,
277
+ }));
278
+ }
279
+ });
280
+ program
281
+ .command("doctor")
282
+ .description("Detect MCP config drift between Claude Code and target CLIs")
283
+ .option("-t, --target <targets...>", "doctor targets (gemini, codex, opencode, kiro, cursor, kimi)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi"])
284
+ .option("--skip-oauth", "ignore OAuth-only Claude servers", false)
285
+ .option("--fix", "auto-run reconcile when drift is detected", false)
286
+ .option("--dry-run", "when used with --fix, preview without writing", false)
287
+ .option("--no-backup", "when used with --fix, skip backup")
288
+ .option("--report <format>", "output format: text or json", "text")
289
+ .option("--codex-home <path>", "Codex config directory (default: ~/.codex, or specify project-level .codex/)")
290
+ .option("--kimi-home <path>", "Kimi config directory (default: ~/.kimi, or specify project-level .kimi/)")
291
+ .action((opts) => {
292
+ const targets = opts.target;
293
+ const skipOAuth = opts.skipOauth;
294
+ const fix = opts.fix;
295
+ const dryRun = opts.dryRun;
296
+ const skipBackup = !opts.backup;
297
+ const reportFormat = opts.report;
298
+ const codexHome = opts.codexHome;
299
+ const kimiHome = opts.kimiHome;
300
+ const jsonReport = reportFormat === "json";
301
+ if (reportFormat !== "text" && reportFormat !== "json") {
302
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
303
+ process.exit(1);
304
+ }
305
+ if (fix) {
306
+ if (jsonReport) {
307
+ console.error("--report json is not supported with --fix for doctor. Use reconcile --report json.");
308
+ process.exit(1);
309
+ }
310
+ const fixed = runAutoFix({
311
+ mode: "doctor",
312
+ targets,
313
+ dryRun,
314
+ skipBackup,
315
+ skipOAuth,
316
+ codexHome,
317
+ kimiHome,
318
+ });
319
+ if (fixed.status === "failed") {
320
+ if (fixed.reason === "doctor_parse") {
321
+ console.error("❌ Auto-fix failed: target config parse error. Fix target config and retry.");
322
+ }
323
+ else if (fixed.reason === "validation") {
324
+ console.error("❌ Auto-fix failed: validation errors detected.");
325
+ }
326
+ else {
327
+ console.error("❌ Auto-fix failed.");
328
+ }
329
+ process.exit(2);
330
+ }
331
+ if (fixed.status === "noop") {
332
+ console.log("✅ No drift detected. Nothing to fix.");
333
+ return;
334
+ }
335
+ console.log("✅ Auto-fix completed via reconcile.");
336
+ return;
337
+ }
338
+ const report = runDoctor(targets, { skipOAuth, codexHome, kimiHome });
339
+ if (jsonReport) {
340
+ console.log(formatDoctorReport(report));
341
+ if (report.hasErrors)
342
+ process.exit(2);
343
+ if (report.hasDrift)
344
+ process.exit(1);
345
+ return;
346
+ }
347
+ console.log("🩺 MCP drift doctor\n");
348
+ console.log(`Claude Code source servers: ${report.sourceCount}\n`);
349
+ for (const result of report.results) {
350
+ const label = getTargetLabel(result.target);
351
+ console.log(`--- ${label} ---`);
352
+ if (result.status === "unavailable") {
353
+ console.log(` ⚠ Unavailable: ${result.note ?? "target directory not found"}`);
354
+ }
355
+ else if (result.status === "error") {
356
+ console.log(` ❌ Parse error: ${result.note ?? "unable to read target config"}`);
357
+ }
358
+ else if (result.status === "ok") {
359
+ console.log(" ✅ No drift");
360
+ }
361
+ else {
362
+ if (result.missing.length > 0) {
363
+ console.log(` Missing in ${label}: ${result.missing.join(", ")}`);
364
+ }
365
+ if (result.extra.length > 0) {
366
+ console.log(` Extra in ${label}: ${result.extra.join(", ")}`);
367
+ }
368
+ }
369
+ console.log();
370
+ }
371
+ if (report.hasErrors) {
372
+ console.error("Config parse error detected. Please fix target config file(s) and retry.");
373
+ process.exit(2);
374
+ }
375
+ if (report.hasDrift) {
376
+ console.error("Drift detected. Run sync to reconcile.");
377
+ process.exit(1);
378
+ }
379
+ console.log("✅ All checked targets are in sync.");
380
+ });
381
+ program
382
+ .command("validate")
383
+ .description("Validate MCP schema and target capability compatibility")
384
+ .option("-t, --target <targets...>", "validation targets (gemini, codex, opencode, kiro, cursor, kimi)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi"])
385
+ .option("--skip-oauth", "ignore OAuth-only Claude servers", false)
386
+ .option("--fix", "auto-run reconcile after validation passes", false)
387
+ .option("--dry-run", "when used with --fix, preview without writing", false)
388
+ .option("--no-backup", "when used with --fix, skip backup")
389
+ .option("--report <format>", "output format: text or json", "text")
390
+ .option("--codex-home <path>", "Codex config directory (used by --fix for reconcile)")
391
+ .option("--kimi-home <path>", "Kimi config directory (used by --fix for reconcile)")
392
+ .action((opts) => {
393
+ const targets = opts.target;
394
+ const skipOAuth = opts.skipOauth;
395
+ const fix = opts.fix;
396
+ const dryRun = opts.dryRun;
397
+ const skipBackup = !opts.backup;
398
+ const reportFormat = opts.report;
399
+ const codexHome = opts.codexHome;
400
+ const kimiHome = opts.kimiHome;
401
+ const jsonReport = reportFormat === "json";
402
+ if (reportFormat !== "text" && reportFormat !== "json") {
403
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
404
+ process.exit(1);
405
+ }
406
+ if (fix && jsonReport) {
407
+ console.error("--report json is not supported with --fix for validate. Use reconcile --report json.");
408
+ process.exit(1);
409
+ }
410
+ const servers = readClaudeMcpServers();
411
+ const report = validateServersForTargets(servers, targets, { skipOAuth });
412
+ if (jsonReport) {
413
+ console.log(formatValidationReport(report));
414
+ if (report.errorCount > 0) {
415
+ process.exit(2);
416
+ }
417
+ return;
418
+ }
419
+ console.log("🧪 MCP schema/capability validation\n");
420
+ console.log(`Checked servers: ${skipOAuth ? "OAuth-skipped subset" : servers.length}`);
421
+ console.log(`Targets: ${targets.join(", ")}\n`);
422
+ if (report.issues.length === 0 && !fix) {
423
+ console.log("✅ No schema/capability issues found.");
424
+ return;
425
+ }
426
+ if (report.issues.length === 0) {
427
+ console.log("✅ No schema/capability issues found.");
428
+ }
429
+ for (const target of targets) {
430
+ const targetIssues = report.issues.filter((issue) => issue.target === target);
431
+ if (targetIssues.length === 0)
432
+ continue;
433
+ const label = getTargetLabel(target);
434
+ console.log(`--- ${label} ---`);
435
+ for (const issue of targetIssues) {
436
+ const icon = issue.severity === "error" ? "❌" : "⚠";
437
+ console.log(` ${icon} [${issue.code}] ${issue.server}: ${issue.message}`);
438
+ }
439
+ console.log();
440
+ }
441
+ if (report.issues.length > 0) {
442
+ console.log(`Summary: ${report.errorCount} error(s), ${report.warningCount} warning(s)`);
443
+ }
444
+ if (report.errorCount > 0) {
445
+ process.exit(2);
446
+ }
447
+ if (fix) {
448
+ const fixed = runAutoFix({
449
+ mode: "validate",
450
+ targets,
451
+ dryRun,
452
+ skipBackup,
453
+ skipOAuth,
454
+ codexHome,
455
+ kimiHome,
456
+ });
457
+ if (fixed.status === "failed") {
458
+ if (fixed.reason === "doctor_parse") {
459
+ console.error("❌ Auto-fix failed: target config parse error. Fix target config and retry.");
460
+ }
461
+ else if (fixed.reason === "validation") {
462
+ console.error("❌ Auto-fix failed: validation errors detected.");
463
+ }
464
+ else {
465
+ console.error("❌ Auto-fix failed.");
466
+ }
467
+ process.exit(2);
468
+ }
469
+ if (fixed.status === "noop") {
470
+ console.log("✅ No drift detected. Nothing to fix.");
471
+ return;
472
+ }
473
+ console.log("✅ Auto-fix completed via reconcile.");
474
+ }
475
+ });
476
+ program
477
+ .command("reconcile")
478
+ .description("Validate + detect drift + sync only missing MCP servers")
479
+ .option("-t, --target <targets...>", "reconcile targets (gemini, codex, opencode, kiro, cursor, kimi)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi"])
480
+ .option("--dry-run", "preview mode, no files will be written", false)
481
+ .option("--no-backup", "skip backup")
482
+ .option("--skip-oauth", "ignore OAuth-only Claude servers", false)
483
+ .option("--codex-home <path>", "Codex config directory (default: ~/.codex, or specify project-level .codex/)")
484
+ .option("--kimi-home <path>", "Kimi config directory (default: ~/.kimi, or specify project-level .kimi/)")
485
+ .option("--report <format>", "output format: text or json", "text")
486
+ .action((opts) => {
487
+ const targets = opts.target;
488
+ const dryRun = opts.dryRun;
489
+ const skipBackup = !opts.backup;
490
+ const skipOAuth = opts.skipOauth;
491
+ const codexHome = opts.codexHome;
492
+ const kimiHome = opts.kimiHome;
493
+ const reportFormat = opts.report;
494
+ const jsonReport = reportFormat === "json";
495
+ if (reportFormat !== "text" && reportFormat !== "json") {
496
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
497
+ process.exit(1);
498
+ }
499
+ if (!jsonReport && dryRun) {
500
+ console.log("🔍 Dry-run mode — no files will be written\n");
501
+ }
502
+ const result = reconcileTargets(targets, {
503
+ dryRun,
504
+ skipBackup,
505
+ skipOAuth,
506
+ codexHome,
507
+ kimiHome,
508
+ });
509
+ if (jsonReport) {
510
+ console.log(formatReconcileReport(result));
511
+ if (result.status === "validation_failed" || result.status === "doctor_failed") {
512
+ process.exit(2);
513
+ }
514
+ return;
515
+ }
516
+ if (result.status === "validation_failed") {
517
+ console.error("❌ Validation failed. Fix schema errors before reconcile.\n");
518
+ const grouped = groupValidationByTarget(result.validation.issues, targets);
519
+ for (const target of targets) {
520
+ const issues = grouped[target].filter((issue) => issue.severity === "error");
521
+ if (issues.length === 0)
522
+ continue;
523
+ console.error(`--- ${getTargetLabel(target)} ---`);
524
+ for (const issue of issues) {
525
+ console.error(` ❌ [${issue.code}] ${issue.server}: ${issue.message}`);
526
+ }
527
+ console.error();
528
+ }
529
+ process.exit(2);
530
+ }
531
+ if (result.status === "doctor_failed") {
532
+ console.error("❌ Target config parse error detected. Fix target config and retry.");
533
+ process.exit(2);
534
+ }
535
+ if (result.validation.warningCount > 0) {
536
+ console.log(`⚠ Validation warnings: ${result.validation.warningCount}\n`);
537
+ }
538
+ if (result.status === "noop") {
539
+ console.log("✅ No drift detected. Nothing to reconcile.");
540
+ return;
541
+ }
542
+ if (result.backupDir) {
543
+ console.log(`💾 Backup directory: ${result.backupDir}\n`);
544
+ }
545
+ console.log("🔄 Reconcile results\n");
546
+ for (const syncResult of result.syncResults) {
547
+ const label = getTargetLabel(syncResult.target);
548
+ console.log(`--- ${label} ---`);
549
+ console.log(` Missing before reconcile: ${syncResult.missing.join(", ")}`);
550
+ if (syncResult.added.length > 0) {
551
+ console.log(` ✅ Added: ${syncResult.added.join(", ")}`);
552
+ }
553
+ if (syncResult.skipped.length > 0) {
554
+ console.log(` ⏭ Skipped: ${syncResult.skipped.join(", ")}`);
555
+ }
556
+ console.log();
557
+ }
558
+ console.log("✅ Reconcile complete!");
126
559
  });
127
560
  program
128
561
  .command("sync-instructions")
129
562
  .description("Sync CLAUDE.md instruction files to other AI agent formats")
130
- .option("-t, --target <targets...>", "sync targets (gemini, codex, opencode, kiro, cursor)", [
131
- "gemini",
132
- "codex",
133
- "opencode",
134
- "kiro",
135
- "cursor",
136
- ])
563
+ .option("-t, --target <targets...>", "sync targets (gemini, codex, opencode, kiro, cursor, kimi)", ["gemini", "codex", "opencode", "kiro", "cursor", "kimi"])
137
564
  .option("--global", "sync global config (~/.claude/CLAUDE.md)", false)
138
- .option("--local", "sync project-level CLAUDE.md in current directory", false)
565
+ .option("--local", "sync project-level CLAUDE.md in current directory (prefers ./.claude/CLAUDE.md, falls back to ./CLAUDE.md)", false)
139
566
  .option("--dry-run", "preview mode, no files will be written", false)
140
567
  .option("--no-backup", "skip backup")
568
+ .option("--import-mode <mode>", "how to handle standalone @imports: inline or strip", "inline")
569
+ .option("--allow-unsafe-imports", "allow standalone @imports to read files outside current project root", false)
570
+ .option("--report <format>", "output format: text or json", "text")
141
571
  .option("--on-conflict <action>", "action when target exists: overwrite, append, skip (skips interactive prompt)")
142
572
  .action(async (opts) => {
143
573
  const targets = opts.target;
@@ -146,31 +576,84 @@ program
146
576
  const dryRun = opts.dryRun;
147
577
  const skipBackup = !opts.backup;
148
578
  const onConflict = opts.onConflict;
579
+ const importMode = opts.importMode;
580
+ const allowUnsafeImports = opts.allowUnsafeImports;
581
+ const reportFormat = opts.report;
582
+ const jsonReport = reportFormat === "json";
583
+ const forceAction = onConflict ?? (jsonReport ? "overwrite" : undefined);
584
+ if (reportFormat !== "text" && reportFormat !== "json") {
585
+ console.error(`Invalid --report: ${reportFormat}. Use "text" or "json".`);
586
+ process.exit(1);
587
+ }
588
+ if (jsonReport && !dryRun && !skipBackup) {
589
+ console.error("--report json requires --no-backup when not using --dry-run.");
590
+ process.exit(1);
591
+ }
592
+ if (jsonReport && !dryRun && !onConflict) {
593
+ console.error("--report json requires --on-conflict when not using --dry-run.");
594
+ process.exit(1);
595
+ }
596
+ if (importMode !== "inline" && importMode !== "strip") {
597
+ console.error(`Invalid --import-mode: ${importMode}. Use "inline" or "strip".`);
598
+ process.exit(1);
599
+ }
149
600
  // Default: sync both if neither flag is set
150
601
  const doGlobal = syncGlobal || (!syncGlobal && !syncLocal);
151
602
  const doLocal = syncLocal || (!syncGlobal && !syncLocal);
152
- if (dryRun) {
603
+ if (!jsonReport && dryRun) {
153
604
  console.log("🔍 Dry-run mode — no files will be written\n");
154
605
  }
606
+ const unsupported = doGlobal ? getUnsupportedGlobalTargets(targets) : [];
607
+ let globalResult;
608
+ let localResult;
155
609
  // Global sync
156
610
  if (doGlobal) {
157
- console.log("📋 Syncing global instructions (~/.claude/CLAUDE.md)...\n");
158
- const unsupported = getUnsupportedGlobalTargets(targets);
159
- for (const msg of unsupported) {
160
- console.log(` ⚠ ${msg}`);
611
+ if (!jsonReport) {
612
+ console.log("📋 Syncing global instructions (~/.claude/CLAUDE.md)...\n");
613
+ for (const msg of unsupported) {
614
+ console.log(` ⚠ ${msg}`);
615
+ }
161
616
  }
162
617
  const pairs = getGlobalSyncPairs(targets);
163
- backupTargets(pairs, skipBackup, dryRun);
164
- const result = await syncInstructions(pairs, { dryRun, force: onConflict });
165
- printInstructionsResult(result);
618
+ if (!jsonReport) {
619
+ backupTargets(pairs, skipBackup, dryRun);
620
+ }
621
+ globalResult = await syncInstructions(pairs, {
622
+ dryRun,
623
+ force: forceAction,
624
+ importMode,
625
+ allowUnsafeImports,
626
+ });
627
+ if (!jsonReport) {
628
+ printInstructionsResult(globalResult);
629
+ }
166
630
  }
167
631
  // Local sync
168
632
  if (doLocal) {
169
- console.log("📋 Syncing local instructions (./CLAUDE.md)...\n");
633
+ if (!jsonReport) {
634
+ console.log("📋 Syncing local instructions (./.claude/CLAUDE.md or ./CLAUDE.md)...\n");
635
+ }
170
636
  const pairs = getLocalSyncPairs(targets, process.cwd());
171
- backupTargets(pairs, skipBackup, dryRun);
172
- const result = await syncInstructions(pairs, { dryRun, force: onConflict });
173
- printInstructionsResult(result);
637
+ if (!jsonReport) {
638
+ backupTargets(pairs, skipBackup, dryRun);
639
+ }
640
+ localResult = await syncInstructions(pairs, {
641
+ dryRun,
642
+ force: forceAction,
643
+ importMode,
644
+ allowUnsafeImports,
645
+ });
646
+ if (!jsonReport) {
647
+ printInstructionsResult(localResult);
648
+ }
649
+ }
650
+ if (jsonReport) {
651
+ console.log(formatSyncInstructionsReport({
652
+ unsupportedGlobalTargets: unsupported,
653
+ global: globalResult,
654
+ local: localResult,
655
+ }));
656
+ return;
174
657
  }
175
658
  console.log("✅ Instructions sync complete!");
176
659
  });
@@ -208,10 +691,7 @@ function readExistingServerNames(configPath, key = "mcpServers") {
208
691
  return new Set();
209
692
  }
210
693
  }
211
- function printDiff(targetName, claudeNames, targetNames) {
212
- const onlyInClaude = [...claudeNames].filter((n) => !targetNames.has(n));
213
- const onlyInTarget = [...targetNames].filter((n) => !claudeNames.has(n));
214
- const shared = [...claudeNames].filter((n) => targetNames.has(n));
694
+ function printDiff(targetName, shared, onlyInClaude, onlyInTarget) {
215
695
  console.log(`--- ${targetName} comparison ---`);
216
696
  if (shared.length > 0) {
217
697
  console.log(` Shared: ${shared.join(", ")}`);
@@ -247,5 +727,12 @@ function printResult(added, skipped) {
247
727
  console.log(" No servers to sync");
248
728
  }
249
729
  }
730
+ function getTargetLabel(target) {
731
+ if (target === "opencode")
732
+ return "OpenCode";
733
+ if (target === "kimi")
734
+ return "Kimi";
735
+ return target.toUpperCase();
736
+ }
250
737
  program.parse();
251
738
  //# sourceMappingURL=cli.js.map