coder-config 0.45.12 → 0.45.14

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/config-loader.js CHANGED
@@ -28,7 +28,7 @@ const { init, show } = require('./lib/init');
28
28
  const { memoryList, memoryInit, memoryAdd, memorySearch } = require('./lib/memory');
29
29
  const { envList, envSet, envUnset } = require('./lib/env');
30
30
  const { getProjectsRegistryPath, loadProjectsRegistry, saveProjectsRegistry, projectList, projectAdd, projectRemove } = require('./lib/projects');
31
- const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet, getActiveWorkstream, countWorkstreamsForProject, workstreamInstallHook, workstreamInstallHookGemini, workstreamInstallHookCodex, workstreamDeactivate, workstreamCheckPath, getSettingsPath, loadSettings, saveSettings, workstreamAddTrigger, workstreamRemoveTrigger, workstreamSetAutoActivate, setGlobalAutoActivate, shouldAutoActivate, workstreamCheckFolder, workstreamInstallCdHook, workstreamUninstallCdHook, workstreamCdHookStatus, generateRulesFromRepos, generateRulesWithClaude } = require('./lib/workstreams');
31
+ const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet, getActiveWorkstream, countWorkstreamsForProject, workstreamInstallHook, workstreamInstallHookGemini, workstreamInstallHookCodex, workstreamDeactivate, workstreamCheckPath, getSettingsPath, loadSettings, saveSettings, workstreamAddTrigger, workstreamRemoveTrigger, workstreamSetAutoActivate, setGlobalAutoActivate, shouldAutoActivate, workstreamCheckFolder, workstreamInstallCdHook, workstreamUninstallCdHook, workstreamCdHookStatus, discoverSubProjects, generateRulesFromRepos, generateRulesWithClaude, generateRulesWithAI, getAvailableAITools, findAIBinary, AI_TOOLS } = require('./lib/workstreams');
32
32
  const { getActivityPath, getDefaultActivity, loadActivity, saveActivity, detectProjectRoot, activityLog, activitySummary, generateWorkstreamName, activitySuggestWorkstreams, activityClear } = require('./lib/activity');
33
33
  const { getLoopsPath, loadLoops, saveLoops, loadLoopState, saveLoopState, loadHistory, saveHistory, loopList, loopCreate, loopGet, loopUpdate, loopDelete, loopStart, loopPause, loopResume, loopCancel, loopApprove, loopComplete, loopFail, loopStatus, loopHistory, loopConfig, getActiveLoop, recordIteration, saveClarifications, savePlan, loadClarifications, loadPlan, loopInject, archiveLoop } = require('./lib/loops');
34
34
  const { getSessionStatus, showSessionStatus, flushContext, clearContext, installHooks: sessionInstallHooks, getFlushedContext, installFlushCommand, installAll: sessionInstallAll, SESSION_DIR, FLUSHED_CONTEXT_FILE } = require('./lib/sessions');
