@howlil/ez-agents 3.0.0 → 3.4.1

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 (61) hide show
  1. package/README.md +295 -714
  2. package/bin/install.js +446 -78
  3. package/commands/ez/auth.md +87 -0
  4. package/commands/ez/join-discord.md +1 -1
  5. package/ez-agents/bin/ez-tools.cjs +120 -2
  6. package/ez-agents/bin/lib/assistant-adapter.cjs +62 -3
  7. package/ez-agents/bin/lib/audit-exec.cjs +20 -8
  8. package/ez-agents/bin/lib/auth.cjs +2 -1
  9. package/ez-agents/bin/lib/circuit-breaker.cjs +1 -1
  10. package/ez-agents/bin/lib/commands.cjs +42 -23
  11. package/ez-agents/bin/lib/config.cjs +18 -11
  12. package/ez-agents/bin/lib/core.cjs +42 -25
  13. package/ez-agents/bin/lib/file-lock.cjs +3 -3
  14. package/ez-agents/bin/lib/fs-utils.cjs +1 -1
  15. package/ez-agents/bin/lib/git-utils.cjs +1 -1
  16. package/ez-agents/bin/lib/health-check.cjs +2 -3
  17. package/ez-agents/bin/lib/index.cjs +1 -1
  18. package/ez-agents/bin/lib/init.cjs +70 -23
  19. package/ez-agents/bin/lib/logger.cjs +11 -4
  20. package/ez-agents/bin/lib/model-provider.cjs +124 -29
  21. package/ez-agents/bin/lib/phase.cjs +39 -22
  22. package/ez-agents/bin/lib/planning-write.cjs +107 -0
  23. package/ez-agents/bin/lib/retry.cjs +1 -1
  24. package/ez-agents/bin/lib/roadmap.cjs +3 -2
  25. package/ez-agents/bin/lib/safe-exec.cjs +1 -1
  26. package/ez-agents/bin/lib/safe-path.cjs +1 -1
  27. package/ez-agents/bin/lib/state.cjs +24 -9
  28. package/ez-agents/bin/lib/temp-file.cjs +1 -1
  29. package/ez-agents/bin/lib/template.cjs +2 -1
  30. package/ez-agents/bin/lib/test-file-lock.cjs +1 -1
  31. package/ez-agents/bin/lib/test-graceful.cjs +2 -2
  32. package/ez-agents/bin/lib/test-logger.cjs +2 -2
  33. package/ez-agents/bin/lib/test-temp-file.cjs +1 -1
  34. package/ez-agents/bin/lib/timeout-exec.cjs +4 -3
  35. package/ez-agents/bin/lib/verify.cjs +54 -25
  36. package/ez-agents/references/continuation-format.md +1 -1
  37. package/ez-agents/workflows/add-tests.md +2 -2
  38. package/ez-agents/workflows/add-todo.md +1 -1
  39. package/ez-agents/workflows/autonomous.md +15 -15
  40. package/ez-agents/workflows/diagnose-issues.md +1 -1
  41. package/ez-agents/workflows/discuss-phase.md +3 -3
  42. package/ez-agents/workflows/execute-phase.md +2 -2
  43. package/ez-agents/workflows/health.md +1 -1
  44. package/ez-agents/workflows/help.md +2 -2
  45. package/ez-agents/workflows/map-codebase.md +1 -1
  46. package/ez-agents/workflows/new-milestone.md +5 -5
  47. package/ez-agents/workflows/new-project.md +12 -10
  48. package/ez-agents/workflows/plan-phase.md +8 -8
  49. package/ez-agents/workflows/progress.md +1 -1
  50. package/ez-agents/workflows/set-profile.md +1 -1
  51. package/ez-agents/workflows/settings.md +9 -9
  52. package/ez-agents/workflows/stats.md +1 -1
  53. package/ez-agents/workflows/ui-phase.md +3 -3
  54. package/ez-agents/workflows/ui-review.md +2 -2
  55. package/ez-agents/workflows/update.md +1 -1
  56. package/ez-agents/workflows/validate-phase.md +3 -3
  57. package/ez-agents/workflows/verify-work.md +3 -3
  58. package/package.json +1 -1
  59. package/scripts/build-hooks.js +1 -1
  60. package/scripts/fix-qwen-installation.js +144 -0
  61. package/README.zh-CN.md +0 -702
package/bin/install.js CHANGED
@@ -17,8 +17,11 @@ const reset = '\x1b[0m';
17
17
  const EZ_CODEX_MARKER = '# EZ_Agents Agent Configuration \u2014 managed by ez-agents installer';
18
18
 
19
19
  // Copilot instructions marker constants
20
- const EZ_COPILOT_INSTRUCTIONS_MARKER = '<!-- EZ_Agents Configuration \u2014 managed by ez-agents installer -->';
21
- const EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER = '<!-- /EZ_Agents Configuration -->';
20
+ const EZ_COPILOT_INSTRUCTIONS_MARKER = '<!-- EZ_AGENTS Configuration \u2014 managed by ez-agents installer -->';
21
+ const EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER = '<!-- /EZ_AGENTS Configuration -->';
22
+ // Legacy markers kept for backward compatibility with older installs.
23
+ const LEGACY_EZ_COPILOT_INSTRUCTIONS_MARKER = '<!-- EZ_Agents Configuration \u2014 managed by ez-agents installer -->';
24
+ const LEGACY_EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER = '<!-- /EZ_Agents Configuration -->';
22
25
 
