coder-config 0.44.22 → 0.44.24

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/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.44.22';
5
+ const VERSION = '0.44.24';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.44.22",
3
+ "version": "0.44.24",
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",
@@ -115,6 +115,12 @@ function startLoop(manager, id) {
115
115
  if (!loop) {
116
116
  return { error: 'Loop not found or cannot be started' };
117
117
  }
118
+
119
+ // Setup hooks for this project
120
+ if (loop.projectPath) {
121
+ setupLoopHooks(loop.projectPath);
122
+ }
123
+
118
124
  return { success: true, loop };
119
125
  }
120
126
 
@@ -139,6 +145,12 @@ function resumeLoop(manager, id) {
139
145
  if (!loop) {
140
146
  return { error: 'Loop not found or cannot be resumed' };
141
147
  }
148
+
149
+ // Setup hooks for this project
150
+ if (loop.projectPath) {
151
+ setupLoopHooks(loop.projectPath);
152
+ }
153
+
142
154
  return { success: true, loop };
143
155
  }
144
156
 
@@ -241,56 +253,32 @@ function recordIteration(manager, id, iteration) {
241
253
  }
242
254
 
243
255
  /**
244
- * Check if loop hooks are installed and registered in Claude Code settings
256
+ * Check if loop hooks are available (scripts exist)
257
+ * Hooks are now installed per-project, not globally
245
258
  */
246
259
  function getLoopHookStatus() {
247
- const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
248
260
  const claudeHooksDir = path.join(os.homedir(), '.claude', 'hooks');
249
261
  const stopHookPath = path.join(claudeHooksDir, 'ralph-loop-stop.sh');
250
262
  const prepromptHookPath = path.join(claudeHooksDir, 'ralph-loop-preprompt.sh');
251
263
 
252
- // Check if hook files exist
264
+ // Check if hook script files exist
253
265
  const stopHookFileExists = fs.existsSync(stopHookPath);
254
266
  const prepromptHookFileExists = fs.existsSync(prepromptHookPath);
255
267
 
256
- // Check if hooks are registered in settings.json
257
- let stopHookRegistered = false;
258
- let prepromptHookRegistered = false;
259
-
260
- if (fs.existsSync(claudeSettingsPath)) {
261
- try {
262
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
263
- const hooks = settings.hooks || {};
264
-
265
- // Check Stop hook registration
266
- if (hooks.Stop) {
267
- stopHookRegistered = hooks.Stop.some(entry =>
268
- entry.hooks?.some(h => h.command?.includes('ralph-loop-stop.sh'))
269
- );
270
- }
271
-
272
- // Check PreToolUse hook registration
273
- if (hooks.PreToolUse) {
274
- prepromptHookRegistered = hooks.PreToolUse.some(entry =>
275
- entry.hooks?.some(h => h.command?.includes('ralph-loop-preprompt.sh'))
276
- );
277
- }
278
- } catch (e) {
279
- // Ignore parse errors
280
- }
281
- }
282
-
268
+ // Hooks are installed per-project now, so "registered" means scripts are available
283
269
  const status = {
284
270
  stopHook: {
285
271
  path: stopHookPath,
286
272
  exists: stopHookFileExists,
287
- registered: stopHookRegistered
273
+ registered: stopHookFileExists // Scripts are ready to be used
288
274
  },
289
275
  prepromptHook: {
290
276
  path: prepromptHookPath,
291
277
  exists: prepromptHookFileExists,
292
- registered: prepromptHookRegistered
293
- }
278
+ registered: prepromptHookFileExists
279
+ },
280
+ mode: 'per-project',
281
+ note: 'Hooks are installed to project .claude/settings.local.json when loops start'
294
282
  };
295
283
 
296
284
  return status;
@@ -437,11 +425,11 @@ function fixRalphLoopPluginStructure() {
437
425
  }
438
426
 
439
427
  /**
440
- * Install loop hooks into Claude Code's settings.json
441
- * Registers Stop hook that increments iteration count after each response
428
+ * Install loop hooks for a specific project directory
429
+ * Creates .claude/settings.local.json with hooks (not committed to git)
430
+ * This avoids the global trust prompt issue
442
431
  */
