coder-config 0.48.1 → 0.48.2-beta

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.
@@ -19,7 +19,7 @@
19
19
 
20
20
  <!-- PWA Manifest -->
21
21
  <link rel="manifest" href="/manifest.json">
22
- <script type="module" crossorigin src="/assets/index-CTISNNVQ.js"></script>
22
+ <script type="module" crossorigin src="/assets/index-tasycRrW.js"></script>
23
23
  <link rel="stylesheet" crossorigin href="/assets/index-DBzIPggj.css">
24
24
  </head>
25
25
  <body>
@@ -5,31 +5,8 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
- const { execFileSync } = require('child_process');
9
-
10
- /**
11
- * Get the full path to the claude binary
12
- * Needed because daemon processes may not have full PATH
13
- */
14
- function getClaudePath() {
15
- const candidates = [
16
- path.join(os.homedir(), '.local', 'bin', 'claude'),
17
- '/usr/local/bin/claude',
18
- '/opt/homebrew/bin/claude',
19
- path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
20
- ];
21
-
22
- for (const p of candidates) {
23
- if (fs.existsSync(p)) return p;
24
- }
25
-
26
- try {
27
- const resolved = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
28
- if (resolved && fs.existsSync(resolved)) return resolved;
29
- } catch (e) {}
30
-
31
- return 'claude';
32
- }
8
+ const { query } = require('@anthropic-ai/claude-agent-sdk');
9
+ const { RALPH_LOOP_SKILL_DIR } = require('./utils');
33
10
 
34
11
  /**
35
12
  * Get all loops
@@ -310,77 +287,14 @@ function getLoopHookStatus() {
310
287
  }
311
288
 
312
289
  /**
313
- * Check if ralph-loop plugin is installed at user scope
314
- * Returns { installed: boolean, scope: string|null }
290
+ * ralph-loop skill is bundled with coder-config — no external plugin install needed.
315
291
  */
316
292
  function getRalphLoopPluginStatus() {
317
- const installedPluginsPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
318
-
319
- if (!fs.existsSync(installedPluginsPath)) {
320
- return { installed: false, scope: null, needsInstall: true };
321
- }
322
-
323
- try {
324
- const data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf8'));
325
- const plugins = data.plugins || {};
326
- const ralphLoop = plugins['ralph-loop@claude-plugins-official'];
327
-
328
- if (!ralphLoop || ralphLoop.length === 0) {
329
- return { installed: false, scope: null, needsInstall: true };
330
- }
331
-
332
- // Check if any installation is at user scope
333
- const userScopeInstall = ralphLoop.find(p => p.scope === 'user');
334
- if (userScopeInstall) {
335
- // Fix plugin structure in case it's using old commands/ format
336
- fixRalphLoopPluginStructure();
337
- return { installed: true, scope: 'user', needsInstall: false };
338
- }
339
-
340
- // Plugin is installed but only at project scope
341
- return {
342
- installed: true,
343
- scope: 'project',
344
- projectPath: ralphLoop[0].projectPath,
345
- needsInstall: true,
346
- message: 'Plugin is installed for a specific project only. Install at user scope for global access.'
347
- };
348
- } catch (e) {
349
- return { installed: false, scope: null, needsInstall: true, error: e.message };
350
- }
293
+ return { installed: true, scope: 'bundled', needsInstall: false };
351
294
  }
352
295
 
353
- /**
354
- * Install ralph-loop plugin at user scope via CLI
355
- * Uses execFileSync with fixed args (no shell injection risk)
356
- */
357
296
  async function installRalphLoopPlugin() {
358
- const claudePath = getClaudePath();
359
-
360
- try {
361
- // Run claude plugin install command with execFileSync (safer than execSync)
362
- // All arguments are fixed strings - no user input
363
- execFileSync(claudePath, ['plugin', 'install', 'ralph-loop@claude-plugins-official', '--scope', 'user'], {
364
- encoding: 'utf8',
365
- timeout: 30000, // 30 second timeout
366
- stdio: ['pipe', 'pipe', 'pipe']
367
- });
368
-
369
- // Fix the plugin structure - create skills symlink if needed
370
- // The official plugin uses commands/ but Claude Code expects skills/
371
- fixRalphLoopPluginStructure();
372
-
373
- return {
374
- success: true,
375
- message: 'ralph-loop plugin installed successfully at user scope'
376
- };
377
- } catch (e) {
378
- return {
379
- success: false,
380
- error: e.message,
381
- suggestion: 'Try running manually: claude plugin install ralph-loop@claude-plugins-official --scope user'
382
- };
383
- }
297
+ return { success: true, message: 'ralph-loop skill is bundled with coder-config' };
384
298
  }