23
26
  const CODEX_AGENT_SANDBOX = {
24
27
  'ez-executor': 'workspace-write',
@@ -63,6 +66,8 @@ const hasClaude = args.includes('--claude');
63
66
  const hasGemini = args.includes('--gemini');
64
67
  const hasCodex = args.includes('--codex');
65
68
  const hasCopilot = args.includes('--copilot');
69
+ const hasQwen = args.includes('--qwen');
70
+ const hasKimi = args.includes('--kimi');
66
71
  const hasBoth = args.includes('--both'); // Legacy flag, keeps working
67
72
  const hasAll = args.includes('--all');
68
73
  const hasUninstall = args.includes('--uninstall') || args.includes('-u');
@@ -70,7 +75,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
70
75
  // Runtime selection - can be set by flags or interactive prompt
71
76
  let selectedRuntimes = [];
72
77
  if (hasAll) {
73
- selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot'];
78
+ selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'qwen', 'kimi'];
74
79
  } else if (hasBoth) {
75
80
  selectedRuntimes = ['claude', 'opencode'];
76
81
  } else {
@@ -79,6 +84,8 @@ if (hasAll) {
79
84
  if (hasGemini) selectedRuntimes.push('gemini');
80
85
  if (hasCodex) selectedRuntimes.push('codex');
81
86
  if (hasCopilot) selectedRuntimes.push('copilot');
87
+ if (hasQwen) selectedRuntimes.push('qwen');
88
+ if (hasKimi) selectedRuntimes.push('kimi');
82
89
  }
83
90
 
84
91
  // WSL + Windows Node.js detection
@@ -140,13 +147,15 @@ function getDirName(runtime) {
140
147
  if (runtime === 'opencode') return '.opencode';
141
148
  if (runtime === 'gemini') return '.gemini';
142
149
  if (runtime === 'codex') return '.codex';
150
+ if (runtime === 'qwen') return '.qwen';
151
+ if (runtime === 'kimi') return '.kimi';
143
152
  return '.claude';
144
153
  }
145
154
 
146
155
  /**
147
156
  * Get the config directory path relative to home directory for a runtime
148
157
  * Used for templating hooks that use path.join(homeDir, '<configDir>', ...)
149
- * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', or 'copilot'
158
+ * @param {string} runtime - 'claude', 'opencode', 'gemini', 'codex', 'copilot', or 'qwen'
150
159
  * @param {boolean} isGlobal - Whether this is a global install
151
160
  */
152
161
  function getConfigDirFromHome(runtime, isGlobal) {
@@ -163,6 +172,8 @@ function getConfigDirFromHome(runtime, isGlobal) {
163
172
  }
164
173
  if (runtime === 'gemini') return "'.gemini'";
165
174
  if (runtime === 'codex') return "'.codex'";
175
+ if (runtime === 'qwen') return "'.qwen'";
176
+ if (runtime === 'kimi') return "'.kimi'";
166
177
  return "'.claude'";
167
178
  }
168
179
 
@@ -237,7 +248,29 @@ function getGlobalDir(runtime, explicitDir = null) {
237
248
  }
238
249
  return path.join(os.homedir(), '.copilot');
239
250
  }
240
-
251
+
252
+ if (runtime === 'qwen') {
253
+ // Qwen Code: --config-dir > QWEN_CONFIG_DIR > ~/.qwen
254
+ if (explicitDir) {
255
+ return expandTilde(explicitDir);
256
+ }
257
+ if (process.env.QWEN_CONFIG_DIR) {
258
+ return expandTilde(process.env.QWEN_CONFIG_DIR);
259
+ }
260
+ return path.join(os.homedir(), '.qwen');
261
+ }
262
+
263
+ if (runtime === 'kimi') {
264
+ // Kimi Code: --config-dir > KIMI_CONFIG_DIR > ~/.kimi
265
+ if (explicitDir) {
266
+ return expandTilde(explicitDir);
267
+ }
268
+ if (process.env.KIMI_CONFIG_DIR) {
269
+ return expandTilde(process.env.KIMI_CONFIG_DIR);
270
+ }
271
+ return path.join(os.homedir(), '.kimi');
272
+ }
273
+
241
274
  // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