443
- function installLoopHooks(manager) {
444
- const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
432
+ function installLoopHooks(manager, projectPath = null) {
445
433
  const claudeHooksDir = path.join(os.homedir(), '.claude', 'hooks');
446
434
 
447
435
  // Ensure hooks directory exists
@@ -458,7 +446,7 @@ function installLoopHooks(manager) {
458
446
  const prepromptHookDest = path.join(claudeHooksDir, 'ralph-loop-preprompt.sh');
459
447
 
460
448
  try {
461
- // Copy hook scripts
449
+ // Copy hook scripts to ~/.claude/hooks/
462
450
  if (fs.existsSync(stopHookSource)) {
463
451
  fs.copyFileSync(stopHookSource, stopHookDest);
464
452
  fs.chmodSync(stopHookDest, '755');
@@ -468,76 +456,153 @@ function installLoopHooks(manager) {
468
456
  fs.chmodSync(prepromptHookDest, '755');
469
457
  }
470
458
 
471
- // Load existing settings
472
- let settings = {};
473
- if (fs.existsSync(claudeSettingsPath)) {
474
- settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
475
- }
459
+ // Remove hooks from global settings.json if they exist
460
+ removeGlobalHooks();
476
461
 
477
- // Initialize hooks if needed
478
- if (!settings.hooks) {
479
- settings.hooks = {};
480
- }
462
+ // If project path provided, install hooks to project's .claude/settings.local.json
463
+ if (projectPath) {
464
+ const projectClaudeDir = path.join(projectPath, '.claude');
465
+ const projectSettingsLocalPath = path.join(projectClaudeDir, 'settings.local.json');
481
466
 
482
- // Add Stop hook for iteration tracking
483
- const stopHookEntry = {
484
- hooks: [{
485
- type: 'command',
486
- command: stopHookDest
487
- }]
488
- };
467
+ // Ensure project .claude directory exists
468
+ if (!fs.existsSync(projectClaudeDir)) {
469
+ fs.mkdirSync(projectClaudeDir, { recursive: true });
470
+ }
489
471
 
490
- // Check if Stop hook already exists
491
- if (!settings.hooks.Stop) {
492
- settings.hooks.Stop = [stopHookEntry];
493
- } else {
494
- // Check if our hook is already registered
495
- const hookExists = settings.hooks.Stop.some(entry =>
496
- entry.hooks?.some(h => h.command?.includes('ralph-loop-stop.sh'))
497
- );
498
- if (!hookExists) {
499
- settings.hooks.Stop.push(stopHookEntry);
472
+ // Load or create project-local settings
473
+ let projectSettings = {};
474
+ if (fs.existsSync(projectSettingsLocalPath)) {
475
+ try {
476
+ projectSettings = JSON.parse(fs.readFileSync(projectSettingsLocalPath, 'utf8'));
477
+ } catch (e) {
478
+ projectSettings = {};
479
+ }
500
480
  }
501
- }
502
481
 
503
- // Add PreToolUse hook for context injection (optional)
504
- const prepromptHookEntry = {
505
- hooks: [{
506
- type: 'command',
507
- command: prepromptHookDest
508
- }]
509
- };
482
+ // Initialize hooks if needed
483
+ if (!projectSettings.hooks) {
484
+ projectSettings.hooks = {};
485
+ }
510
486
 
511
- if (!settings.hooks.PreToolUse) {
512
- settings.hooks.PreToolUse = [prepromptHookEntry];
513
- } else {
514
- const hookExists = settings.hooks.PreToolUse.some(entry =>
515
- entry.hooks?.some(h => h.command?.includes('ralph-loop-preprompt.sh'))
516
- );
517
- if (!hookExists) {
518
- settings.hooks.PreToolUse.push(prepromptHookEntry);
487
+ // Add Stop hook
488
+ const stopHookEntry = {
489
+ hooks: [{
490
+ type: 'command',
491
+ command: stopHookDest
492
+ }]
493
+ };
494
+
495
+ if (!projectSettings.hooks.Stop) {
496
+ projectSettings.hooks.Stop = [stopHookEntry];
497
+ } else {
498
+ const hookExists = projectSettings.hooks.Stop.some(entry =>
499
+ entry.hooks?.some(h => h.command?.includes('ralph-loop-stop.sh'))
500
+ );
501
+ if (!hookExists) {
502
+ projectSettings.hooks.Stop.push(stopHookEntry);
503
+ }
519
504
  }
520
- }
521
505
 
522
- // Save updated settings
523
- fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
506
+ // Save project-local settings
507
+ fs.writeFileSync(projectSettingsLocalPath, JSON.stringify(projectSettings, null, 2) + '\n');
508
+
509
+ return {
510
+ success: true,
511
+ message: 'Loop hooks installed to project',
512
+ projectPath,
513
+ settingsFile: projectSettingsLocalPath,
514
+ stopHook: stopHookDest,
515
+ note: 'Hooks installed to .claude/settings.local.json (not committed to git)'
516
+ };
517
+ }
524
518
 
525
519
  return {
526
520
  success: true,
527
- message: 'Loop hooks installed successfully',
521
+ message: 'Hook scripts copied to ~/.claude/hooks/',
528
522
  stopHook: stopHookDest,
529
523
  prepromptHook: prepromptHookDest,
530
- note: 'Stop hook will increment iteration count after each Claude response when CODER_LOOP_ID is set'
524
+ note: 'Hooks will be installed to project directories when loops are started'
531
525
  };
532
526
  } catch (error) {
533
527
  return {
534
528
  success: false,
535
- error: error.message,
536
- suggestion: 'Try manually copying hooks to ~/.claude/hooks/ and adding to ~/.claude/settings.json'
529
+ error: error.message
537
530
  };
538
531
  }
539
532
  }
540
533
 
534
+ /**
535
+ * Remove ralph-loop hooks from global ~/.claude/settings.json
536
+ */
537
+ function removeGlobalHooks() {
538
+ const claudeSettingsPath = path.join(os.homedir(), '.claude', 'settings.json');
539
+
540
+ if (!fs.existsSync(claudeSettingsPath)) {
541
+ return;
542
+ }
543
+
544
+ try {
545
+ const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
546
+
547
+ if (!settings.hooks) {
548
+ return;
549
+ }
550
+
551
+ let modified = false;
552
+
553
+ // Remove Stop hooks that reference ralph-loop-stop.sh
554
+ if (settings.hooks.Stop) {
555
+ const original = settings.hooks.Stop.length;
556
+ settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
557
+ !entry.hooks?.some(h => h.command?.includes('ralph-loop-stop.sh'))
558
+ );
559
+ if (settings.hooks.Stop.length === 0) {
560
+ delete settings.hooks.Stop;
561
+ }
562
+ if (settings.hooks.Stop?.length !== original) {
563
+ modified = true;
564
+ }
565
+ }
566
+
567
+ // Remove PreToolUse hooks that reference ralph-loop-preprompt.sh
568
+ if (settings.hooks.PreToolUse) {
569
+ const original = settings.hooks.PreToolUse.length;
570
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(entry =>
571
+ !entry.hooks?.some(h => h.command?.includes('ralph-loop-preprompt.sh'))
572
+ );
573
+ if (settings.hooks.PreToolUse.length === 0) {
574
+ delete settings.hooks.PreToolUse;
575
+ }
576
+ if (settings.hooks.PreToolUse?.length !== original) {
577
+ modified = true;
578
+ }
579
+ }
580
+
581
+ // Clean up empty hooks object
582
+ if (Object.keys(settings.hooks).length === 0) {
583
+ delete settings.hooks;
584
+ }
585
+
586
+ if (modified) {
587
+ fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + '\n');
588
+ }
589
+ } catch (e) {
590
+ // Ignore errors
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Setup hooks for a specific loop run
596
+ * Called when starting/resuming a loop
597
+ */
598
+ function setupLoopHooks(projectPath) {
599
+ if (!projectPath) {
600
+ return { success: false, error: 'No project path provided' };
601
+ }
602
+
603
+ return installLoopHooks(null, projectPath);
604
+ }
605
+
541
606
  module.exports = {
542
607
  getLoops,
543
608
  getActiveLoop,
@@ -559,6 +624,8 @@ module.exports = {
559
624
  recordIteration,
560
625
  getLoopHookStatus,
561
626
  installLoopHooks,
627
+ setupLoopHooks,
628
+ removeGlobalHooks,
562
629
  getRalphLoopPluginStatus,
563
630
  installRalphLoopPlugin,
564
631
  fixRalphLoopPluginStructure,
package/ui/server.cjs CHANGED
@@ -252,6 +252,9 @@ class ConfigUIServer {
252
252
  // ==================== HTTP Server ====================
253
253
 
254
254
  start() {
255
+ // Run migrations
256
+ this.runMigrations();
257
+
255
258
  const server = http.createServer((req, res) => this.handleRequest(req, res));
256
259
  this.terminalServer.attach(server);
257
260
 
@@ -262,6 +265,19 @@ class ConfigUIServer {
262
265
  });
263
266
  }
264
267
 
268
+ /**
269
+ * Run migrations on server start
270
+ */
271
+ runMigrations() {
272
+ try {
273
+ // Migration: Remove global ralph-loop hooks (v0.44.23+)
274
+ // Hooks are now installed per-project instead of globally
275
+ routes.loops.removeGlobalHooks();
276
+ } catch (e) {
277
+ // Ignore migration errors
278
+ }
279
+ }
280
+
265
281
  async handleRequest(req, res) {
266
282
  const parsedUrl = url.parse(req.url, true);
267
283
  const pathname = parsedUrl.pathname;