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.
- package/lib/constants.js +1 -1
- package/lib/workstreams.js +17 -7
- package/package.json +3 -1
- package/skills/ralph-loop/SKILL.md +38 -0
- package/ui/dist/assets/{index-CTISNNVQ.js → index-tasycRrW.js} +53 -53
- package/ui/dist/index.html +1 -1
- package/ui/routes/loops.js +21 -259
- package/ui/routes/plugins.js +2 -28
- package/ui/routes/projects.js +37 -67
- package/ui/routes/utils.js +42 -0
- package/ui/server.cjs +1 -1
package/ui/dist/index.html
CHANGED
|
@@ -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-
|
|
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>
|
package/ui/routes/loops.js
CHANGED
|
@@ -5,31 +5,8 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
package/ui/routes/plugins.js
CHANGED
|
@@ -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
|
|
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
|
package/ui/routes/projects.js
CHANGED
|
@@ -5,31 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const {
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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: '
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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':
|