242
275
  if (explicitDir) {
243
276
  return expandTilde(explicitDir);
@@ -257,12 +290,13 @@ const banner = '\n' +
257
290
  ' ╚══════╝╚═╝ ╚═╝' + reset + '\n' +
258
291
  '\n' +
259
292
  ' EZ_Agents ' + dim + 'v' + pkg.version + reset + '\n' +
260
- ' ' + dim + 'Multi-Model Edition' + reset + '\n' +
293
+ ' Multi-Model Edition' + reset + '\n' +
261
294
  ' A meta-prompting, context engineering and spec-driven\n' +
262
295
  ' development system for Claude Code, OpenCode, Gemini, Codex, Copilot,\n' +
263
- ' with multi-model support (Qwen, Kimi, OpenAI, Anthropic).\n' +
296
+ ' Qwen Code, Kimi Code, with multi-model support.\n' +
264
297
  ' Original by TÂCHES | Multi-Model Fork by @howlil.\n';
265
298
 
299
+
266
300
  // Parse --config-dir argument
267
301
  function parseConfigDirArg() {
268
302
  const configDirIndex = args.findIndex(arg => arg === '--config-dir' || arg === '-c');
@@ -299,7 +333,7 @@ if (hasUninstall) {
299
333
 
300
334
  // Show help if requested
301
335
  if (hasHelp) {
302
- console.log(` ${yellow}Usage:${reset} npx ez-agents [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall EZ_Agents (remove all EZ_Agents files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ez-agents\n\n ${dim}# Install for Claude Code globally${reset}\n npx ez-agents --claude --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx ez-agents --all --global\n\n ${dim}# Uninstall EZ_Agents globally${reset}\n npx ez-agents --all --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR environment variables.\n`);
336
+ console.log(` ${yellow}Usage:${reset} npx ez-agents [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\\n ${cyan}--qwen${reset} Install for Qwen Code only\\n ${cyan}--kimi${reset} Install for Kimi Code only\\n ${cyan}--all${reset} Install for all runtimes\\n ${cyan}-u, --uninstall${reset} Uninstall EZ_Agents (remove all EZ_Agents files)\\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\\n ${cyan}-h, --help${reset} Show this help message\\n ${cyan}--force-statusline${reset} Replace existing statusline config\\n\\n ${yellow}Examples:${reset}\\n ${dim}# Interactive install (prompts for runtime and location)${reset}\\n npx ez-agents\\n\\n ${dim}# Install for Claude Code globally${reset}\\n npx ez-agents --claude --global\\n\\n ${dim}# Install for Qwen Code globally${reset}\\n npx ez-agents --qwen --global\\n\\n ${dim}# Install for Kimi Code globally${reset}\\n npx ez-agents --kimi --global\\n\\n ${dim}# Install for all runtimes globally${reset}\\n npx ez-agents --all --global\\n\\n ${dim}# Uninstall EZ_Agents globally${reset}\\n npx ez-agents --all --global --uninstall\\n\\n ${yellow}Notes:${reset}\\n The --config-dir option is useful when you have multiple configurations.\\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / QWEN_CONFIG_DIR / KIMI_CONFIG_DIR environment variables.\\n\\n ${yellow}Model Providers:${reset}\\n OpenAI and Anthropic are model providers configured in settings.json,\\n not separate CLI runtimes. See README.md for model configuration.\\n`);
303
337
  process.exit(0);
304
338
  }
305
339
 
@@ -717,6 +751,52 @@ function convertClaudeCommandToCodexSkill(content, skillName) {
717
751
  return `---\nname: ${yamlQuote(skillName)}\ndescription: ${yamlQuote(description)}\nmetadata:\n short-description: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
718
752
  }
719
753
 
754
+ function convertClaudeToQwenAgent(content) {
755
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
756
+ if (!frontmatter) return content;
757
+
758
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
759
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
760
+
761
+ // Qwen Code uses simple markdown. We'll put name and description in the body.
762
+ return `# Agent: ${name}\n\n${description}\n\n${body.trimStart()}`;
763
+ }
764
+
765
+ function convertClaudeToKimiSkill(content, skillName) {
766
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
767
+ if (!frontmatter) return content;
768
+
769
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
770
+
771
+ // Reconstruct as simple Markdown
772
+ return `# Skill: ${skillName}\n\n${description}\n\n${body.trimStart()}`;
773
+ }
774
+
775
+ function convertClaudeToKimiAgent(content) {
776
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
777
+ if (!frontmatter) return content;
778
+
779
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
780
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
781
+
782
+ // Kimi Code uses simple markdown
783
+ return `# Agent: ${name}\n\n${description}\n\n${body.trimStart()}`;
784
+ }
785
+
786
+ /**
787
+ * Convert a Claude command (.md) to a Qwen skill (SKILL.md).
788
+ * Strips frontmatter and formats as simple Markdown.
789
+ */
790
+ function convertClaudeToQwenSkill(content, skillName) {
791
+ const { frontmatter, body } = extractFrontmatterAndBody(content);
792
+ if (!frontmatter) return content;
793
+
794
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
795
+
796
+ // Reconstruct as simple Markdown
797
+ return `# Skill: ${skillName}\n\n${description}\n\n${body.trimStart()}`;
798
+ }
799
+
720
800
  /**
721
801
  * Convert Claude Code agent markdown to Codex agent format.
722
802
  * Applies base markdown conversions, then adds a <codex_agent_role> header
@@ -860,6 +940,23 @@ function mergeCodexConfig(configPath, EZ_AgentsBlock) {
860
940
  fs.writeFileSync(configPath, content);
861
941
  }
862
942
 
943
+ function findCopilotInstructionsMarkerPair(content) {
944
+ const markerPairs = [
945
+ [EZ_COPILOT_INSTRUCTIONS_MARKER, EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER],
946
+ [LEGACY_EZ_COPILOT_INSTRUCTIONS_MARKER, LEGACY_EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER],
947
+ ];
948
+
949
+ for (const [openMarker, closeMarker] of markerPairs) {
950
+ const openIndex = content.indexOf(openMarker);
951
+ const closeIndex = content.indexOf(closeMarker);
952
+ if (openIndex !== -1 && closeIndex !== -1 && closeIndex >= openIndex) {
953
+ return { openIndex, closeIndex, closeMarker };
954
+ }
955
+ }
956
+
957
+ return null;
958
+ }
959
+
863
960
  /**
864
961
  * Merge EZ_Agents instructions into copilot-instructions.md.
865
962
  * Three cases: new file, existing with markers, existing without markers.
@@ -878,13 +975,12 @@ function mergeCopilotInstructions(filePath, EZ_AgentsContent) {
878
975
  }
879
976
 
880
977
  const existing = fs.readFileSync(filePath, 'utf8');
881
- const openIndex = existing.indexOf(EZ_COPILOT_INSTRUCTIONS_MARKER);
882
- const closeIndex = existing.indexOf(EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
978
+ const markerMatch = findCopilotInstructionsMarkerPair(existing);
883
979
 
884
980
  // Case 2: Has EZ_Agents markers — replace between markers
885
- if (openIndex !== -1 && closeIndex !== -1) {
886
- const before = existing.substring(0, openIndex).trimEnd();
887
- const after = existing.substring(closeIndex + EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart();
981
+ if (markerMatch) {
982
+ const before = existing.substring(0, markerMatch.openIndex).trimEnd();
983
+ const after = existing.substring(markerMatch.closeIndex + markerMatch.closeMarker.length).trimStart();
888
984
  let newContent = '';
889
985
  if (before) newContent += before + '\n\n';
890
986
  newContent += EZ_AgentsBlock;
@@ -906,12 +1002,11 @@ function mergeCopilotInstructions(filePath, EZ_AgentsContent) {
906
1002
  * @returns {string|null} - Cleaned content or null if empty
907
1003
  */
908
1004
  function stripEzAgentsFromCopilotInstructions(content) {
909
- const openIndex = content.indexOf(EZ_COPILOT_INSTRUCTIONS_MARKER);
910
- const closeIndex = content.indexOf(EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER);
1005
+ const markerMatch = findCopilotInstructionsMarkerPair(content);
911
1006
 
912
- if (openIndex !== -1 && closeIndex !== -1) {
913
- const before = content.substring(0, openIndex).trimEnd();
914
- const after = content.substring(closeIndex + EZ_COPILOT_INSTRUCTIONS_CLOSE_MARKER.length).trimStart();
1007
+ if (markerMatch) {
1008
+ const before = content.substring(0, markerMatch.openIndex).trimEnd();
1009
+ const after = content.substring(markerMatch.closeIndex + markerMatch.closeMarker.length).trimStart();
915
1010
  const cleaned = (before + (before && after ? '\n\n' : '') + after).trim();
916
1011
  if (!cleaned) return null;
917
1012
  return cleaned + '\n';
@@ -930,7 +1025,7 @@ function installCodexConfig(targetDir, agentsSrc) {
930
1025
  const agentsTomlDir = path.join(targetDir, 'agents');
931
1026
  fs.mkdirSync(agentsTomlDir, { recursive: true });
932
1027
 
933
- const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('EZ-') && f.endsWith('.md'));
1028
+ const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('ez-') && f.endsWith('.md'));
934
1029
  const agents = [];
935
1030
 
936
1031
  // Compute the Codex pathPrefix for replacing .claude paths
@@ -1389,6 +1484,176 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false
1389
1484
  recurse(srcDir, prefix);
1390
1485
  }
1391
1486
 
1487
+ /**
1488
+ * Copy Claude commands as Kimi Code skills — one folder per skill with SKILL.md.
1489
+ */
1490
+ function copyCommandsAsKimiSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
1491
+ if (!fs.existsSync(srcDir)) {
1492
+ return;
1493
+ }
1494
+
1495
+ fs.mkdirSync(skillsDir, { recursive: true });
1496
+
1497
+ // Remove previous EZ Kimi skills
1498
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
1499
+ for (const entry of existing) {
1500
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
1501
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
1502
+ }
1503
+ }
1504
+
1505
+ function recurse(currentSrcDir, currentPrefix) {
1506
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
1507
+
1508
+ for (const entry of entries) {
1509
+ const srcPath = path.join(currentSrcDir, entry.name);
1510
+ if (entry.isDirectory()) {
1511
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
1512
+ continue;
1513
+ }
1514
+
1515
+ if (!entry.name.endsWith('.md')) {
1516
+ continue;
1517
+ }
1518
+
1519
+ const baseName = entry.name.replace('.md', '');
1520
+ const skillName = `${currentPrefix}-${baseName}`;
1521
+ const skillDir = path.join(skillsDir, skillName);
1522
+ fs.mkdirSync(skillDir, { recursive: true });
1523
+
1524
+ let content = fs.readFileSync(srcPath, 'utf8');
1525
+ const globalClaudeRegex = /~\/\.claude\//g;
1526
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
1527
+ const localClaudeRegex = /\.\/\.claude\//g;
1528
+ const kimiDirRegex = /~\/\.kimi\//g;
1529
+ content = content.replace(globalClaudeRegex, pathPrefix);
1530
+ content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
1531
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
1532
+ content = content.replace(kimiDirRegex, pathPrefix);
1533
+ content = processAttribution(content, getCommitAttribution(runtime));
1534
+ content = convertClaudeToKimiSkill(content, skillName);
1535
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
1536
+ }
1537
+ }
1538
+
1539
+ recurse(srcDir, prefix);
1540
+ }
1541
+
1542
+ /**
1543
+ * Copy Claude commands as Qwen Code commands — flat structure in commands/ez/.
1544
+ * Qwen Code uses ~/.qwen/commands/ez/*.md (like Claude Code ~/.claude/commands/ez/)
1545
+ */
1546
+ function copyCommandsAsQwenCommands(srcDir, commandsDir, prefix, pathPrefix, runtime) {
1547
+ if (!fs.existsSync(srcDir)) {
1548
+ return;
1549
+ }
1550
+
1551
+ fs.mkdirSync(commandsDir, { recursive: true });
1552
+
1553
+ // Remove previous EZ Qwen commands
1554
+ const existing = fs.readdirSync(commandsDir, { withFileTypes: true });
1555
+ for (const entry of existing) {
1556
+ if (entry.isFile() && entry.name.startsWith(`${prefix}-`) && entry.name.endsWith('.md')) {
1557
+ fs.rmSync(path.join(commandsDir, entry.name));
1558
+ }
1559
+ }
1560
+
1561
+ function recurse(currentSrcDir, currentPrefix) {
1562
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
1563
+
1564
+ for (const entry of entries) {
1565
+ const srcPath = path.join(currentSrcDir, entry.name);
1566
+ if (entry.isDirectory()) {
1567
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
1568
+ continue;
1569
+ }
1570
+
1571
+ if (!entry.name.endsWith('.md')) {
1572
+ continue;
1573
+ }
1574
+
1575
+ const baseName = entry.name.replace('.md', '');
1576
+ const commandName = `${currentPrefix}-${baseName}`;
1577
+ const commandFile = path.join(commandsDir, `${commandName}.md`);
1578
+
1579
+ let content = fs.readFileSync(srcPath, 'utf8');
1580
+ const globalClaudeRegex = /~\/\.claude\//g;
1581
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
1582
+ const localClaudeRegex = /\.\/\.claude\//g;
1583
+ const qwenDirRegex = /~\/\.qwen\//g;
1584
+ content = content.replace(globalClaudeRegex, pathPrefix);
1585
+ content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
1586
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
1587
+ content = content.replace(qwenDirRegex, pathPrefix);
1588
+ content = processAttribution(content, getCommitAttribution(runtime));
1589
+
1590
+ // Qwen Code uses simple markdown commands (no SKILL.md wrapper)
1591
+ // Just copy the content as-is with path replacements
1592
+ fs.writeFileSync(commandFile, content);
1593
+ }
1594
+ }
1595
+
1596
+ recurse(srcDir, prefix);
1597
+ }
1598
+
1599
+ /**
1600
+ * Copy Claude commands as Qwen Code skills — one folder per skill with SKILL.md.
1601
+ * Qwen Code uses the same skills/ structure as Codex but with simpler format (no adapters needed).
1602
+ * @deprecated Use copyCommandsAsQwenCommands instead - Qwen Code uses commands/ez/ not skills/
1603
+ */
1604
+ function copyCommandsAsQwenSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
1605
+ if (!fs.existsSync(srcDir)) {
1606
+ return;
1607
+ }
1608
+
1609
+ fs.mkdirSync(skillsDir, { recursive: true });
1610
+
1611
+ // Remove previous EZ Qwen skills
1612
+ const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
1613
+ for (const entry of existing) {
1614
+ if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
1615
+ fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
1616
+ }
1617
+ }
1618
+
1619
+ function recurse(currentSrcDir, currentPrefix) {
1620
+ const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
1621
+
1622
+ for (const entry of entries) {
1623
+ const srcPath = path.join(currentSrcDir, entry.name);
1624
+ if (entry.isDirectory()) {
1625
+ recurse(srcPath, `${currentPrefix}-${entry.name}`);
1626
+ continue;
1627
+ }
1628
+
1629
+ if (!entry.name.endsWith('.md')) {
1630
+ continue;
1631
+ }
1632
+
1633
+ const baseName = entry.name.replace('.md', '');
1634
+ const skillName = `${currentPrefix}-${baseName}`;
1635
+ const skillDir = path.join(skillsDir, skillName);
1636
+ fs.mkdirSync(skillDir, { recursive: true });
1637
+
1638
+ let content = fs.readFileSync(srcPath, 'utf8');
1639
+ const globalClaudeRegex = /~\/\.claude\//g;
1640
+ const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
1641
+ const localClaudeRegex = /\.\/\.claude\//g;
1642
+ const qwenDirRegex = /~\/\.qwen\//g;
1643
+ content = content.replace(globalClaudeRegex, pathPrefix);
1644
+ content = content.replace(globalClaudeHomeRegex, toHomePrefix(pathPrefix));
1645
+ content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
1646
+ content = content.replace(qwenDirRegex, pathPrefix);
1647
+ content = processAttribution(content, getCommitAttribution(runtime));
1648
+ // Qwen Code uses simple markdown skills without frontmatter adapters
1649
+ content = convertClaudeToQwenSkill(content, skillName);
1650
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
1651
+ }
1652
+ }
1653
+
1654
+ recurse(srcDir, prefix);
1655
+ }
1656
+
1392
1657
  /**
1393
1658
  * Recursively copy directory, replacing paths in .md files
1394
1659
  * Deletes existing destDir first to remove orphaned files from previous versions
@@ -1552,6 +1817,8 @@ function uninstall(isGlobal, runtime = 'claude') {
1552
1817
  const isOpencode = runtime === 'opencode';
1553
1818
  const isCodex = runtime === 'codex';
1554
1819
  const isCopilot = runtime === 'copilot';
1820
+ const isQwen = runtime === 'qwen';
1821
+ const isKimi = runtime === 'kimi';
1555
1822
  const dirName = getDirName(runtime);
1556
1823
 
1557
1824
  // Get the target directory based on runtime and install type
@@ -1568,6 +1835,7 @@ function uninstall(isGlobal, runtime = 'claude') {
1568
1835
  if (runtime === 'gemini') runtimeLabel = 'Gemini';
1569
1836
  if (runtime === 'codex') runtimeLabel = 'Codex';
1570
1837
  if (runtime === 'copilot') runtimeLabel = 'Copilot';
1838
+ if (runtime === 'qwen') runtimeLabel = 'Qwen Code';
1571
1839
 
1572
1840
  console.log(` Uninstalling EZ_Agents from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
1573
1841
 
@@ -1594,8 +1862,8 @@ function uninstall(isGlobal, runtime = 'claude') {
1594
1862
  }
1595
1863
  console.log(` ${green}✓${reset} Removed EZ_Agents commands from command/`);
1596
1864
  }
1597
- } else if (isCodex) {
1598
- // Codex: remove skills/ez-*/SKILL.md skill directories
1865
+ } else if (isCodex || isQwen || isKimi) {
1866
+ // Codex/Qwen/Kimi: remove skills/ez-*/SKILL.md skill directories
1599
1867
  const skillsDir = path.join(targetDir, 'skills');
1600
1868
  if (fs.existsSync(skillsDir)) {
1601
1869
  let skillCount = 0;
@@ -1608,41 +1876,46 @@ function uninstall(isGlobal, runtime = 'claude') {
1608
1876
  }
1609
1877
  if (skillCount > 0) {
1610
1878
  removedCount++;
1611
- console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
1879
+ let runtimeLabel = 'Codex';
1880
+ if (isQwen) runtimeLabel = 'Qwen Code';
1881
+ if (isKimi) runtimeLabel = 'Kimi Code';
1882
+ console.log(` ${green}✓${reset} Removed ${skillCount} ${runtimeLabel} skills`);
1612
1883
  }
1613
1884
  }
1614
1885
 
1615
- // Codex: remove EZ agent .toml config files
1616
- const codexAgentsDir = path.join(targetDir, 'agents');
1617
- if (fs.existsSync(codexAgentsDir)) {
1618
- const tomlFiles = fs.readdirSync(codexAgentsDir);
1619
- let tomlCount = 0;
1620
- for (const file of tomlFiles) {
1621
- if (file.startsWith('ez-') && file.endsWith('.toml')) {
1622
- fs.unlinkSync(path.join(codexAgentsDir, file));
1623
- tomlCount++;
1886
+ if (isCodex) {
1887
+ // Codex: remove EZ agent .toml config files
1888
+ const codexAgentsDir = path.join(targetDir, 'agents');
1889
+ if (fs.existsSync(codexAgentsDir)) {
1890
+ const tomlFiles = fs.readdirSync(codexAgentsDir);
1891
+ let tomlCount = 0;
1892
+ for (const file of tomlFiles) {
1893
+ if (file.startsWith('ez-') && file.endsWith('.toml')) {
1894
+ fs.unlinkSync(path.join(codexAgentsDir, file));
1895
+ tomlCount++;
1896
+ }
1897
+ }
1898
+ if (tomlCount > 0) {
1899
+ removedCount++;
1900
+ console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
1624
1901
  }
1625
1902
  }
1626
- if (tomlCount > 0) {
1627
- removedCount++;
1628
- console.log(` ${green}✓${reset} Removed ${tomlCount} agent .toml configs`);
1629
- }
1630
- }
1631
1903
 
1632
- // Codex: clean EZ sections from config.toml
1633
- const configPath = path.join(targetDir, 'config.toml');
1634
- if (fs.existsSync(configPath)) {
1635
- const content = fs.readFileSync(configPath, 'utf8');
1636
- const cleaned = stripEzAgentsFromCodexConfig(content);
1637
- if (cleaned === null) {
1638
- // File is empty after stripping — delete it
1639
- fs.unlinkSync(configPath);
1640
- removedCount++;
1641
- console.log(` ${green}✓${reset} Removed config.toml (was EZ-only)`);
1642
- } else if (cleaned !== content) {
1643
- fs.writeFileSync(configPath, cleaned);
1644
- removedCount++;
1645
- console.log(` ${green}✓${reset} Cleaned EZ_Agents sections from config.toml`);
1904
+ // Codex: clean EZ sections from config.toml
1905
+ const configPath = path.join(targetDir, 'config.toml');
1906
+ if (fs.existsSync(configPath)) {
1907
+ const content = fs.readFileSync(configPath, 'utf8');
1908
+ const cleaned = stripEzAgentsFromCodexConfig(content);
1909
+ if (cleaned === null) {
1910
+ // File is empty after stripping — delete it
1911
+ fs.unlinkSync(configPath);
1912
+ removedCount++;
1913
+ console.log(` ${green}✓${reset} Removed config.toml (was EZ-only)`);
1914
+ } else if (cleaned !== content) {
1915
+ fs.writeFileSync(configPath, cleaned);
1916
+ removedCount++;
1917
+ console.log(` ${green}✓${reset} Cleaned EZ_Agents sections from config.toml`);
1918
+ }
1646
1919
  }
1647
1920
  }
1648
1921
  } else if (isCopilot) {
@@ -2208,6 +2481,8 @@ function install(isGlobal, runtime = 'claude') {
2208
2481
  const isGemini = runtime === 'gemini';
2209
2482
  const isCodex = runtime === 'codex';
2210
2483
  const isCopilot = runtime === 'copilot';
2484
+ const isQwen = runtime === 'qwen';
2485
+ const isKimi = runtime === 'kimi';
2211
2486
  const dirName = getDirName(runtime);
2212
2487
  const src = path.join(__dirname, '..');
2213
2488
 
@@ -2233,6 +2508,8 @@ function install(isGlobal, runtime = 'claude') {
2233
2508
  if (isGemini) runtimeLabel = 'Gemini';
2234
2509
  if (isCodex) runtimeLabel = 'Codex';
2235
2510
  if (isCopilot) runtimeLabel = 'Copilot';
2511
+ if (isQwen) runtimeLabel = 'Qwen Code';
2512
+ if (isKimi) runtimeLabel = 'Kimi Code';
2236
2513
 
2237
2514
  console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
2238
2515
 
@@ -2245,7 +2522,7 @@ function install(isGlobal, runtime = 'claude') {
2245
2522
  // Clean up orphaned files from previous versions
2246
2523
  cleanupOrphanedFiles(targetDir);
2247
2524
 
2248
- // OpenCode uses command/ (flat), Codex uses skills/, Claude/Gemini use commands/ez/
2525
+ // OpenCode uses command/ (flat), Codex/Copilot/Qwen use skills/, Claude/Gemini use commands/ez/
2249
2526
  if (isOpencode) {
2250
2527
  // OpenCode: flat structure in command/ directory
2251
2528
  const commandDir = path.join(targetDir, 'command');
@@ -2260,16 +2537,41 @@ function install(isGlobal, runtime = 'claude') {
2260
2537
  } else {
2261
2538
  failures.push('command/ez-*');
2262
2539
  }
2263
- } else if (isCodex) {
2540
+ } else if (isCodex || isKimi) {
2541
+ // Codex, Kimi: skills structure in skills/ directory
2264
2542
  const skillsDir = path.join(targetDir, 'skills');
2265
2543
  const ezSrc = path.join(src, 'commands', 'ez');
2266
- copyCommandsAsCodexSkills(ezSrc, skillsDir, 'ez', pathPrefix, runtime);
2544
+ if (isKimi) {
2545
+ copyCommandsAsKimiSkills(ezSrc, skillsDir, 'ez', pathPrefix, runtime);
2546
+ } else {
2547
+ copyCommandsAsCodexSkills(ezSrc, skillsDir, 'ez', pathPrefix, runtime);
2548
+ }
2267
2549
  const installedSkillNames = listCodexSkillNames(skillsDir);
2268
2550
  if (installedSkillNames.length > 0) {
2269
2551
  console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
2270
2552
  } else {
2271
2553
  failures.push('skills/ez-*');
2272
2554
  }
2555
+ } else if (isQwen) {
2556
+ // Qwen Code: commands structure in commands/ez/ directory (like Claude Code/Gemini)
2557
+ // Qwen Code uses ~/.qwen/commands/ez/*.md, NOT ~/.qwen/skills/
2558
+ const commandsDir = path.join(targetDir, 'commands');
2559
+ const ezCommandsDir = path.join(commandsDir, 'ez');
2560
+ fs.mkdirSync(ezCommandsDir, { recursive: true });
2561
+
2562
+ const ezSrc = path.join(src, 'commands', 'ez');
2563
+ copyCommandsAsQwenCommands(ezSrc, ezCommandsDir, 'ez', pathPrefix, runtime);
2564
+
2565
+ if (fs.existsSync(ezCommandsDir)) {
2566
+ const count = fs.readdirSync(ezCommandsDir).filter(f => f.endsWith('.md')).length;
2567
+ if (count > 0) {
2568
+ console.log(` ${green}✓${reset} Installed ${count} commands to commands/ez/`);
2569
+ } else {
2570
+ failures.push('commands/ez/*.md');
2571
+ }
2572
+ } else {
2573
+ failures.push('commands/ez/*.md');
2574
+ }
2273
2575
  } else if (isCopilot) {
2274
2576
  const skillsDir = path.join(targetDir, 'skills');
2275
2577
  const ezSrc = path.join(src, 'commands', 'ez');
@@ -2356,6 +2658,10 @@ function install(isGlobal, runtime = 'claude') {
2356
2658
  content = convertClaudeAgentToCodexAgent(content);
2357
2659
  } else if (isCopilot) {
2358
2660
  content = convertClaudeAgentToCopilotAgent(content, isGlobal);
2661
+ } else if (isQwen) {
2662
+ content = convertClaudeToQwenAgent(content);
2663
+ } else if (isKimi) {
2664
+ content = convertClaudeToKimiAgent(content);
2359
2665
  }
2360
2666
  const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
2361
2667
  fs.writeFileSync(path.join(agentsDest, destName), content);
@@ -2389,7 +2695,7 @@ function install(isGlobal, runtime = 'claude') {
2389
2695
  failures.push('VERSION');
2390
2696
  }
2391
2697
 
2392
- if (!isCodex && !isCopilot) {
2698
+ if (!isCodex && !isCopilot && !isQwen && !isKimi) {
2393
2699
  // Write package.json to force CommonJS mode for EZ scripts
2394
2700
  // Prevents "require is not defined" errors when project has "type": "module"
2395
2701
  // Node.js walks up looking for package.json - this stops inheritance from project
@@ -2493,6 +2799,12 @@ function install(isGlobal, runtime = 'claude') {
2493
2799
  return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
2494
2800
  }
2495
2801
 
2802
+ if (isQwen || isKimi) {
2803
+ // Qwen/Kimi: no settings.json hooks, no statusline (like Codex/Copilot)
2804
+ // Skills are installed directly in skills/ez-*/SKILL.md
2805
+ return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
2806
+ }
2807
+
2496
2808
  // Configure statusline and hooks in settings.json
2497
2809
  // Gemini uses AfterTool instead of PostToolUse for post-tool hooks
2498
2810
  const postToolEvent = runtime === 'gemini' ? 'AfterTool' : 'PostToolUse';
@@ -2576,8 +2888,10 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
2576
2888
  const isOpencode = runtime === 'opencode';
2577
2889
  const isCodex = runtime === 'codex';
2578
2890
  const isCopilot = runtime === 'copilot';
2891
+ const isQwen = runtime === 'qwen';
2892
+ const isKimi = runtime === 'kimi';
2579
2893
 
2580
- if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot) {
2894
+ if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isQwen && !isKimi) {
2581
2895
  settings.statusLine = {
2582
2896
  type: 'command',
2583
2897
  command: statuslineCommand
@@ -2586,7 +2900,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
2586
2900
  }
2587
2901
 
2588
2902
  // Write settings when runtime supports settings.json
2589
- if (!isCodex && !isCopilot) {
2903
+ if (!isCodex && !isCopilot && !isQwen && !isKimi) {
2590
2904
  writeSettings(settingsPath, settings);
2591
2905
  }
2592
2906
 
@@ -2600,11 +2914,12 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
2600
2914
  if (runtime === 'gemini') program = 'Gemini';
2601
2915
  if (runtime === 'codex') program = 'Codex';
2602
2916
  if (runtime === 'copilot') program = 'Copilot';
2917
+ if (runtime === 'qwen') program = 'Qwen Code';
2918
+ if (runtime === 'kimi') program = 'Kimi Code';
2603
2919
 
2604
2920
  let command = '/ez:new-project';
2605
- if (runtime === 'opencode') command = '/ez-new-project';
2921
+ if (runtime === 'opencode' || runtime === 'gemini' || runtime === 'copilot' || runtime === 'qwen' || runtime === 'kimi') command = '/ez-new-project';
2606
2922
  if (runtime === 'codex') command = '$ez-new-project';
2607
- if (runtime === 'copilot') command = '/ez-new-project';
2608
2923
  console.log(`
2609
2924
  ${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
2610
2925
 
@@ -2687,15 +3002,24 @@ function promptRuntime(callback) {
2687
3002
  ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
2688
3003
  ${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
2689
3004
  ${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
2690
- ${cyan}6${reset}) All
3005
+ ${cyan}6${reset}) Qwen Code ${dim}(~/.qwen)${reset} - Alibaba Qwen official CLI
3006
+ ${cyan}7${reset}) Kimi Code ${dim}(~/.kimi)${reset} - Moonshot Kimi official CLI
3007
+ ${cyan}8${reset}) All ${dim}(all 7 runtimes above)${reset}
3008
+
3009
+ ${dim}Note: OpenAI and Anthropic are model providers configured within each runtime's settings,
3010
+ not separate CLI runtimes. See README.md for model configuration.${reset}
2691
3011
  `);
2692
3012
 
2693
3013
  rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
2694
3014
  answered = true;
2695
3015
  rl.close();
2696
3016
  const choice = answer.trim() || '1';
2697
- if (choice === '6') {
2698
- callback(['claude', 'opencode', 'gemini', 'codex', 'copilot']);
3017
+ if (choice === '8') {
3018
+ callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'qwen', 'kimi']);
3019
+ } else if (choice === '7') {
3020
+ callback(['kimi']);
3021
+ } else if (choice === '6') {
3022
+ callback(['qwen']);
2699
3023
  } else if (choice === '5') {
2700
3024
  callback(['copilot']);
2701
3025
  } else if (choice === '4') {
@@ -2767,26 +3091,66 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
2767
3091
  }
2768
3092
 
2769
3093
  const statuslineRuntimes = ['claude', 'gemini'];
2770
- const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
2771
-
2772
- const finalize = (shouldInstallStatusline) => {
2773
- for (const result of results) {
2774
- const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline;
2775
- finishInstall(
2776
- result.settingsPath,
2777
- result.settings,
2778
- result.statuslineCommand,
2779
- useStatusline,
2780
- result.runtime,
2781
- isGlobal
2782
- );
2783
- }
3094
+ const statuslineResults = results.filter(r => statuslineRuntimes.includes(r.runtime));
3095
+
3096
+ // Handle statusline per-runtime to avoid conflicts
3097
+ // Each runtime with settings.json gets its own statusline prompt
3098
+ const finalizePerRuntime = (runtimeResult, shouldInstallStatusline) => {
3099
+ const useStatusline = statuslineRuntimes.includes(runtimeResult.runtime) && shouldInstallStatusline;
3100
+ finishInstall(
3101
+ runtimeResult.settingsPath,
3102
+ runtimeResult.settings,
3103
+ runtimeResult.statuslineCommand,
3104
+ useStatusline,
3105
+ runtimeResult.runtime,
3106
+ isGlobal
3107
+ );
2784
3108
  };
2785
3109
 
2786
- if (primaryStatuslineResult) {
2787
- handleStatusline(primaryStatuslineResult.settings, isInteractive, finalize);
3110
+ if (statuslineResults.length === 0) {
3111
+ // No runtimes support statusline - finalize all without statusline
3112
+ for (const result of results) {
3113
+ finalizePerRuntime(result, false);
3114
+ }
3115
+ } else if (statuslineResults.length === 1) {
3116
+ // Single runtime with statusline support - prompt once
3117
+ const singleResult = statuslineResults[0];
3118
+ handleStatusline(singleResult.settings, isInteractive, (shouldInstall) => {
3119
+ finalizePerRuntime(singleResult, shouldInstall);
3120
+ // Finalize other runtimes without statusline
3121
+ for (const result of results) {
3122
+ if (!statuslineRuntimes.includes(result.runtime)) {
3123
+ finalizePerRuntime(result, false);
3124
+ }
3125
+ }
3126
+ });
2788
3127
  } else {
2789
- finalize(false);
3128
+ // Multiple runtimes with statusline support - prompt for each
3129
+ let currentIndex = 0;
3130
+ const statuslineChoices = new Array(statuslineResults.length).fill(false);
3131
+
3132
+ const promptNextStatusline = () => {
3133
+ if (currentIndex >= statuslineResults.length) {
3134
+ // All statusline prompts done - finalize all runtimes
3135
+ for (let i = 0; i < results.length; i++) {
3136
+ const result = results[i];
3137
+ const statuslineIndex = statuslineResults.findIndex(r => r.runtime === result.runtime);
3138
+ const shouldInstall = statuslineIndex !== -1 ? statuslineChoices[statuslineIndex] : false;
3139
+ finalizePerRuntime(result, shouldInstall);
3140
+ }
3141
+ return;
3142
+ }
3143
+
3144
+ const currentResult = statuslineResults[currentIndex];
3145
+ const currentRuntimeIndex = currentIndex;
3146
+ handleStatusline(currentResult.settings, isInteractive, (shouldInstall) => {
3147
+ statuslineChoices[currentRuntimeIndex] = shouldInstall;
3148
+ currentIndex++;
3149
+ promptNextStatusline();
3150
+ });
3151
+ };
3152
+
3153
+ promptNextStatusline();
2790
3154
  }
2791
3155
  }
2792
3156
 
@@ -2819,6 +3183,10 @@ if (process.env.EZ_AGENTS_TEST_MODE) {
2819
3183
  stripEzAgentsFromCopilotInstructions,
2820
3184
  writeManifest,
2821
3185
  reportLocalPatches,
3186
+ convertClaudeToQwenSkill,
3187
+ convertClaudeToQwenAgent,
3188
+ convertClaudeToKimiSkill,
3189
+ convertClaudeToKimiAgent,
2822
3190
  };
2823
3191
  } else {
2824
3192