385
299
 
386
300
  /**
@@ -482,180 +396,29 @@ ${task}
482
396
 
483
397
  ## Rewritten Task:`;
484
398
 
485
- return new Promise((resolve) => {
486
- const { spawn } = require('child_process');
487
- const claudePath = getClaudePath();
488
- const args = ['-p', metaPrompt];
489
-
490
- // Run from project directory if provided
491
- const options = {
492
- encoding: 'utf8',
493
- cwd: projectPath || process.cwd()
494
- };
495
-
496
- let output = '';
497
- let errorOutput = '';
498
- let resolved = false;
499
- let timeoutId = null;
500
-
501
- const proc = spawn(claudePath, args, options);
502
-
503
- // Close stdin immediately - claude -p reads from args, not stdin
504
- proc.stdin.end();
505
-
506
- const safeResolve = (result) => {
507
- if (resolved) return;
508
- resolved = true;
509
- if (timeoutId) clearTimeout(timeoutId);
510
- resolve(result);
511
- };
512
-
513
- proc.stdout.on('data', (data) => {
514
- output += data.toString();
515
- });
516
-
517
- proc.stderr.on('data', (data) => {
518
- errorOutput += data.toString();
519
- });
520
-
521
- proc.on('close', (code) => {
522
- if (code === 0 && output.trim()) {
523
- safeResolve({
524
- success: true,
525
- tunedPrompt: output.trim()
526
- });
527
- } else {
528
- safeResolve({
529
- success: false,
530
- error: errorOutput || 'Failed to tune prompt',
531
- originalPrompt: task
532
- });
399
+ try {
400
+ let tunedPrompt = '';
401
+ for await (const msg of query({
402
+ prompt: metaPrompt,
403
+ options: {
404
+ cwd: projectPath || process.cwd(),
405
+ maxTurns: 1,
533
406
  }
534
- });
535
-
536
- proc.on('error', (err) => {
537
- safeResolve({
538
- success: false,
539
- error: err.message,
540
- originalPrompt: task
541
- });
542
- });
543
-
544
- // Handle timeout manually since spawn doesn't have timeout
545
- timeoutId = setTimeout(() => {
546
- if (!resolved) {
547
- proc.kill();
548
- safeResolve({
549
- success: false,
550
- error: 'Prompt tuning timed out',
551
- originalPrompt: task
552
- });
407
+ })) {
408
+ if (msg.type === 'result' && msg.subtype === 'success') {
409
+ tunedPrompt = msg.result;
553
410
  }
554
- }, 60000);
555
- });
556
- }
557
-
558
- /**
559
- * Fix the ralph-loop plugin structure by converting commands to skills format
560
- * Claude Code expects skills/<name>/SKILL.md, but the plugin has commands/<name>.md
561
- * Also fixes frontmatter issues (hide-from-slash-command-tool -> name)
562
- * Also fixes hooks.json to use absolute paths instead of ${CLAUDE_PLUGIN_ROOT}
563
- */
564
- function fixRalphLoopPluginStructure() {
565
- // Fix both cache and marketplace directories
566
- // Claude Code reads hooks from marketplace source, not cache
567
- const pluginLocations = [
568
- path.join(os.homedir(), '.claude', 'plugins', 'cache', 'claude-plugins-official', 'ralph-loop'),
569
- path.join(os.homedir(), '.claude', 'plugins', 'marketplaces', 'claude-plugins-official', 'plugins', 'ralph-loop')
570
- ];
571
-
572
- for (const pluginDir of pluginLocations) {
573
- if (!fs.existsSync(pluginDir)) {
574
- continue;
575
411
  }
576
-
577
- // Check if this is a versioned cache dir or direct marketplace dir
578
- const hasVersionDirs = fs.readdirSync(pluginDir).some(f => {
579
- const fullPath = path.join(pluginDir, f);
580
- return fs.statSync(fullPath).isDirectory() && !['commands', 'skills', 'hooks', 'scripts', '.claude-plugin'].includes(f);
581
- });
582
-
583
- const dirsToFix = hasVersionDirs
584
- ? fs.readdirSync(pluginDir).filter(f => {
585
- const fullPath = path.join(pluginDir, f);
586
- return fs.statSync(fullPath).isDirectory();
587
- }).map(v => path.join(pluginDir, v))
588
- : [pluginDir];
589
-
590
- for (const versionDir of dirsToFix) {
591
- const commandsDir = path.join(versionDir, 'commands');
592
- const skillsDir = path.join(versionDir, 'skills');
593
- const hooksDir = path.join(versionDir, 'hooks');
594
-
595
- // Disable plugin's hooks.json - we use our own env-var-based hooks
596
- // The plugin's hooks use project-local state files which affect ALL terminals
597
- // in the same project, causing input freezing in other Claude sessions
598
- const hooksJsonPath = path.join(hooksDir, 'hooks.json');
599
- if (fs.existsSync(hooksJsonPath)) {
600
- try {
601
- const hooksContent = fs.readFileSync(hooksJsonPath, 'utf8');
602
- const hooks = JSON.parse(hooksContent);
603
- // Only disable if it has hooks defined (not already disabled)
604
- if (hooks.hooks && Object.keys(hooks.hooks).length > 0) {
605
- hooks._disabled_hooks = hooks.hooks; // Keep for reference
606
- hooks.hooks = {}; // Disable all hooks
607
- hooks._disabled_reason = 'Disabled by coder-config - using env-var-based hooks instead';
608
- fs.writeFileSync(hooksJsonPath, JSON.stringify(hooks, null, 2), 'utf8');
609
- }
610
- } catch (e) {
611
- // Ignore errors fixing hooks
612
- }
613
- }
614
-
615
- if (!fs.existsSync(commandsDir)) {
616
- continue;
617
- }
618
-
619
- // Remove old symlink if it exists
620
- if (fs.existsSync(skillsDir) && fs.lstatSync(skillsDir).isSymbolicLink()) {
621
- fs.unlinkSync(skillsDir);
622
- }
623
-
624
- // Create skills directory if it doesn't exist
625
- if (!fs.existsSync(skillsDir)) {
626
- fs.mkdirSync(skillsDir, { recursive: true });
627
- }
628
-
629
- // Convert each command to skill format
630
- // commands/ralph-loop.md -> skills/ralph-loop/SKILL.md
631
- const commands = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
632
- for (const cmdFile of commands) {
633
- const skillName = cmdFile.replace('.md', '');
634
- const skillDir = path.join(skillsDir, skillName);
635
- const skillFile = path.join(skillDir, 'SKILL.md');
636
-
637
- // Create skill directory
638
- if (!fs.existsSync(skillDir)) {
639
- fs.mkdirSync(skillDir, { recursive: true });
640
- }
641
-
642
- // Read command file content
643
- const cmdPath = path.join(commandsDir, cmdFile);
644
- let content = fs.readFileSync(cmdPath, 'utf8');
645
-
646
- // Fix frontmatter: replace hide-from-slash-command-tool with name
647
- content = content.replace(
648
- /hide-from-slash-command-tool:\s*["']true["']/g,
649
- `name: ${skillName}`
650
- );
651
-
652
- // Write skill file (always overwrite to ensure fix is applied)
653
- fs.writeFileSync(skillFile, content, 'utf8');
654
- }
412
+ if (tunedPrompt) {
413
+ return { success: true, tunedPrompt: tunedPrompt.trim() };
655
414
  }
415
+ return { success: false, error: 'No result from Claude', originalPrompt: task };
416
+ } catch (err) {
417
+ return { success: false, error: err.message, originalPrompt: task };
656
418
  }
657
419
  }
658
420
 
421
+
659
422
  /**
660
423
  * Install loop hooks for a specific project directory
661
424
  * Creates .claude/settings.local.json with hooks (not committed to git)
@@ -860,6 +623,5 @@ module.exports = {
860
623
  removeGlobalHooks,
861
624
  getRalphLoopPluginStatus,
862
625
  installRalphLoopPlugin,
863
- fixRalphLoopPluginStructure,
864
626
  tunePrompt,
865
627
  };
@@ -5,34 +5,8 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
- const { spawn, execFileSync } = require('child_process');
9
-
10
- /**
11
- * Get the full path to the claude binary
12
- * Needed because daemon processes may not have full PATH
13
- */
14
- function getClaudePath() {
15
- // Common locations
16
- const candidates = [
17
- path.join(os.homedir(), '.local', 'bin', 'claude'),
18
- '/usr/local/bin/claude',
19
- '/opt/homebrew/bin/claude',
20
- path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
21
- ];
22
-
23
- for (const p of candidates) {
24
- if (fs.existsSync(p)) return p;
25
- }
26
-
27
- // Try to resolve via which command
28
- try {
29
- const resolved = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
30
- if (resolved && fs.existsSync(resolved)) return resolved;
31
- } catch (e) {}
32
-
33
- // Fallback to hoping it's in PATH
34
- return 'claude';
35
- }
8
+ const { spawn } = require('child_process');
9
+ const { getClaudePath } = require('./utils');
36
10
 
37
11
  /**
38
12
  * Default marketplace to auto-install
@@ -5,31 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
- const { execFileSync, spawn } = require('child_process');
9
-
10
- /**
11
- * Get the full path to the claude binary
12
- * Needed because daemon processes may not have full PATH
13
- */
14
- function getClaudePath() {
15
- const candidates = [
16
- path.join(os.homedir(), '.local', 'bin', 'claude'),
17
- '/usr/local/bin/claude',
18
- '/opt/homebrew/bin/claude',
19
- path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
20
- ];
21
-
22
- for (const p of candidates) {
23
- if (fs.existsSync(p)) return p;
24
- }
25
-
26
- try {
27
- const resolved = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
28
- if (resolved && fs.existsSync(resolved)) return resolved;
29
- } catch (e) {}
30
-
31
- return 'claude';
32
- }
8
+ const { query } = require('@anthropic-ai/claude-agent-sdk');
33
9
 
34
10
  /**
35
11
  * Get all registered projects with status info
@@ -85,7 +61,7 @@ function getActiveProject(manager, projectDir, getHierarchy, getSubprojects) {
85
61
  * Add a project to the registry
86
62
  * @param {boolean} runClaudeInit - If true, run `claude /init` to create CLAUDE.md
87
63
  */
88
- function addProject(manager, projectPath, name, setProjectDir, runClaudeInit = false) {
64
+ async function addProject(manager, projectPath, name, setProjectDir, runClaudeInit = false) {
89
65
  if (!manager) return { error: 'Manager not available' };
90
66
 
91
67
  const absPath = path.resolve(projectPath.replace(/^~/, os.homedir()));
@@ -109,14 +85,13 @@ function addProject(manager, projectPath, name, setProjectDir, runClaudeInit = f
109
85
  // Run claude /init if requested and CLAUDE.md doesn't exist
110
86
  if (runClaudeInit && !fs.existsSync(claudeMd)) {
111
87
  try {
112
- execFileSync(getClaudePath(), ['-p', '/init'], {
113
- cwd: absPath,
114
- stdio: 'pipe',
115
- timeout: 30000
116
- });
117
- claudeInitRan = true;
88
+ for await (const msg of query({
89
+ prompt: '/init',
90
+ options: { cwd: absPath, maxTurns: 5, settingSources: ['user', 'project'] }
91
+ })) {
92
+ if (msg.type === 'result') claudeInitRan = !msg.is_error;
93
+ }
118
94
  } catch (err) {
119
- // Claude Code not installed or init failed
120
95
  claudeInitError = err.message;
121
96
  }
122
97
  }
@@ -244,7 +219,7 @@ function setActiveProject(manager, projectId, setProjectDir, getHierarchy, getSu
244
219
  * @param {object} res - HTTP response object for SSE
245
220
  * @param {string} projectPath - Path to project directory
246
221
  */
247
- function streamClaudeInit(res, projectPath) {
222
+ async function streamClaudeInit(res, projectPath) {
248
223
  const absPath = path.resolve(projectPath.replace(/^~/, os.homedir()));
249
224
 
250
225
  if (!fs.existsSync(absPath)) {
@@ -261,42 +236,37 @@ function streamClaudeInit(res, projectPath) {
261
236
  return;
262
237
  }
263
238
 
264
- res.write(`data: ${JSON.stringify({ type: 'status', message: 'Starting claude -p /init...' })}\n\n`);
265
-
266
- const child = spawn(getClaudePath(), ['-p', '/init'], {
267
- cwd: absPath,
268
- env: { ...process.env, TERM: 'dumb' },
269
- stdio: ['ignore', 'pipe', 'pipe']
270
- });
239
+ res.write(`data: ${JSON.stringify({ type: 'status', message: 'Running /init...' })}\n\n`);
271
240
 
272
- child.stdout.on('data', (data) => {
273
- const text = data.toString();
274
- res.write(`data: ${JSON.stringify({ type: 'output', text })}\n\n`);
275
- });
241
+ const abortController = new AbortController();
242
+ res.on('close', () => abortController.abort());
276
243
 
277
- child.stderr.on('data', (data) => {
278
- const text = data.toString();
279
- res.write(`data: ${JSON.stringify({ type: 'output', text })}\n\n`);
280
- });
281
-
282
- child.on('close', (code) => {
283
- if (code === 0) {
284
- res.write(`data: ${JSON.stringify({ type: 'done', success: true })}\n\n`);
285
- } else {
286
- res.write(`data: ${JSON.stringify({ type: 'done', success: false, code })}\n\n`);
244
+ try {
245
+ for await (const msg of query({
246
+ prompt: '/init',
247
+ options: {
248
+ cwd: absPath,
249
+ maxTurns: 5,
250
+ settingSources: ['user', 'project'],
251
+ abortController,
252
+ }
253
+ })) {
254
+ if (msg.type === 'assistant') {
255
+ const text = msg.message.content
256
+ .filter(b => b.type === 'text')
257
+ .map(b => b.text)
258
+ .join('');
259
+ if (text) res.write(`data: ${JSON.stringify({ type: 'output', text })}\n\n`);
260
+ } else if (msg.type === 'result') {
261
+ res.write(`data: ${JSON.stringify({ type: 'done', success: !msg.is_error })}\n\n`);
262
+ }
287
263
  }
288
- res.end();
289
- });
290
-
291
- child.on('error', (err) => {
292
- res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
293
- res.end();
294
- });
295
-
296
- // Handle client disconnect
297
- res.on('close', () => {
298
- child.kill();
299
- });
264
+ } catch (err) {
265
+ if (err.name !== 'AbortError') {
266
+ res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
267
+ }
268
+ }
269
+ res.end();
300
270
  }
301
271
 
302
272
  module.exports = {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Shared route utilities
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { execFileSync } = require('child_process');
9
+
10
+ /**
11
+ * Get the full path to the claude binary.
12
+ * Needed because daemon processes may not have full PATH.
13
+ * Only used for plugin management commands (install, uninstall, marketplace)
14
+ * that the Agent SDK does not expose.
15
+ */
16
+ function getClaudePath() {
17
+ const candidates = [
18
+ path.join(os.homedir(), '.local', 'bin', 'claude'),
19
+ '/usr/local/bin/claude',
20
+ '/opt/homebrew/bin/claude',
21
+ path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
22
+ ];
23
+
24
+ for (const p of candidates) {
25
+ if (fs.existsSync(p)) return p;
26
+ }
27
+
28
+ try {
29
+ const resolved = execFileSync('which', ['claude'], { encoding: 'utf8' }).trim();
30
+ if (resolved && fs.existsSync(resolved)) return resolved;
31
+ } catch (e) {}
32
+
33
+ return 'claude';
34
+ }
35
+
36
+ /**
37
+ * Path to the bundled ralph-loop skill directory.
38
+ * Passed to Agent SDK as plugins: [{ type: 'local', path: RALPH_LOOP_SKILL_DIR }]
39
+ */
40
+ const RALPH_LOOP_SKILL_DIR = path.join(__dirname, '..', '..', 'skills');
41
+
42
+ module.exports = { getClaudePath, RALPH_LOOP_SKILL_DIR };
package/ui/server.cjs CHANGED
@@ -736,7 +736,7 @@ class ConfigUIServer {
736
736
 
737
737
  case '/api/projects':
738
738
  if (req.method === 'GET') return this.json(res, routes.projects.getProjects(this.manager, this.projectDir));
739
- if (req.method === 'POST') return this.json(res, routes.projects.addProject(this.manager, body.path, body.name, (p) => { this.projectDir = p; }, body.runClaudeInit));
739
+ if (req.method === 'POST') return this.json(res, await routes.projects.addProject(this.manager, body.path, body.name, (p) => { this.projectDir = p; }, body.runClaudeInit));
740
740
  break;
741
741
 
742
742
  case '/api/projects/init-stream':