@@ -208,6 +208,11 @@ class ClaudeConfigManager {
208
208
  workstreamCdHookStatus() { return workstreamCdHookStatus(); }
209
209
  generateRulesFromRepos(projects) { return generateRulesFromRepos(projects); }
210
210
  generateRulesWithClaude(projects) { return generateRulesWithClaude(projects); }
211
+ generateRulesWithAI(projects, toolId, options) { return generateRulesWithAI(projects, toolId, options); }
212
+ getAvailableAITools() { return getAvailableAITools(); }
213
+ findAIBinary(toolId) { return findAIBinary(toolId); }
214
+ get AI_TOOLS() { return AI_TOOLS; }
215
+ discoverSubProjects(rootPath, maxDepth) { return discoverSubProjects(rootPath, maxDepth); }
211
216
 
212
217
  // Loops (Ralph Loop)
213
218
  getLoopsPath() { return getLoopsPath(this.installDir); }
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.45.12';
5
+ const VERSION = '0.45.14';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
@@ -688,40 +688,153 @@ function workstreamCheckPath(installDir, targetPath, silent = false) {
688
688
  }
689
689
 
690
690
  /**
691
- * Generate rules/context from project repositories using Claude Code
692
- * Runs `claude -p` to analyze repos and generate smart context
691
+ * Supported AI tools for context generation
693
692
  */
694
- async function generateRulesWithClaude(projects) {
693
+ const AI_TOOLS = {
694
+ claude: {
695
+ name: 'Claude',
696
+ binary: 'claude',
697
+ buildArgs: (prompt) => ['-p', prompt],
698
+ candidates: (os) => [
699
+ path.join(os.homedir(), '.local', 'bin', 'claude'),
700
+ '/usr/local/bin/claude',
701
+ '/opt/homebrew/bin/claude',
702
+ path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
703
+ ],
704
+ },
705
+ gemini: {
706
+ name: 'Gemini',
707
+ binary: 'gemini',
708
+ buildArgs: (prompt) => ['-p', prompt],
709
+ candidates: (os) => [
710
+ path.join(os.homedir(), '.local', 'bin', 'gemini'),
711
+ '/usr/local/bin/gemini',
712
+ '/opt/homebrew/bin/gemini',
713
+ path.join(os.homedir(), '.npm-global', 'bin', 'gemini'),
714
+ ],
715
+ },
716
+ codex: {
717
+ name: 'Codex',
718
+ binary: 'codex',
719
+ buildArgs: (prompt) => ['exec', prompt],
720
+ candidates: (os) => [
721
+ path.join(os.homedir(), '.local', 'bin', 'codex'),
722
+ '/usr/local/bin/codex',
723
+ '/opt/homebrew/bin/codex',
724
+ path.join(os.homedir(), '.npm-global', 'bin', 'codex'),
725
+ ],
726
+ },
727
+ ollama: {
728
+ name: 'Ollama',
729
+ binary: 'ollama',
730
+ // Model must be specified in options
731
+ buildArgs: (prompt, options) => ['run', options.model || 'llama3.2', prompt],
732
+ candidates: (os) => [
733
+ path.join(os.homedir(), '.local', 'bin', 'ollama'),
734
+ '/usr/local/bin/ollama',
735
+ '/opt/homebrew/bin/ollama',
736
+ ],
737
+ },
738
+ aider: {
739
+ name: 'Aider',
740
+ binary: 'aider',
741
+ buildArgs: (prompt) => ['--message', prompt, '--yes', '--no-git'],
742
+ candidates: (os) => [
743
+ path.join(os.homedir(), '.local', 'bin', 'aider'),
744
+ '/usr/local/bin/aider',
745
+ '/opt/homebrew/bin/aider',
746
+ path.join(os.homedir(), '.local', 'pipx', 'venvs', 'aider-chat', 'bin', 'aider'),
747
+ ],
748
+ },
749
+ };
750
+
751
+ /**
752
+ * Find the binary path for an AI tool
753
+ */
754
+ function findAIBinary(toolId) {
755
+ const { execFileSync } = require('child_process');
756
+ const os = require('os');
757
+
758
+ const tool = AI_TOOLS[toolId];
759
+ if (!tool) {
760
+ throw new Error(`Unknown AI tool: ${toolId}`);
761
+ }
762
+
763
+ // Check candidate paths
764
+ for (const p of tool.candidates(os)) {
765
+ if (fs.existsSync(p)) return p;
766
+ }
767
+
768
+ // Try which command
769
+ try {
770
+ const resolved = execFileSync('which', [tool.binary], { encoding: 'utf8' }).trim();
771
+ if (resolved && fs.existsSync(resolved)) return resolved;
772
+ } catch (e) {}
773
+
774
+ // Fall back to bare binary name (let shell resolve it)
775
+ return tool.binary;
776
+ }
777
+
778
+ /**
779
+ * Get list of available AI tools (ones that are installed)
780
+ */
781
+ function getAvailableAITools() {
782
+ const available = [];
783
+ for (const [id, tool] of Object.entries(AI_TOOLS)) {
784
+ try {
785
+ const binaryPath = findAIBinary(id);
786
+ if (fs.existsSync(binaryPath)) {
787
+ available.push({ id, name: tool.name, path: binaryPath });
788
+ }
789
+ } catch (e) {
790
+ // Tool not available
791
+ }
792
+ }
793
+ return available;
794
+ }
795
+
796
+ /**
797
+ * Generate rules/context from project repositories using an AI tool
798
+ * Supports: claude, gemini, codex, ollama, aider
799
+ * @param {string[]} projects - Array of project paths
800
+ * @param {string} toolId - AI tool to use (default: 'claude')
801
+ * @param {object} options - Tool-specific options (e.g., { model: 'llama3.2' } for ollama)
802
+ */
803
+ async function generateRulesWithAI(projects, toolId = 'claude', options = {}) {
695
804
  if (!projects || projects.length === 0) {
696
805
  return '';
697
806
  }
698
807
 
699
808
  const { execFileSync } = require('child_process');
700
- const os = require('os');
701
809
 
702
- // Find claude binary (daemon processes may not have full PATH)
703
- const getClaudePath = () => {
704
- const candidates = [
705
- path.join(os.homedir(), '.local', 'bin', 'claude'),
706
- '/usr/local/bin/claude',
707
- '/opt/homebrew/bin/claude',
708
- path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
709
- ];
710
- for (const p of candidates) {
711
- if (fs.existsSync(p)) return p;
810
+ const tool = AI_TOOLS[toolId];
811
+ if (!tool) {
812
+ console.error(`Unknown AI tool: ${toolId}. Available: ${Object.keys(AI_TOOLS).join(', ')}`);
813
+ return generateRulesFromRepos(projects);
814
+ }
815
+
816
+ // Expand projects to include discovered sub-projects
817
+ const allProjects = [];
818
+ const seen = new Set();
819
+
820
+ for (const projectPath of projects) {
821
+ const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
822
+ if (!seen.has(absPath)) {
823
+ seen.add(absPath);
824
+ allProjects.push(absPath);
712
825
  }
713
- try {
714
- const resolved = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
715
- if (resolved && fs.existsSync(resolved)) return resolved;
716
- } catch (e) {}
717
- return 'claude';
718
- };
719
826
 
720
- const projectPaths = projects.map(p =>
721
- path.resolve(p.replace(/^~/, process.env.HOME || ''))
722
- );
827
+ // Discover sub-projects
828
+ const subProjects = discoverSubProjects(absPath);
829
+ for (const subPath of subProjects) {
830
+ if (!seen.has(subPath)) {
831
+ seen.add(subPath);
832
+ allProjects.push(subPath);
833
+ }
834
+ }
835
+ }
723
836
 
724
- const projectList = projectPaths.map(p => `- ${p}`).join('\n');
837
+ const projectList = allProjects.map(p => `- ${p}`).join('\n');
725
838
 
726
839
  const prompt = `Analyze these project repositories and generate concise workstream context rules for an AI coding assistant. Focus on:
727
840
  1. What each project does (brief description)
@@ -735,40 +848,124 @@ ${projectList}
735
848
  Output markdown suitable for injecting into an AI assistant's context. Keep it concise (under 500 words). Do not include code blocks or examples - just descriptions and guidelines.`;
736
849
 
737
850
  try {
738
- // Run claude -p with the prompt using execFileSync (safer than exec)
739
- const claudePath = getClaudePath();
740
- const result = execFileSync(claudePath, ['-p', prompt], {
741
- cwd: projectPaths[0],
851
+ const binaryPath = findAIBinary(toolId);
852
+ const args = tool.buildArgs(prompt, options);
853
+
854
+ console.log(`Generating context with ${tool.name}...`);
855
+
856
+ const result = execFileSync(binaryPath, args, {
857
+ cwd: allProjects[0],
742
858
  encoding: 'utf8',
743
- timeout: 60000, // 60 second timeout
859
+ timeout: 120000, // 2 minute timeout (some models are slower)
744
860
  maxBuffer: 1024 * 1024, // 1MB buffer
745
861
  });
746
862
 
747
863
  return result.trim();
748
864
  } catch (error) {
749
- console.error('Claude generation failed:', error.message);
865
+ console.error(`${tool.name} generation failed:`, error.message);
750
866
  // Fall back to simple generation
751
867
  return generateRulesFromRepos(projects);
752
868
  }
753
869
  }
754
870
 
871
+ /**
872
+ * Generate rules/context from project repositories using Claude Code
873
+ * @deprecated Use generateRulesWithAI(projects, 'claude') instead
874
+ */
875
+ async function generateRulesWithClaude(projects) {
876
+ return generateRulesWithAI(projects, 'claude');
877
+ }
878
+
879
+ /**
880
+ * Discover sub-projects within a directory
881
+ * Looks for directories containing project markers (package.json, pyproject.toml, etc.)
882
+ * Returns array of absolute paths to discovered sub-projects
883
+ */
884
+ function discoverSubProjects(rootPath, maxDepth = 2) {
885
+ const subProjects = [];
886
+ const skipDirs = new Set([
887
+ 'node_modules', '.git', '__pycache__', '.venv', 'venv', 'env',
888
+ 'dist', 'build', '.next', '.nuxt', 'target', 'vendor', '.tox',
889
+ 'coverage', '.pytest_cache', '.mypy_cache', '.ruff_cache'
890
+ ]);
891
+ const projectMarkers = [
892
+ 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod',
893
+ 'CLAUDE.md', 'setup.py', 'pom.xml', 'build.gradle'
894
+ ];
895
+
896
+ function scan(dir, depth) {
897
+ if (depth > maxDepth) return;
898
+
899
+ let entries;
900
+ try {
901
+ entries = fs.readdirSync(dir, { withFileTypes: true });
902
+ } catch (e) {
903
+ return; // Can't read directory
904
+ }
905
+
906
+ for (const entry of entries) {
907
+ if (!entry.isDirectory()) continue;
908
+ if (skipDirs.has(entry.name)) continue;
909
+ if (entry.name.startsWith('.')) continue;
910
+
911
+ const subPath = path.join(dir, entry.name);
912
+
913
+ // Check if this subdirectory is a project
914
+ const hasMarker = projectMarkers.some(marker =>
915
+ fs.existsSync(path.join(subPath, marker))
916
+ );
917
+
918
+ if (hasMarker) {
919
+ subProjects.push(subPath);
920
+ }
921
+
922
+ // Continue scanning deeper
923
+ scan(subPath, depth + 1);
924
+ }
925
+ }
926
+
927
+ scan(rootPath, 0);
928
+ return subProjects;
929
+ }
930
+
755
931
  /**
756
932
  * Generate rules/context from project repositories
757
933
  * Reads README.md, package.json, CLAUDE.md, etc. to create a summary
934
+ * Automatically discovers sub-projects within each project directory
758
935
  */
759
936
  function generateRulesFromRepos(projects) {
760
937
  if (!projects || projects.length === 0) {
761
938
  return '';
762
939
  }
763
940
 
941
+ // Expand projects to include discovered sub-projects
942
+ const allProjects = [];
943
+ const seen = new Set();
944
+
945
+ for (const projectPath of projects) {
946
+ const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
947
+ if (!seen.has(absPath)) {
948
+ seen.add(absPath);
949
+ allProjects.push(absPath);
950
+ }
951
+
952
+ // Discover sub-projects
953
+ const subProjects = discoverSubProjects(absPath);
954
+ for (const subPath of subProjects) {
955
+ if (!seen.has(subPath)) {
956
+ seen.add(subPath);
957
+ allProjects.push(subPath);
958
+ }
959
+ }
960
+ }
961
+
764
962
  const lines = [];
765
963
  lines.push('# Workstream Context');
766
964
  lines.push('');
767
965
  lines.push('## Repositories');
768
966
  lines.push('');
769
967
 
770
- for (const projectPath of projects) {
771
- const absPath = path.resolve(projectPath.replace(/^~/, process.env.HOME || ''));
968
+ for (const absPath of allProjects) {
772
969
  const name = path.basename(absPath);
773
970
 
774
971
  lines.push(`### ${name}`);
@@ -1367,8 +1564,13 @@ module.exports = {
1367
1564
  workstreamInstallHookCodex,
1368
1565
  workstreamDeactivate,
1369
1566
  workstreamCheckPath,
1567
+ discoverSubProjects,
1370
1568
  generateRulesFromRepos,
1371
1569
  generateRulesWithClaude,
1570
+ generateRulesWithAI,
1571
+ getAvailableAITools,
1572
+ findAIBinary,
1573
+ AI_TOOLS,
1372
1574
  // New folder auto-activation functions
1373
1575
  getSettingsPath,
1374
1576
  loadSettings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.45.12",
3
+ "version": "0.45.14",
4
4
  "description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",