stigmergy 1.3.13 → 1.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stigmergy",
3
- "version": "1.3.13",
3
+ "version": "1.3.15",
4
4
  "description": "Stigmergy CLI - Multi-Agents Cross-AI CLI Tools Collaboration System",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -16,6 +16,7 @@ const LocalSkillScanner = require('../../core/local_skill_scanner');
16
16
  const CLIHelpAnalyzer = require('../../core/cli_help_analyzer');
17
17
  const { CLI_TOOLS } = require('../../core/cli_tools');
18
18
  const { ensureSkillsCache } = require('../utils/skills_cache');
19
+ const packageJson = require('../../../package.json');
19
20
 
20
21
  // Import execution mode detection and CLI adapters
21
22
  const ExecutionModeDetector = require('../../core/execution_mode_detector');
@@ -150,7 +151,7 @@ async function handleInitCommand(options = {}) {
150
151
 
151
152
  // Create basic config
152
153
  const config = {
153
- version: '1.3.8',
154
+ version: packageJson.version,
154
155
  created: new Date().toISOString(),
155
156
  project: path.basename(projectDir)
156
157
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Resume Session Commands
3
- * Modular implementation for resume/resumesession/sg-resume commands
3
+ * Modular implementation for resume command
4
4
  */
5
5
 
6
6
  const chalk = require('chalk');
@@ -47,7 +47,7 @@ async function handleResumeCommand(args = [], options = {}) {
47
47
  console.log(chalk.yellow('[INFO] ResumeSession is an optional component for session recovery'));
48
48
 
49
49
  console.log(chalk.blue('\nšŸ“¦ To install ResumeSession:'));
50
- console.log(' npm install -g @stigmergy/resume');
50
+ console.log(' npm install -g resumesession');
51
51
  console.log('');
52
52
  console.log(chalk.blue('šŸ”§ ResumeSession provides:'));
53
53
  console.log(' • Cross-CLI session history');
@@ -100,18 +100,16 @@ function printResumeHelp() {
100
100
  console.log(chalk.cyan(`
101
101
  šŸ”„ Stigmergy Resume Session System
102
102
 
103
- šŸ“‹ ResumeSession forwards to the @stigmergy/resume CLI tool for session management.
103
+ šŸ“‹ ResumeSession forwards to the resumesession CLI tool for session management.
104
104
 
105
105
  šŸ› ļø Available Commands:
106
106
  stigmergy resume [args] Forward to resumesession CLI
107
- stigmergy resumesession [args] Same as resume (full name)
108
- stigmergy sg-resume [args] Same as resume (short alias)
109
107
 
110
108
  šŸ“¦ Requirements:
111
- @stigmergy/resume CLI tool must be installed separately.
109
+ resumesession CLI tool must be installed separately.
112
110
 
113
111
  šŸ’¾ Installation:
114
- npm install -g @stigmergy/resume
112
+ npm install -g resumesession
115
113
 
116
114
  šŸ” Common Usage:
117
115
  stigmergy resume list # Show available sessions
@@ -92,10 +92,11 @@ function addOAuthAuthArgs(toolName, args = []) {
92
92
  */
93
93
  async function main() {
94
94
  const program = new Command();
95
+ const packageJson = require('../../package.json');
95
96
 
96
97
  // Program setup
97
98
  program
98
- .version('1.3.8')
99
+ .version(packageJson.version)
99
100
  .description('Stigmergy CLI - Multi-Agents Cross-AI CLI Tools Collaboration System')
100
101
  .name('stigmergy');
101
102
 
@@ -319,7 +320,7 @@ async function main() {
319
320
  await handleAutoInstallCommand(options);
320
321
  });
321
322
 
322
- // Resume session commands
323
+ // Resume session command
323
324
  program
324
325
  .command('resume')
325
326
  .description('Resume session (forwards to @stigmergy/resume CLI tool)')
@@ -329,26 +330,8 @@ async function main() {
329
330
  await handleResumeCommand(args, options);
330
331
  });
331
332
 
332
- program
333
- .command('resumesession')
334
- .description('Resume session management (forwards to @stigmergy/resume)')
335
- .argument('[args...]', 'Arguments to pass to resumesession')
336
- .option('-v, --verbose', 'Verbose output')
337
- .action(async (args, options) => {
338
- await handleResumeCommand(args, options);
339
- });
340
-
341
- program
342
- .command('sg-resume')
343
- .description('Resume session management (short alias)')
344
- .argument('[args...]', 'Arguments to pass to resumesession')
345
- .option('-v, --verbose', 'Verbose output')
346
- .action(async (args, options) => {
347
- await handleResumeCommand(args, options);
348
- });
349
-
350
333
  // Route commands to CLI tools
351
- for (const tool of ['claude', 'gemini', 'qwen', 'codebuddy', 'codex', 'iflow', 'qodercli', 'copilot', 'kode', 'resumesession']) {
334
+ for (const tool of ['claude', 'gemini', 'qwen', 'codebuddy', 'codex', 'iflow', 'qodercli', 'copilot', 'kode', 'opencode', 'oh-my-opencode']) {
352
335
  program
353
336
  .command(tool)
354
337
  .description(`Use ${tool} CLI tool`)
@@ -76,6 +76,22 @@ const CLI_TOOLS = {
76
76
  hooksDir: path.join(os.homedir(), '.resumesession', 'hooks'),
77
77
  config: path.join(os.homedir(), '.resumesession', 'config.json'),
78
78
  },
79
+ opencode: {
80
+ name: 'OpenCode AI CLI',
81
+ version: 'opencode --version',
82
+ install: 'npm install -g opencode-ai',
83
+ hooksDir: path.join(os.homedir(), '.opencode', 'hooks'),
84
+ config: path.join(os.homedir(), '.opencode', 'config.json'),
85
+ },
86
+ 'oh-my-opencode': {
87
+ name: 'Oh-My-OpenCode Plugin Manager',
88
+ version: 'oh-my-opencode --version',
89
+ install: 'npm install bun -g && bunx oh-my-opencode install --no-tui --claude=no --chatgpt=no --gemini=no',
90
+ hooksDir: path.join(os.homedir(), '.opencode', 'plugins'),
91
+ config: path.join(os.homedir(), '.opencode', 'config.json'),
92
+ skipVersionCheck: true, // Version check may not work properly for bunx packages
93
+ requiresBun: true, // Requires bun runtime
94
+ },
79
95
  };
80
96
 
81
97
  /**
@@ -72,6 +72,24 @@ class CLIIntegrationManager {
72
72
  executable: 'copilot',
73
73
  mcp: ['cross_cli_execute'],
74
74
  },
75
+ opencode: {
76
+ name: 'OpenCode AI CLI',
77
+ executable: 'opencode',
78
+ skills: [
79
+ 'on_skill_invocation',
80
+ 'on_code_generation',
81
+ 'on_cross_cli_task',
82
+ ],
83
+ },
84
+ 'oh-my-opencode': {
85
+ name: 'Oh-My-OpenCode Plugin Manager',
86
+ executable: 'oh-my-opencode',
87
+ plugins: [
88
+ 'on_plugin_install',
89
+ 'on_plugin_load',
90
+ 'on_cross_cli_integration',
91
+ ],
92
+ },
75
93
  };
76
94
  }
77
95
 
@@ -20,7 +20,9 @@ class HookDeploymentManager {
20
20
  'codebuddy',
21
21
  'codex',
22
22
  'copilot',
23
- 'kode'
23
+ 'kode',
24
+ 'opencode',
25
+ 'oh-my-opencode'
24
26
  // Note: 'resumesession' is handled separately as a session recovery tool, not a regular CLI
25
27
  ];
26
28
 
@@ -7,7 +7,8 @@ class ResumeSessionGenerator {
7
7
  constructor() {
8
8
  this.supportedCLIs = [
9
9
  'claude', 'gemini', 'qwen', 'codebuddy', 'codex',
10
- 'iflow', 'qodercli', 'copilot', 'kode', 'resumesession'
10
+ 'iflow', 'qodercli', 'copilot', 'kode', 'opencode',
11
+ 'oh-my-opencode', 'resumesession'
11
12
  ];
12
13
  }
13
14
 
@@ -534,8 +535,8 @@ function buildQuery(input) {
534
535
  search: null
535
536
  };
536
537
 
537
- const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\s*', 'i'), '').trim();
538
- const parts = cleanInput.split(/\\\s+/).filter(p => p.length > 0);
538
+ const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + '${commandName}' + '\\\\\\s*', 'i'), '').trim();
539
+ const parts = cleanInput.split(/\\\\s+/).filter(p => p.length > 0);
539
540
 
540
541
  for (let i = 0; i < parts.length; i++) {
541
542
  const part = parts[i].toLowerCase();
@@ -624,8 +625,8 @@ class GeminiHistoryHandler {
624
625
  search: null
625
626
  };
626
627
 
627
- const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + this.commandName + '\\\\\s*', 'i'), '').trim();
628
- const parts = cleanInput.split(/\\\s+/).filter(p => p.length > 0);
628
+ const cleanInput = input.replace(new RegExp('^\\\\\\\\/?' + this.commandName + '\\\\\\s*', 'i'), '').trim();
629
+ const parts = cleanInput.split(/\\\\s+/).filter(p => p.length > 0);
629
630
 
630
631
  for (let i = 0; i < parts.length; i++) {
631
632
  const part = parts[i].toLowerCase();
@@ -59,8 +59,8 @@ class SkillsIntegrationGenerator {
59
59
  return `"""
60
60
  Claude CLI Skills Integration Hook
61
61
  Auto-generated by Stigmergy CLI v1.2.1
62
- Project: ${projectPath}
63
- Skills Directory: ${skillsDir}
62
+ Project: ${projectPath.replace(/\\/g, '/')}
63
+ Skills Directory: ${skillsDir.replace(/\\/g, '/')}
64
64
 
65
65
  This hook provides skills integration for Claude CLI with dynamic loading
66
66
  and execution of skills from the centralized skills directory.
@@ -75,7 +75,7 @@ from typing import Dict, List, Optional, Any
75
75
 
76
76
  class ClaudeSkillsHook:
77
77
  def __init__(self):
78
- self.skills_dir = Path("${skillsDir.replace(/\\/g, '\\\\')}")
78
+ self.skills_dir = Path("${skillsDir.replace(/\\/g, '/')}")
79
79
  self.loaded_skills = {}
80
80
  self.skill_metadata = {}
81
81
 
@@ -105,7 +105,21 @@ class ClaudeSkillsHook:
105
105
  def load_skill(self, skill_dir):
106
106
  """Load a single skill from directory"""
107
107
  try:
108
- # Check for skill implementation files
108
+ # Check for skill implementation files in scripts subdirectory
109
+ scripts_dir = skill_dir / 'scripts'
110
+ if scripts_dir.exists() and scripts_dir.is_dir():
111
+ for py_file in scripts_dir.glob("*.py"):
112
+ if py_file.name != "test_skill.py": # Skip test files
113
+ spec = importlib.util.spec_from_file_location(
114
+ f"{skill_dir.name}.{py_file.stem}", py_file
115
+ )
116
+ if spec and spec.loader:
117
+ module = importlib.util.module_from_spec(spec)
118
+ spec.loader.exec_module(module)
119
+ print(f"[CLAUDE_SKILLS] Loaded module: {py_file.name}")
120
+ return module
121
+
122
+ # Fallback: check for Python files in root directory
109
123
  for py_file in skill_dir.glob("*.py"):
110
124
  if py_file.name != "test_skill.py": # Skip test files
111
125
  spec = importlib.util.spec_from_file_location(
@@ -114,7 +128,14 @@ class ClaudeSkillsHook:
114
128
  if spec and spec.loader:
115
129
  module = importlib.util.module_from_spec(spec)
116
130
  spec.loader.exec_module(module)
131
+ print(f"[CLAUDE_SKILLS] Loaded module: {py_file.name}")
117
132
  return module
133
+
134
+ # If no Python files found, return the skill directory info
135
+ # This allows skills to be registered even without Python implementation
136
+ print(f"[CLAUDE_SKILLS] No Python files found, registering skill directory")
137
+ return {'type': 'directory', 'path': skill_dir}
138
+
118
139
  except Exception as e:
119
140
  print(f"[CLAUDE_SKILLS] Failed to load skill {skill_dir.name}: {e}")
120
141
  return None
@@ -127,13 +148,25 @@ class ClaudeSkillsHook:
127
148
  content = skill_md.read_text(encoding='utf-8')
128
149
  metadata = {}
129
150
 
130
- for line in content.split('\\n'):
131
- if line.startswith('## Description:'):
132
- metadata['description'] = line.replace('## Description:', '').strip()
133
- elif line.startswith('## Usage:'):
134
- metadata['usage'] = line.replace('## Usage:', '').strip()
135
- elif line.startswith('## Location:'):
136
- metadata['location'] = line.replace('## Location:', '').strip()
151
+ # Parse YAML-style frontmatter
152
+ lines = content.split('\\n')
153
+ in_frontmatter = False
154
+ for line in lines:
155
+ if line.strip() == '---':
156
+ in_frontmatter = not in_frontmatter
157
+ continue
158
+ if in_frontmatter:
159
+ if ':' in line:
160
+ key, value = line.split(':', 1)
161
+ metadata[key.strip()] = value.strip()
162
+ else:
163
+ # Fallback to old format
164
+ if line.startswith('## Description:'):
165
+ metadata['description'] = line.replace('## Description:', '').strip()
166
+ elif line.startswith('## Usage:'):
167
+ metadata['usage'] = line.replace('## Usage:', '').strip()
168
+ elif line.startswith('## Location:'):
169
+ metadata['location'] = line.replace('## Location:', '').strip()
137
170
 
138
171
  return metadata
139
172
  except Exception as e:
@@ -226,8 +259,8 @@ __all__ = ['on_prompt', 'initialize']
226
259
  generateGeminiSkillsIntegration(projectPath, skillsDir) {
227
260
  return `// Gemini CLI Skills Integration Extension
228
261
  // Auto-generated by Stigmergy CLI v1.2.1
229
- // Project: ${projectPath}
230
- // Skills Directory: ${skillsDir}
262
+ // Project: ${projectPath.replace(/\\/g, '/')}
263
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
231
264
 
232
265
  const fs = require('fs');
233
266
  const path = require('path');
@@ -235,7 +268,7 @@ const os = require('os');
235
268
 
236
269
  class GeminiSkillsExtension {
237
270
  constructor() {
238
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
271
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
239
272
  this.loadedSkills = new Map();
240
273
  this.skillMetadata = new Map();
241
274
  }
@@ -280,6 +313,27 @@ class GeminiSkillsExtension {
280
313
 
281
314
  async loadSkill(skillPath) {
282
315
  try {
316
+ // Check for skill implementation files in scripts subdirectory
317
+ const scriptsDir = path.join(skillPath, 'scripts');
318
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
319
+ const jsFiles = fs.readdirSync(scriptsDir).filter(file => file.endsWith('.js'));
320
+
321
+ for (const jsFile of jsFiles) {
322
+ if (!jsFile.includes('test')) {
323
+ const skillModulePath = path.join(scriptsDir, jsFile);
324
+ try {
325
+ delete require.cache[require.resolve(skillModulePath)];
326
+ const skillModule = require(skillModulePath);
327
+ console.log(\`[GEMINI_SKILLS] Loaded module: \${jsFile}\`);
328
+ return skillModule;
329
+ } catch (moduleError) {
330
+ console.warn(\`[GEMINI_SKILLS] Could not load module \${jsFile}: \${moduleError.message}\`);
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ // Fallback: check for JavaScript files in root directory
283
337
  const jsFiles = fs.readdirSync(skillPath).filter(file => file.endsWith('.js'));
284
338
 
285
339
  for (const jsFile of jsFiles) {
@@ -288,12 +342,18 @@ class GeminiSkillsExtension {
288
342
  try {
289
343
  delete require.cache[require.resolve(skillModulePath)];
290
344
  const skillModule = require(skillModulePath);
345
+ console.log(\`[GEMINI_SKILLS] Loaded module: \${jsFile}\`);
291
346
  return skillModule;
292
347
  } catch (moduleError) {
293
348
  console.warn(\`[GEMINI_SKILLS] Could not load module \${jsFile}: \${moduleError.message}\`);
294
349
  }
295
350
  }
296
351
  }
352
+
353
+ // If no JavaScript files found, return skill directory info
354
+ console.log(\`[GEMINI_SKILLS] No JavaScript files found, registering skill directory\`);
355
+ return { type: 'directory', path: skillPath };
356
+
297
357
  } catch (error) {
298
358
  console.error(\`[GEMINI_SKILLS] Failed to load skill from \${skillPath}:\`, error);
299
359
  }
@@ -454,8 +514,8 @@ if (typeof geminiCLI !== 'undefined') {
454
514
  generateQwenSkillsIntegration(projectPath, skillsDir) {
455
515
  return `// Qwen CLI Skills Integration Hook
456
516
  // Auto-generated by Stigmergy CLI v1.2.1
457
- // Project: ${projectPath}
458
- // Skills Directory: ${skillsDir}
517
+ // Project: ${projectPath.replace(/\\/g, '/')}
518
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
459
519
 
460
520
  const fs = require('fs');
461
521
  const path = require('path');
@@ -463,7 +523,7 @@ const path = require('path');
463
523
  class QwenSkillsHook {
464
524
  constructor() {
465
525
  this.toolName = 'qwen';
466
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
526
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
467
527
  this.loadedSkills = new Map();
468
528
  }
469
529
 
@@ -503,6 +563,25 @@ class QwenSkillsHook {
503
563
 
504
564
  async loadSkill(skillPath) {
505
565
  try {
566
+ // Check for skill implementation files in scripts subdirectory
567
+ const scriptsDir = path.join(skillPath, 'scripts');
568
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
569
+ const files = fs.readdirSync(scriptsDir);
570
+ const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
571
+
572
+ for (const jsFile of jsFiles) {
573
+ const skillModulePath = path.join(scriptsDir, jsFile);
574
+ try {
575
+ const skillModule = require(skillModulePath);
576
+ console.log(\`[QWEN_SKILLS] Loaded module: \${jsFile}\`);
577
+ return skillModule;
578
+ } catch (moduleError) {
579
+ console.warn(\`[QWEN_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
580
+ }
581
+ }
582
+ }
583
+
584
+ // Fallback: check for JavaScript files in root directory
506
585
  const files = fs.readdirSync(skillPath);
507
586
  const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
508
587
 
@@ -510,11 +589,17 @@ class QwenSkillsHook {
510
589
  const skillModulePath = path.join(skillPath, jsFile);
511
590
  try {
512
591
  const skillModule = require(skillModulePath);
592
+ console.log(\`[QWEN_SKILLS] Loaded module: \${jsFile}\`);
513
593
  return skillModule;
514
594
  } catch (moduleError) {
515
595
  console.warn(\`[QWEN_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
516
596
  }
517
597
  }
598
+
599
+ // If no JavaScript files found, return skill directory info
600
+ console.log(\`[QWEN_SKILLS] No JavaScript files found, registering skill directory\`);
601
+ return { type: 'directory', path: skillPath };
602
+
518
603
  } catch (error) {
519
604
  console.error(\`[QWEN_SKILLS] Failed to load skill from \${skillPath}:\`, error);
520
605
  }
@@ -618,8 +703,8 @@ module.exports = QwenSkillsHook;
618
703
  generateCodeBuddySkillsIntegration(projectPath, skillsDir) {
619
704
  return `// CodeBuddy CLI Skills Integration
620
705
  // Auto-generated by Stigmergy CLI v1.2.1
621
- // Project: ${projectPath}
622
- // Skills Directory: ${skillsDir}
706
+ // Project: ${projectPath.replace(/\\/g, '/')}
707
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
623
708
 
624
709
  const fs = require('fs');
625
710
  const path = require('path');
@@ -627,7 +712,7 @@ const path = require('path');
627
712
  class CodeBuddySkillsIntegration {
628
713
  constructor() {
629
714
  this.toolName = 'codebuddy';
630
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
715
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
631
716
  this.loadedSkills = new Map();
632
717
  }
633
718
 
@@ -667,6 +752,25 @@ class CodeBuddySkillsIntegration {
667
752
 
668
753
  async loadSkill(skillPath) {
669
754
  try {
755
+ // Check for skill implementation files in scripts subdirectory
756
+ const scriptsDir = path.join(skillPath, 'scripts');
757
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
758
+ const files = fs.readdirSync(scriptsDir);
759
+ const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
760
+
761
+ for (const jsFile of jsFiles) {
762
+ const skillModulePath = path.join(scriptsDir, jsFile);
763
+ try {
764
+ const skillModule = require(skillModulePath);
765
+ console.log(\`[CODEBUDDY_SKILLS] Loaded module: \${jsFile}\`);
766
+ return skillModule;
767
+ } catch (moduleError) {
768
+ console.warn(\`[CODEBUDDY_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
769
+ }
770
+ }
771
+ }
772
+
773
+ // Fallback: check for JavaScript files in root directory
670
774
  const files = fs.readdirSync(skillPath);
671
775
  const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
672
776
 
@@ -674,11 +778,17 @@ class CodeBuddySkillsIntegration {
674
778
  const skillModulePath = path.join(skillPath, jsFile);
675
779
  try {
676
780
  const skillModule = require(skillModulePath);
781
+ console.log(\`[CODEBUDDY_SKILLS] Loaded module: \${jsFile}\`);
677
782
  return skillModule;
678
783
  } catch (moduleError) {
679
784
  console.warn(\`[CODEBUDDY_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
680
785
  }
681
786
  }
787
+
788
+ // If no JavaScript files found, return skill directory info
789
+ console.log(\`[CODEBUDDY_SKILLS] No JavaScript files found, registering skill directory\`);
790
+ return { type: 'directory', path: skillPath };
791
+
682
792
  } catch (error) {
683
793
  console.error(\`[CODEBUDDY_SKILLS] Failed to load skill from \${skillPath}:\`, error);
684
794
  }
@@ -781,8 +891,8 @@ module.exports = CodeBuddySkillsIntegration;
781
891
  generateCodexSkillsIntegration(projectPath, skillsDir) {
782
892
  return `// Codex CLI Skills Integration Handler
783
893
  // Auto-generated by Stigmergy CLI v1.2.1
784
- // Project: ${projectPath}
785
- // Skills Directory: ${skillsDir}
894
+ // Project: ${projectPath.replace(/\\/g, '/')}
895
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
786
896
 
787
897
  const fs = require('fs');
788
898
  const path = require('path');
@@ -790,7 +900,7 @@ const path = require('path');
790
900
  class CodexSkillsHandler {
791
901
  constructor() {
792
902
  this.toolName = 'codex';
793
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
903
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
794
904
  this.loadedSkills = new Map();
795
905
  }
796
906
 
@@ -830,6 +940,25 @@ class CodexSkillsHandler {
830
940
 
831
941
  async loadSkill(skillPath) {
832
942
  try {
943
+ // Check for skill implementation files in scripts subdirectory
944
+ const scriptsDir = path.join(skillPath, 'scripts');
945
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
946
+ const files = fs.readdirSync(scriptsDir);
947
+ const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
948
+
949
+ for (const jsFile of jsFiles) {
950
+ const skillModulePath = path.join(scriptsDir, jsFile);
951
+ try {
952
+ const skillModule = require(skillModulePath);
953
+ console.log(\`[CODEX_SKILLS] Loaded module: \${jsFile}\`);
954
+ return skillModule;
955
+ } catch (moduleError) {
956
+ console.warn(\`[CODEX_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
957
+ }
958
+ }
959
+ }
960
+
961
+ // Fallback: check for JavaScript files in root directory
833
962
  const files = fs.readdirSync(skillPath);
834
963
  const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
835
964
 
@@ -837,11 +966,17 @@ class CodexSkillsHandler {
837
966
  const skillModulePath = path.join(skillPath, jsFile);
838
967
  try {
839
968
  const skillModule = require(skillModulePath);
969
+ console.log(\`[CODEX_SKILLS] Loaded module: \${jsFile}\`);
840
970
  return skillModule;
841
971
  } catch (moduleError) {
842
972
  console.warn(\`[CODEX_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
843
973
  }
844
974
  }
975
+
976
+ // If no JavaScript files found, return skill directory info
977
+ console.log(\`[CODEX_SKILLS] No JavaScript files found, registering skill directory\`);
978
+ return { type: 'directory', path: skillPath };
979
+
845
980
  } catch (error) {
846
981
  console.error(\`[CODEX_SKILLS] Failed to load skill from \${skillPath}:\`, error);
847
982
  }
@@ -945,8 +1080,8 @@ module.exports = CodexSkillsHandler;
945
1080
  generateQoderCliSkillsIntegration(projectPath, skillsDir) {
946
1081
  return `// QoderCLI Skills Integration Extension
947
1082
  // Auto-generated by Stigmergy CLI v1.2.1
948
- // Project: ${projectPath}
949
- // Skills Directory: ${skillsDir}
1083
+ // Project: ${projectPath.replace(/\\/g, '/')}
1084
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
950
1085
 
951
1086
  const fs = require('fs');
952
1087
  const path = require('path');
@@ -954,7 +1089,7 @@ const path = require('path');
954
1089
  class QoderCliSkillsExtension {
955
1090
  constructor() {
956
1091
  this.toolName = 'qodercli';
957
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
1092
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
958
1093
  this.loadedSkills = new Map();
959
1094
  }
960
1095
 
@@ -994,6 +1129,25 @@ class QoderCliSkillsExtension {
994
1129
 
995
1130
  async loadSkill(skillPath) {
996
1131
  try {
1132
+ // Check for skill implementation files in scripts subdirectory
1133
+ const scriptsDir = path.join(skillPath, 'scripts');
1134
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
1135
+ const files = fs.readdirSync(scriptsDir);
1136
+ const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
1137
+
1138
+ for (const jsFile of jsFiles) {
1139
+ const skillModulePath = path.join(scriptsDir, jsFile);
1140
+ try {
1141
+ const skillModule = require(skillModulePath);
1142
+ console.log(\`[QODERCLI_SKILLS] Loaded module: \${jsFile}\`);
1143
+ return skillModule;
1144
+ } catch (moduleError) {
1145
+ console.warn(\`[QODERCLI_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ // Fallback: check for JavaScript files in root directory
997
1151
  const files = fs.readdirSync(skillPath);
998
1152
  const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
999
1153
 
@@ -1001,11 +1155,17 @@ class QoderCliSkillsExtension {
1001
1155
  const skillModulePath = path.join(skillPath, jsFile);
1002
1156
  try {
1003
1157
  const skillModule = require(skillModulePath);
1158
+ console.log(\`[QODERCLI_SKILLS] Loaded module: \${jsFile}\`);
1004
1159
  return skillModule;
1005
1160
  } catch (moduleError) {
1006
1161
  console.warn(\`[QODERCLI_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
1007
1162
  }
1008
1163
  }
1164
+
1165
+ // If no JavaScript files found, return skill directory info
1166
+ console.log(\`[QODERCLI_SKILLS] No JavaScript files found, registering skill directory\`);
1167
+ return { type: 'directory', path: skillPath };
1168
+
1009
1169
  } catch (error) {
1010
1170
  console.error(\`[QODERCLI_SKILLS] Failed to load skill from \${skillPath}:\`, error);
1011
1171
  }
@@ -1109,8 +1269,8 @@ module.exports = QoderCliSkillsExtension;
1109
1269
 
1110
1270
  return `// ${className}
1111
1271
  // Auto-generated by Stigmergy CLI v1.2.1
1112
- // Project: ${projectPath}
1113
- // Skills Directory: ${skillsDir}
1272
+ // Project: ${projectPath.replace(/\\/g, '/')}
1273
+ // Skills Directory: ${skillsDir.replace(/\\/g, '/')}
1114
1274
 
1115
1275
  const fs = require('fs');
1116
1276
  const path = require('path');
@@ -1118,7 +1278,7 @@ const path = require('path');
1118
1278
  class ${className} {
1119
1279
  constructor() {
1120
1280
  this.toolName = '${cliName}';
1121
- this.skillsDir = path.join('${skillsDir.replace(/\\/g, '\\\\')}');
1281
+ this.skillsDir = path.join('${skillsDir.replace(/\\/g, '/')}');
1122
1282
  this.loadedSkills = new Map();
1123
1283
  }
1124
1284
 
@@ -1158,6 +1318,25 @@ class ${className} {
1158
1318
 
1159
1319
  async loadSkill(skillPath) {
1160
1320
  try {
1321
+ // Check for skill implementation files in scripts subdirectory
1322
+ const scriptsDir = path.join(skillPath, 'scripts');
1323
+ if (fs.existsSync(scriptsDir) && fs.statSync(scriptsDir).isDirectory()) {
1324
+ const files = fs.readdirSync(scriptsDir);
1325
+ const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
1326
+
1327
+ for (const jsFile of jsFiles) {
1328
+ const skillModulePath = path.join(scriptsDir, jsFile);
1329
+ try {
1330
+ const skillModule = require(skillModulePath);
1331
+ console.log(\`[\${this.toolName.toUpperCase()}_SKILLS] Loaded module: \${jsFile}\`);
1332
+ return skillModule;
1333
+ } catch (moduleError) {
1334
+ console.warn(\`[\${this.toolName.toUpperCase()}_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ // Fallback: check for JavaScript files in root directory
1161
1340
  const files = fs.readdirSync(skillPath);
1162
1341
  const jsFiles = files.filter(file => file.endsWith('.js') && !file.includes('test'));
1163
1342
 
@@ -1165,11 +1344,17 @@ class ${className} {
1165
1344
  const skillModulePath = path.join(skillPath, jsFile);
1166
1345
  try {
1167
1346
  const skillModule = require(skillModulePath);
1347
+ console.log(\`[\${this.toolName.toUpperCase()}_SKILLS] Loaded module: \${jsFile}\`);
1168
1348
  return skillModule;
1169
1349
  } catch (moduleError) {
1170
1350
  console.warn(\`[\${this.toolName.toUpperCase()}_SKILLS] Could not load \${jsFile}: \${moduleError.message}\`);
1171
1351
  }
1172
1352
  }
1353
+
1354
+ // If no JavaScript files found, return skill directory info
1355
+ console.log(\`[\${this.toolName.toUpperCase()}_SKILLS] No JavaScript files found, registering skill directory\`);
1356
+ return { type: 'directory', path: skillPath };
1357
+
1173
1358
  } catch (error) {
1174
1359
  console.error(\`[\${this.toolName.toUpperCase()}_SKILLS] Failed to load skill from \${skillPath}:\`, error);
1175
1360
  }
@@ -465,6 +465,31 @@ class EnhancedCLIInstaller {
465
465
  };
466
466
  }
467
467
 
468
+ // Check if tool requires bun runtime
469
+ if (toolInfo.requiresBun) {
470
+ const bunAvailable = await this.checkBunAvailable();
471
+ if (!bunAvailable) {
472
+ this.log('warn', `${toolInfo.name} requires bun, but bun is not installed`);
473
+ this.log('info', `Installing bun first...`);
474
+
475
+ const bunResult = await this.executeInstallationCommand('npm install bun -g');
476
+
477
+ if (!bunResult.success) {
478
+ return {
479
+ success: false,
480
+ error: `Failed to install bun (required for ${toolInfo.name}): ${bunResult.error}`
481
+ };
482
+ }
483
+
484
+ this.log('success', 'Bun installed successfully');
485
+ }
486
+ }
487
+
488
+ // Check if install command contains multiple steps (&&)
489
+ if (toolInfo.install.includes('&&')) {
490
+ return await this.executeMultiStepInstallation(toolInfo);
491
+ }
492
+
468
493
  const [command, ...args] = toolInfo.install.split(' ');
469
494
 
470
495
  this.log('debug', `Executing: ${toolInfo.install}`);
@@ -1089,6 +1114,96 @@ class EnhancedCLIInstaller {
1089
1114
  details: installations
1090
1115
  };
1091
1116
  }
1117
+
1118
+ /**
1119
+ * Check if bun runtime is available
1120
+ * @returns {Promise<boolean>} True if bun is available
1121
+ */
1122
+ async checkBunAvailable() {
1123
+ const { spawnSync } = require('child_process');
1124
+
1125
+ try {
1126
+ const result = spawnSync('bun', ['--version'], {
1127
+ stdio: 'pipe',
1128
+ shell: true
1129
+ });
1130
+
1131
+ return result.status === 0 || result.error === undefined;
1132
+ } catch (error) {
1133
+ return false;
1134
+ }
1135
+ }
1136
+
1137
+ /**
1138
+ * Execute multi-step installation (commands with &&)
1139
+ * @param {Object} toolInfo - Tool information
1140
+ * @returns {Promise<Object>} Installation result
1141
+ */
1142
+ async executeMultiStepInstallation(toolInfo) {
1143
+ const steps = toolInfo.install.split('&&').map(s => s.trim());
1144
+
1145
+ this.log('info', `Executing multi-step installation (${steps.length} steps) for ${toolInfo.name}...`);
1146
+
1147
+ for (let i = 0; i < steps.length; i++) {
1148
+ const step = steps[i];
1149
+ const stepNumber = i + 1;
1150
+
1151
+ this.log('info', `Step ${stepNumber}/${steps.length}: ${step}`);
1152
+
1153
+ const result = await this.executeInstallationCommand(step);
1154
+
1155
+ if (!result.success) {
1156
+ this.log('error', `Step ${stepNumber} failed: ${result.error}`);
1157
+ return {
1158
+ success: false,
1159
+ error: `Installation failed at step ${stepNumber}: ${result.error}`,
1160
+ failedAtStep: stepNumber,
1161
+ failedCommand: step
1162
+ };
1163
+ }
1164
+
1165
+ this.log('success', `Step ${stepNumber}/${steps.length} completed`);
1166
+ }
1167
+
1168
+ this.log('success', `All ${steps.length} steps completed successfully for ${toolInfo.name}`);
1169
+
1170
+ return {
1171
+ success: true,
1172
+ stepsCompleted: steps.length
1173
+ };
1174
+ }
1175
+
1176
+ /**
1177
+ * Execute a single installation command
1178
+ * @param {string} command - Installation command
1179
+ * @returns {Promise<Object>} Execution result
1180
+ */
1181
+ async executeInstallationCommand(command) {
1182
+ const { executeCommand } = require('../utils');
1183
+
1184
+ try {
1185
+ const result = await executeCommand(command, [], {
1186
+ stdio: this.verbose ? 'inherit' : 'pipe',
1187
+ shell: true,
1188
+ timeout: 300000 // 5 minutes
1189
+ });
1190
+
1191
+ if (result.success) {
1192
+ return { success: true };
1193
+ } else {
1194
+ return {
1195
+ success: false,
1196
+ error: result.error || `Command exited with code ${result.code}`,
1197
+ code: result.code
1198
+ };
1199
+ }
1200
+ } catch (error) {
1201
+ return {
1202
+ success: false,
1203
+ error: error.message
1204
+ };
1205
+ }
1206
+ }
1092
1207
  }
1093
1208
 
1094
1209
  module.exports = EnhancedCLIInstaller;
@@ -121,11 +121,11 @@ export class StigmergySkillManager {
121
121
 
122
122
  /**
123
123
  * Sync skills to CLI configuration files
124
- * Also refreshes the LocalSkillScanner cache
124
+ * Also copies skills to CLI-specific directories and refreshes cache
125
125
  * @returns {Promise<void>}
126
126
  */
127
127
  async sync() {
128
- console.log('[INFO] Syncing skills to CLI configuration files...');
128
+ console.log('[INFO] Syncing skills to CLI directories...');
129
129
 
130
130
  try {
131
131
  // Refresh the LocalSkillScanner cache
@@ -152,10 +152,14 @@ export class StigmergySkillManager {
152
152
  return;
153
153
  }
154
154
 
155
- // Generate <available_skills> XML
155
+ // Step 1: Copy skills to CLI-specific directories
156
+ console.log('[INFO] Copying skills to CLI directories...');
157
+ const copyResults = await this.syncSkillFiles(skills);
158
+
159
+ // Step 2: Generate <available_skills> XML
156
160
  const skillsXml = this.generateSkillsXml(skills);
157
161
 
158
- // All CLI configuration files to update
162
+ // Step 3: All CLI configuration files to update
159
163
  const cliFiles = [
160
164
  'AGENTS.md', // Universal config
161
165
  'claude.md', // Claude CLI
@@ -165,7 +169,9 @@ export class StigmergySkillManager {
165
169
  'qodercli.md', // Qoder CLI
166
170
  'codebuddy.md', // CodeBuddy CLI
167
171
  'copilot.md', // Copilot CLI
168
- 'codex.md' // Codex CLI
172
+ 'codex.md', // Codex CLI
173
+ 'opencode.md', // OpenCode CLI
174
+ 'oh-my-opencode.md' // Oh-My-OpenCode Plugin Manager
169
175
  ];
170
176
 
171
177
  let syncedCount = 0;
@@ -191,19 +197,102 @@ export class StigmergySkillManager {
191
197
 
192
198
  // Output sync result summary
193
199
  console.log(`\n[OK] Sync completed:`);
194
- console.log(` - Updated: ${syncedCount} file(s)`);
200
+ console.log(` - Skills copied: ${copyResults.copied} to ${copyResults.targets} CLI directory(ies)`);
201
+ console.log(` - Config updated: ${syncedCount} file(s)`);
195
202
  if (createdCount > 0) {
196
- console.log(` - Created: ${createdCount} file(s)`);
203
+ console.log(` - Config created: ${createdCount} file(s)`);
197
204
  }
198
205
  if (skippedCount > 0) {
199
- console.log(` - Skipped: ${skippedCount} file(s)`);
206
+ console.log(` - Config skipped: ${skippedCount} file(s)`);
200
207
  }
201
- console.log(` - Skills: ${skills.length}`);
208
+ console.log(` - Total skills: ${skills.length}`);
202
209
  } catch (err) {
203
210
  console.error(`[X] Sync failed: ${err.message}`);
204
211
  throw err;
205
212
  }
206
213
  }
214
+
215
+ /**
216
+ * Copy skill files to CLI-specific directories
217
+ * @private
218
+ * @param {Array} skills - List of skills to copy
219
+ * @returns {Promise<Object>} Copy results
220
+ */
221
+ async syncSkillFiles(skills) {
222
+ const fsPromises = await import('fs/promises');
223
+
224
+ // Define CLI skill directories
225
+ const cliSkillDirs = [
226
+ { name: 'Claude', path: path.join(os.homedir(), '.claude', 'skills') },
227
+ { name: 'Universal', path: path.join(process.cwd(), '.agent', 'skills') },
228
+ { name: 'Universal (global)', path: path.join(os.homedir(), '.agent', 'skills') }
229
+ ];
230
+
231
+ let copiedCount = 0;
232
+ let targets = 0;
233
+
234
+ for (const cliDir of cliSkillDirs) {
235
+ try {
236
+ // Ensure CLI directory exists
237
+ await fsPromises.mkdir(cliDir.path, { recursive: true });
238
+
239
+ // Copy each skill
240
+ for (const skill of skills) {
241
+ const skillName = skill.name;
242
+ const sourceDir = path.join(this.skillsDir, skillName);
243
+ const targetDir = path.join(cliDir.path, skillName);
244
+
245
+ try {
246
+ // Check if source exists
247
+ await fsPromises.access(sourceDir);
248
+
249
+ // Remove existing if present
250
+ await fsPromises.rm(targetDir, { recursive: true, force: true });
251
+
252
+ // Copy directory
253
+ await this.copyDirectoryRecursive(sourceDir, targetDir);
254
+ copiedCount++;
255
+ } catch (err) {
256
+ // Skip if source doesn't exist or copy fails
257
+ if (process.env.DEBUG === 'true') {
258
+ console.log(`[DEBUG] Skipped ${skillName} for ${cliDir.name}: ${err.message}`);
259
+ }
260
+ }
261
+ }
262
+
263
+ targets++;
264
+ console.log(` [OK] Synced to ${cliDir.name}: ${cliDir.path}`);
265
+ } catch (err) {
266
+ console.log(` [INFO] Skipped ${cliDir.name}: ${err.message}`);
267
+ }
268
+ }
269
+
270
+ return { copied: copiedCount, targets: targets };
271
+ }
272
+
273
+ /**
274
+ * Recursively copy a directory
275
+ * @private
276
+ * @param {string} src - Source directory
277
+ * @param {string} dest - Destination directory
278
+ */
279
+ async copyDirectoryRecursive(src, dest) {
280
+ const fsPromises = await import('fs/promises');
281
+
282
+ await fsPromises.mkdir(dest, { recursive: true });
283
+ const entries = await fsPromises.readdir(src, { withFileTypes: true });
284
+
285
+ for (const entry of entries) {
286
+ const srcPath = path.join(src, entry.name);
287
+ const destPath = path.join(dest, entry.name);
288
+
289
+ if (entry.isDirectory()) {
290
+ await this.copyDirectoryRecursive(srcPath, destPath);
291
+ } else {
292
+ await fsPromises.copyFile(srcPath, destPath);
293
+ }
294
+ }
295
+ }
207
296
 
208
297
  /**
209
298
  * Sync skills to a single file
@@ -13,7 +13,9 @@ export class SkillParser {
13
13
  * @returns {Object} Parsed metadata
14
14
  */
15
15
  parseMetadata(content) {
16
- const match = content.match(/^---\n(.*?)\n---/s);
16
+ // Normalize line endings to LF
17
+ const normalizedContent = content.replace(/\r\n/g, '\n');
18
+ const match = normalizedContent.match(/^---\n(.*?)\n---/s);
17
19
  if (!match) {
18
20
  return {};
19
21
  }
@@ -78,8 +80,10 @@ export class SkillParser {
78
80
  * @returns {string} Content without frontmatter
79
81
  */
80
82
  extractContent(content) {
81
- const match = content.match(/^---\n.*?\n---\n(.*)$/s);
82
- return match ? match[1].trim() : content;
83
+ // Normalize line endings to LF
84
+ const normalizedContent = content.replace(/\r\n/g, '\n');
85
+ const match = normalizedContent.match(/^---\n.*?\n---\n(.*)$/s);
86
+ return match ? match[1].trim() : normalizedContent;
83
87
  }
84
88
 
85
89
  /**