ralph-cli-sandboxed 0.4.0 → 0.4.2
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/README.md +30 -0
- package/dist/commands/action.js +47 -20
- package/dist/commands/chat.d.ts +1 -1
- package/dist/commands/chat.js +325 -62
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.d.ts +2 -5
- package/dist/commands/daemon.js +118 -49
- package/dist/commands/docker.js +110 -73
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/help.js +19 -3
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +116 -5
- package/dist/commands/logo.d.ts +5 -0
- package/dist/commands/logo.js +41 -0
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +19 -9
- package/dist/commands/prd.js +20 -2
- package/dist/commands/run.js +111 -27
- package/dist/commands/slack.d.ts +10 -0
- package/dist/commands/slack.js +333 -0
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +6 -1
- package/dist/providers/discord.d.ts +82 -0
- package/dist/providers/discord.js +697 -0
- package/dist/providers/slack.d.ts +79 -0
- package/dist/providers/slack.js +715 -0
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +190 -7
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +42 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +69 -5
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/tui/utils/presets.js +15 -2
- package/dist/utils/chat-client.d.ts +33 -4
- package/dist/utils/chat-client.js +20 -1
- package/dist/utils/config.d.ts +100 -1
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-actions.d.ts +19 -0
- package/dist/utils/daemon-actions.js +111 -0
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +10 -2
- package/dist/utils/notification.js +111 -4
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-CLIENTS.md +520 -0
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/USEFUL_ACTIONS.md +815 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +14 -1
package/dist/commands/docker.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, writeFileSync, readFileSync, mkdirSync, chmodSync, openSync
|
|
|
2
2
|
import { join, basename } from "path";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
|
-
import { loadConfig, getRalphDir } from "../utils/config.js";
|
|
5
|
+
import { loadConfig, getRalphDir, } from "../utils/config.js";
|
|
6
6
|
import { promptConfirm } from "../utils/prompt.js";
|
|
7
7
|
import { getLanguagesJson, getCliProvidersJson } from "../templates/prompts.js";
|
|
8
8
|
// Track background processes for cleanup
|
|
@@ -19,11 +19,11 @@ function computeConfigHash(config) {
|
|
|
19
19
|
claude: config.claude,
|
|
20
20
|
};
|
|
21
21
|
const content = JSON.stringify(relevantConfig, null, 2);
|
|
22
|
-
return createHash(
|
|
22
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 16);
|
|
23
23
|
}
|
|
24
24
|
// Save config hash to docker directory
|
|
25
25
|
function saveConfigHash(dockerDir, hash) {
|
|
26
|
-
writeFileSync(join(dockerDir, CONFIG_HASH_FILE), hash +
|
|
26
|
+
writeFileSync(join(dockerDir, CONFIG_HASH_FILE), hash + "\n");
|
|
27
27
|
}
|
|
28
28
|
// Load saved config hash, returns null if not found
|
|
29
29
|
function loadConfigHash(dockerDir) {
|
|
@@ -31,7 +31,7 @@ function loadConfigHash(dockerDir) {
|
|
|
31
31
|
if (!existsSync(hashPath)) {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
|
-
return readFileSync(hashPath,
|
|
34
|
+
return readFileSync(hashPath, "utf-8").trim();
|
|
35
35
|
}
|
|
36
36
|
// Check if config has changed since last docker init
|
|
37
37
|
function hasConfigChanged(ralphDir, config) {
|
|
@@ -75,30 +75,30 @@ function generateDockerfile(language, javaVersion, cliProvider, dockerConfig) {
|
|
|
75
75
|
const languageSnippet = getLanguageSnippet(language, javaVersion);
|
|
76
76
|
const cliSnippet = getCliProviderSnippet(cliProvider);
|
|
77
77
|
// Build custom packages section
|
|
78
|
-
let customPackages =
|
|
78
|
+
let customPackages = "";
|
|
79
79
|
if (dockerConfig?.packages && dockerConfig.packages.length > 0) {
|
|
80
|
-
customPackages = dockerConfig.packages.map(pkg => ` ${pkg} \\`).join(
|
|
80
|
+
customPackages = dockerConfig.packages.map((pkg) => ` ${pkg} \\`).join("\n") + "\n";
|
|
81
81
|
}
|
|
82
82
|
// Build root build commands section
|
|
83
|
-
let rootBuildCommands =
|
|
83
|
+
let rootBuildCommands = "";
|
|
84
84
|
if (dockerConfig?.buildCommands?.root && dockerConfig.buildCommands.root.length > 0) {
|
|
85
|
-
const commands = dockerConfig.buildCommands.root.map(cmd => `RUN ${cmd}`).join(
|
|
85
|
+
const commands = dockerConfig.buildCommands.root.map((cmd) => `RUN ${cmd}`).join("\n");
|
|
86
86
|
rootBuildCommands = `
|
|
87
87
|
# Custom build commands (root)
|
|
88
88
|
${commands}
|
|
89
89
|
`;
|
|
90
90
|
}
|
|
91
91
|
// Build node build commands section
|
|
92
|
-
let nodeBuildCommands =
|
|
92
|
+
let nodeBuildCommands = "";
|
|
93
93
|
if (dockerConfig?.buildCommands?.node && dockerConfig.buildCommands.node.length > 0) {
|
|
94
|
-
const commands = dockerConfig.buildCommands.node.map(cmd => `RUN ${cmd}`).join(
|
|
94
|
+
const commands = dockerConfig.buildCommands.node.map((cmd) => `RUN ${cmd}`).join("\n");
|
|
95
95
|
nodeBuildCommands = `
|
|
96
96
|
# Custom build commands (node user)
|
|
97
97
|
${commands}
|
|
98
98
|
`;
|
|
99
99
|
}
|
|
100
100
|
// Build git config section if configured
|
|
101
|
-
let gitConfigSection =
|
|
101
|
+
let gitConfigSection = "";
|
|
102
102
|
if (dockerConfig?.git && (dockerConfig.git.name || dockerConfig.git.email)) {
|
|
103
103
|
const gitCommands = [];
|
|
104
104
|
if (dockerConfig.git.name) {
|
|
@@ -109,15 +109,15 @@ ${commands}
|
|
|
109
109
|
}
|
|
110
110
|
gitConfigSection = `
|
|
111
111
|
# Configure git identity
|
|
112
|
-
RUN ${gitCommands.join(
|
|
112
|
+
RUN ${gitCommands.join(" \\\n && ")}
|
|
113
113
|
`;
|
|
114
114
|
}
|
|
115
115
|
// Build asciinema installation section if enabled
|
|
116
|
-
let asciinemaInstall =
|
|
117
|
-
let asciinemaDir =
|
|
118
|
-
let streamScriptCopy =
|
|
116
|
+
let asciinemaInstall = "";
|
|
117
|
+
let asciinemaDir = "";
|
|
118
|
+
let streamScriptCopy = "";
|
|
119
119
|
if (dockerConfig?.asciinema?.enabled) {
|
|
120
|
-
const outputDir = dockerConfig.asciinema.outputDir ||
|
|
120
|
+
const outputDir = dockerConfig.asciinema.outputDir || ".recordings";
|
|
121
121
|
asciinemaInstall = `
|
|
122
122
|
# Install asciinema for terminal recording/streaming
|
|
123
123
|
RUN apt-get update && apt-get install -y asciinema && rm -rf /var/lib/apt/lists/*
|
|
@@ -168,6 +168,7 @@ RUN apt-get update && apt-get install -y \\
|
|
|
168
168
|
ipset \\
|
|
169
169
|
iproute2 \\
|
|
170
170
|
dnsutils \\
|
|
171
|
+
ripgrep \\
|
|
171
172
|
zsh \\
|
|
172
173
|
${customPackages} && rm -rf /var/lib/apt/lists/*
|
|
173
174
|
|
|
@@ -191,14 +192,14 @@ RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/nod
|
|
|
191
192
|
echo 'if [ -z "$RALPH_BANNER_SHOWN" ]; then' >> /home/node/.zshrc && \\
|
|
192
193
|
echo ' export RALPH_BANNER_SHOWN=1' >> /home/node/.zshrc && \\
|
|
193
194
|
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
194
|
-
echo ' echo "
|
|
195
|
-
echo ' echo "
|
|
196
|
-
echo ' echo "
|
|
197
|
-
echo ' echo "
|
|
198
|
-
echo ' echo "
|
|
199
|
-
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
195
|
+
echo ' echo "\\033[38;2;255;245;157m██████╗ █████╗ ██╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗\\033[0m"' >> /home/node/.zshrc && \\
|
|
196
|
+
echo ' echo "\\033[38;2;255;238;88m██╔══██╗██╔══██╗██║ ██╔══██╗██║ ██║ ██╔════╝██║ ██║\\033[0m"' >> /home/node/.zshrc && \\
|
|
197
|
+
echo ' echo "\\033[38;2;255;235;59m██████╔╝███████║██║ ██████╔╝███████║ ██║ ██║ ██║ sandboxed\\033[0m"' >> /home/node/.zshrc && \\
|
|
198
|
+
echo ' echo "\\033[38;2;253;216;53m██╔══██╗██╔══██║██║ ██╔═══╝ ██╔══██║ ██║ ██║ ██║\\033[0m"' >> /home/node/.zshrc && \\
|
|
199
|
+
echo ' echo "\\033[38;2;251;192;45m██║ ██║██║ ██║███████╗██║ ██║ ██║ ╚██████╗███████╗██║\\033[0m"' >> /home/node/.zshrc && \\
|
|
200
|
+
echo ' echo "\\033[38;2;249;168;37m╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝\\033[0m"' >> /home/node/.zshrc && \\
|
|
200
201
|
echo ' RALPH_VERSION=$(ralph --version 2>/dev/null | head -1 || echo "unknown")' >> /home/node/.zshrc && \\
|
|
201
|
-
echo ' echo "
|
|
202
|
+
echo ' echo "\\033[38;5;248mv$RALPH_VERSION\\033[0m"' >> /home/node/.zshrc && \\
|
|
202
203
|
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
203
204
|
echo 'fi' >> /home/node/.zshrc
|
|
204
205
|
|
|
@@ -241,9 +242,9 @@ CMD ["zsh"]
|
|
|
241
242
|
}
|
|
242
243
|
function generateFirewallScript(customDomains = []) {
|
|
243
244
|
// Generate custom domains section if any are configured
|
|
244
|
-
let customDomainsSection =
|
|
245
|
+
let customDomainsSection = "";
|
|
245
246
|
if (customDomains.length > 0) {
|
|
246
|
-
const domainList = customDomains.join(
|
|
247
|
+
const domainList = customDomains.join(" ");
|
|
247
248
|
customDomainsSection = `
|
|
248
249
|
# Custom allowed domains (from config)
|
|
249
250
|
for ip in $(dig +short ${domainList}); do
|
|
@@ -253,8 +254,8 @@ done
|
|
|
253
254
|
}
|
|
254
255
|
// Generate echo line with custom domains if configured
|
|
255
256
|
const allowedList = customDomains.length > 0
|
|
256
|
-
? `GitHub, npm, Anthropic API, local network, ${customDomains.join(
|
|
257
|
-
:
|
|
257
|
+
? `GitHub, npm, Anthropic API, local network, ${customDomains.join(", ")}`
|
|
258
|
+
: "GitHub, npm, Anthropic API, local network";
|
|
258
259
|
return `#!/bin/bash
|
|
259
260
|
# Firewall initialization script for Ralph sandbox
|
|
260
261
|
# Based on Claude Code devcontainer firewall
|
|
@@ -337,26 +338,26 @@ echo "Allowed: ${allowedList}"
|
|
|
337
338
|
}
|
|
338
339
|
function generateDockerCompose(imageName, dockerConfig) {
|
|
339
340
|
// Build ports section if configured
|
|
340
|
-
let portsSection =
|
|
341
|
+
let portsSection = "";
|
|
341
342
|
if (dockerConfig?.ports && dockerConfig.ports.length > 0) {
|
|
342
|
-
const portLines = dockerConfig.ports.map(port => ` - "${port}"`).join(
|
|
343
|
+
const portLines = dockerConfig.ports.map((port) => ` - "${port}"`).join("\n");
|
|
343
344
|
portsSection = ` ports:\n${portLines}\n`;
|
|
344
345
|
}
|
|
345
346
|
// Build volumes array: base volumes + custom volumes
|
|
346
347
|
const baseVolumes = [
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
" # Mount project root (two levels up from .ralph/docker/)",
|
|
349
|
+
" - ../..:/workspace",
|
|
349
350
|
" # Mount host's ~/.claude for Pro/Max OAuth credentials",
|
|
350
|
-
|
|
351
|
+
" - ${HOME}/.claude:/home/node/.claude",
|
|
351
352
|
` - ${imageName}-history:/commandhistory`,
|
|
352
353
|
];
|
|
353
354
|
if (dockerConfig?.volumes && dockerConfig.volumes.length > 0) {
|
|
354
|
-
const customVolumeLines = dockerConfig.volumes.map(vol => ` - ${vol}`);
|
|
355
|
+
const customVolumeLines = dockerConfig.volumes.map((vol) => ` - ${vol}`);
|
|
355
356
|
baseVolumes.push(...customVolumeLines);
|
|
356
357
|
}
|
|
357
|
-
const volumesSection = baseVolumes.join(
|
|
358
|
+
const volumesSection = baseVolumes.join("\n");
|
|
358
359
|
// Build environment section if configured
|
|
359
|
-
let environmentSection =
|
|
360
|
+
let environmentSection = "";
|
|
360
361
|
const envEntries = [];
|
|
361
362
|
// Add user-configured environment variables
|
|
362
363
|
if (dockerConfig?.environment && Object.keys(dockerConfig.environment).length > 0) {
|
|
@@ -365,7 +366,7 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
if (envEntries.length > 0) {
|
|
368
|
-
environmentSection = ` environment:\n${envEntries.join(
|
|
369
|
+
environmentSection = ` environment:\n${envEntries.join("\n")}\n`;
|
|
369
370
|
}
|
|
370
371
|
else {
|
|
371
372
|
// Keep the commented placeholder for users who don't have config
|
|
@@ -374,12 +375,12 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
374
375
|
# - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}\n`;
|
|
375
376
|
}
|
|
376
377
|
// Build command section if configured
|
|
377
|
-
let commandSection =
|
|
378
|
-
let streamJsonNote =
|
|
378
|
+
let commandSection = "";
|
|
379
|
+
let streamJsonNote = "";
|
|
379
380
|
if (dockerConfig?.asciinema?.enabled && dockerConfig?.asciinema?.autoRecord) {
|
|
380
381
|
// Wrap with asciinema recording
|
|
381
|
-
const outputDir = dockerConfig.asciinema.outputDir ||
|
|
382
|
-
const innerCommand = dockerConfig.startCommand ||
|
|
382
|
+
const outputDir = dockerConfig.asciinema.outputDir || ".recordings";
|
|
383
|
+
const innerCommand = dockerConfig.startCommand || "zsh";
|
|
383
384
|
commandSection = ` command: bash -c "mkdir -p /workspace/${outputDir} && asciinema rec -c '${innerCommand}' /workspace/${outputDir}/session-$$(date +%Y%m%d-%H%M%S).cast"\n`;
|
|
384
385
|
// Add note about stream-json if enabled
|
|
385
386
|
if (dockerConfig.asciinema.streamJson?.enabled) {
|
|
@@ -401,14 +402,14 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
401
402
|
}
|
|
402
403
|
// Build restart policy section
|
|
403
404
|
// Priority: restartCount (on-failure with max retries) > autoStart (unless-stopped)
|
|
404
|
-
let restartSection =
|
|
405
|
+
let restartSection = "";
|
|
405
406
|
if (dockerConfig?.restartCount !== undefined && dockerConfig.restartCount > 0) {
|
|
406
407
|
// Use on-failure policy with max retry count
|
|
407
408
|
restartSection = ` restart: on-failure:${dockerConfig.restartCount}\n`;
|
|
408
409
|
}
|
|
409
410
|
else if (dockerConfig?.autoStart) {
|
|
410
411
|
// Use unless-stopped for auto-restart on daemon start
|
|
411
|
-
restartSection =
|
|
412
|
+
restartSection = " restart: unless-stopped\n";
|
|
412
413
|
}
|
|
413
414
|
return `# Ralph CLI Docker Compose
|
|
414
415
|
# Generated by ralph-cli
|
|
@@ -439,10 +440,12 @@ dist
|
|
|
439
440
|
`;
|
|
440
441
|
// Generate stream wrapper script for clean asciinema recordings
|
|
441
442
|
function generateStreamScript(outputDir, saveRawJson) {
|
|
442
|
-
const saveJsonSection = saveRawJson
|
|
443
|
+
const saveJsonSection = saveRawJson
|
|
444
|
+
? `
|
|
443
445
|
# Save raw JSON for later analysis
|
|
444
446
|
JSON_LOG="$OUTPUT_DIR/session-$TIMESTAMP.jsonl"
|
|
445
|
-
TEE_CMD="tee \\"$JSON_LOG\\""`
|
|
447
|
+
TEE_CMD="tee \\"$JSON_LOG\\""`
|
|
448
|
+
: `
|
|
446
449
|
TEE_CMD="cat"`;
|
|
447
450
|
return `#!/bin/bash
|
|
448
451
|
# Ralph stream wrapper - formats Claude stream-json output for clean terminal display
|
|
@@ -519,12 +522,12 @@ function generateMcpJson(mcpServers) {
|
|
|
519
522
|
}
|
|
520
523
|
// Generate skill file content with YAML frontmatter
|
|
521
524
|
function generateSkillFile(skill) {
|
|
522
|
-
const lines = [
|
|
525
|
+
const lines = ["---", `description: ${skill.description}`];
|
|
523
526
|
if (skill.userInvocable === false) {
|
|
524
|
-
lines.push(
|
|
527
|
+
lines.push("user-invocable: false");
|
|
525
528
|
}
|
|
526
|
-
lines.push(
|
|
527
|
-
return lines.join(
|
|
529
|
+
lines.push("---", "", skill.instructions, "");
|
|
530
|
+
return lines.join("\n");
|
|
528
531
|
}
|
|
529
532
|
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider, dockerConfig, claudeConfig) {
|
|
530
533
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
@@ -535,14 +538,17 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
535
538
|
}
|
|
536
539
|
const customDomains = dockerConfig?.firewall?.allowedDomains || [];
|
|
537
540
|
const files = [
|
|
538
|
-
{
|
|
541
|
+
{
|
|
542
|
+
name: "Dockerfile",
|
|
543
|
+
content: generateDockerfile(language, javaVersion, cliProvider, dockerConfig),
|
|
544
|
+
},
|
|
539
545
|
{ name: "init-firewall.sh", content: generateFirewallScript(customDomains) },
|
|
540
546
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
|
|
541
547
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
542
548
|
];
|
|
543
549
|
// Add stream script if streamJson is enabled
|
|
544
550
|
if (dockerConfig?.asciinema?.enabled && dockerConfig.asciinema.streamJson?.enabled) {
|
|
545
|
-
const outputDir = dockerConfig.asciinema.outputDir ||
|
|
551
|
+
const outputDir = dockerConfig.asciinema.outputDir || ".recordings";
|
|
546
552
|
const saveRawJson = dockerConfig.asciinema.streamJson.saveRawJson !== false; // default true
|
|
547
553
|
files.push({ name: "ralph-stream.sh", content: generateStreamScript(outputDir, saveRawJson) });
|
|
548
554
|
}
|
|
@@ -565,28 +571,28 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
565
571
|
const projectRoot = process.cwd();
|
|
566
572
|
// Generate .mcp.json if MCP servers are configured
|
|
567
573
|
if (claudeConfig?.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
|
|
568
|
-
const mcpJsonPath = join(projectRoot,
|
|
574
|
+
const mcpJsonPath = join(projectRoot, ".mcp.json");
|
|
569
575
|
if (existsSync(mcpJsonPath) && !force) {
|
|
570
|
-
const overwrite = await promptConfirm(
|
|
576
|
+
const overwrite = await promptConfirm(".mcp.json already exists. Overwrite?");
|
|
571
577
|
if (!overwrite) {
|
|
572
|
-
console.log(
|
|
578
|
+
console.log("Skipped .mcp.json");
|
|
573
579
|
}
|
|
574
580
|
else {
|
|
575
581
|
writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
|
|
576
|
-
console.log(
|
|
582
|
+
console.log("Created .mcp.json");
|
|
577
583
|
}
|
|
578
584
|
}
|
|
579
585
|
else {
|
|
580
586
|
writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
|
|
581
|
-
console.log(
|
|
587
|
+
console.log("Created .mcp.json");
|
|
582
588
|
}
|
|
583
589
|
}
|
|
584
590
|
// Generate skill files if skills are configured
|
|
585
591
|
if (claudeConfig?.skills && claudeConfig.skills.length > 0) {
|
|
586
|
-
const commandsDir = join(projectRoot,
|
|
592
|
+
const commandsDir = join(projectRoot, ".claude", "commands");
|
|
587
593
|
if (!existsSync(commandsDir)) {
|
|
588
594
|
mkdirSync(commandsDir, { recursive: true });
|
|
589
|
-
console.log(
|
|
595
|
+
console.log("Created .claude/commands/");
|
|
590
596
|
}
|
|
591
597
|
for (const skill of claudeConfig.skills) {
|
|
592
598
|
const skillPath = join(commandsDir, `${skill.name}.md`);
|
|
@@ -604,8 +610,8 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
604
610
|
// Save config hash for change detection
|
|
605
611
|
const configForHash = {
|
|
606
612
|
language,
|
|
607
|
-
checkCommand:
|
|
608
|
-
testCommand:
|
|
613
|
+
checkCommand: "",
|
|
614
|
+
testCommand: "",
|
|
609
615
|
javaVersion,
|
|
610
616
|
cliProvider,
|
|
611
617
|
docker: dockerConfig,
|
|
@@ -626,12 +632,18 @@ async function buildImage(ralphDir) {
|
|
|
626
632
|
if (hasConfigChanged(ralphDir, config)) {
|
|
627
633
|
const regenerate = await promptConfirm("Config has changed since last docker init. Regenerate Docker files?");
|
|
628
634
|
if (regenerate) {
|
|
629
|
-
await generateFiles(ralphDir, config.language, config.imageName ||
|
|
635
|
+
await generateFiles(ralphDir, config.language, config.imageName ||
|
|
636
|
+
`ralph-${basename(process.cwd())
|
|
637
|
+
.toLowerCase()
|
|
638
|
+
.replace(/[^a-z0-9-]/g, "-")}`, true, config.javaVersion, config.cliProvider, config.docker, config.claude);
|
|
630
639
|
console.log("");
|
|
631
640
|
}
|
|
632
641
|
}
|
|
633
642
|
console.log("Building Docker image...\n");
|
|
634
|
-
const imageName = config.imageName ||
|
|
643
|
+
const imageName = config.imageName ||
|
|
644
|
+
`ralph-${basename(process.cwd())
|
|
645
|
+
.toLowerCase()
|
|
646
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
635
647
|
return new Promise((resolve, reject) => {
|
|
636
648
|
// Use --no-cache and --pull to ensure we always get the latest CLI versions
|
|
637
649
|
// Use -p to set unique project name per ralph project
|
|
@@ -717,8 +729,7 @@ function startBackgroundServices(config) {
|
|
|
717
729
|
services.push("daemon");
|
|
718
730
|
}
|
|
719
731
|
// Start chat if telegram is configured and not explicitly disabled
|
|
720
|
-
const telegramEnabled = config.chat?.telegram?.botToken &&
|
|
721
|
-
config.chat.telegram.enabled !== false;
|
|
732
|
+
const telegramEnabled = config.chat?.telegram?.botToken && config.chat.telegram.enabled !== false;
|
|
722
733
|
if (telegramEnabled) {
|
|
723
734
|
const logPath = join(ralphDir, "chat.log");
|
|
724
735
|
const logFd = openSync(logPath, "w");
|
|
@@ -759,8 +770,8 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
|
|
|
759
770
|
if (dockerfileExists) {
|
|
760
771
|
const configForHash = {
|
|
761
772
|
language,
|
|
762
|
-
checkCommand:
|
|
763
|
-
testCommand:
|
|
773
|
+
checkCommand: "",
|
|
774
|
+
testCommand: "",
|
|
764
775
|
javaVersion,
|
|
765
776
|
cliProvider,
|
|
766
777
|
docker: dockerConfig,
|
|
@@ -876,7 +887,18 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
876
887
|
// Remove containers, volumes, networks, and local images
|
|
877
888
|
// Use -p to target only this project's resources
|
|
878
889
|
await new Promise((resolve) => {
|
|
879
|
-
const proc = spawn("docker", [
|
|
890
|
+
const proc = spawn("docker", [
|
|
891
|
+
"compose",
|
|
892
|
+
"-p",
|
|
893
|
+
imageName,
|
|
894
|
+
"down",
|
|
895
|
+
"--rmi",
|
|
896
|
+
"local",
|
|
897
|
+
"-v",
|
|
898
|
+
"--remove-orphans",
|
|
899
|
+
"--timeout",
|
|
900
|
+
"5",
|
|
901
|
+
], {
|
|
880
902
|
cwd: dockerDir,
|
|
881
903
|
stdio: "inherit",
|
|
882
904
|
});
|
|
@@ -903,7 +925,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
903
925
|
output += data.toString();
|
|
904
926
|
});
|
|
905
927
|
proc.on("close", async () => {
|
|
906
|
-
const containerIds = output
|
|
928
|
+
const containerIds = output
|
|
929
|
+
.trim()
|
|
930
|
+
.split("\n")
|
|
931
|
+
.filter((id) => id.length > 0);
|
|
907
932
|
if (containerIds.length > 0) {
|
|
908
933
|
// Force remove these containers
|
|
909
934
|
await new Promise((innerResolve) => {
|
|
@@ -943,7 +968,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
943
968
|
output += data.toString();
|
|
944
969
|
});
|
|
945
970
|
proc.on("close", async () => {
|
|
946
|
-
const volumeNames = output
|
|
971
|
+
const volumeNames = output
|
|
972
|
+
.trim()
|
|
973
|
+
.split("\n")
|
|
974
|
+
.filter((name) => name.length > 0);
|
|
947
975
|
if (volumeNames.length > 0) {
|
|
948
976
|
// Force remove these volumes
|
|
949
977
|
await new Promise((innerResolve) => {
|
|
@@ -984,7 +1012,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
984
1012
|
output += data.toString();
|
|
985
1013
|
});
|
|
986
1014
|
proc.on("close", async () => {
|
|
987
|
-
const podIds = output
|
|
1015
|
+
const podIds = output
|
|
1016
|
+
.trim()
|
|
1017
|
+
.split("\n")
|
|
1018
|
+
.filter((id) => id.length > 0);
|
|
988
1019
|
if (podIds.length > 0) {
|
|
989
1020
|
// Force remove these pods (this also removes their containers)
|
|
990
1021
|
await new Promise((innerResolve) => {
|
|
@@ -1025,7 +1056,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
1025
1056
|
export async function dockerInit(silent = false) {
|
|
1026
1057
|
const config = loadConfig();
|
|
1027
1058
|
const ralphDir = getRalphDir();
|
|
1028
|
-
const imageName = config.imageName ??
|
|
1059
|
+
const imageName = config.imageName ??
|
|
1060
|
+
`ralph-${basename(process.cwd())
|
|
1061
|
+
.toLowerCase()
|
|
1062
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
1029
1063
|
console.log(`\nGenerating Docker files for: ${config.language}`);
|
|
1030
1064
|
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
1031
1065
|
console.log(`Java version: ${config.javaVersion}`);
|
|
@@ -1111,7 +1145,10 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
1111
1145
|
}
|
|
1112
1146
|
const config = loadConfig();
|
|
1113
1147
|
// Get image name from config or generate default
|
|
1114
|
-
const imageName = config.imageName ||
|
|
1148
|
+
const imageName = config.imageName ||
|
|
1149
|
+
`ralph-${basename(process.cwd())
|
|
1150
|
+
.toLowerCase()
|
|
1151
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
1115
1152
|
const hasFlag = (flag) => subArgs.includes(flag);
|
|
1116
1153
|
switch (subcommand) {
|
|
1117
1154
|
case "build":
|
|
@@ -1133,8 +1170,8 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
1133
1170
|
default: {
|
|
1134
1171
|
// Default to init if no subcommand or unrecognized subcommand
|
|
1135
1172
|
const force = subcommand === "init"
|
|
1136
|
-
?
|
|
1137
|
-
:
|
|
1173
|
+
? subArgs[0] === "-y" || subArgs[0] === "--yes"
|
|
1174
|
+
: subcommand === "-y" || subcommand === "--yes";
|
|
1138
1175
|
console.log(`Generating Docker files for: ${config.language}`);
|
|
1139
1176
|
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
1140
1177
|
console.log(`Java version: ${config.javaVersion}`);
|
|
@@ -206,7 +206,8 @@ function recoverSections(corruptContent, parsedPartial) {
|
|
|
206
206
|
source = "parsed";
|
|
207
207
|
}
|
|
208
208
|
// If not found or invalid, try regex extraction for simple string fields
|
|
209
|
-
if ((value === undefined || !validateSection(section, value)) &&
|
|
209
|
+
if ((value === undefined || !validateSection(section, value)) &&
|
|
210
|
+
typeof corruptContent === "string") {
|
|
210
211
|
const extracted = extractSectionFromCorrupt(corruptContent, section);
|
|
211
212
|
if (extracted !== undefined && validateSection(section, extracted)) {
|
|
212
213
|
value = extracted;
|
package/dist/commands/fix-prd.js
CHANGED
|
@@ -31,7 +31,7 @@ function restoreFromBackup(prdPath, backupPath) {
|
|
|
31
31
|
const validation = validatePrd(backupParsed);
|
|
32
32
|
if (!validation.valid) {
|
|
33
33
|
console.error("Error: Backup file contains invalid PRD structure:");
|
|
34
|
-
validation.errors.slice(0, 3).forEach(err => {
|
|
34
|
+
validation.errors.slice(0, 3).forEach((err) => {
|
|
35
35
|
console.error(` - ${err}`);
|
|
36
36
|
});
|
|
37
37
|
return false;
|
|
@@ -138,7 +138,7 @@ export async function fixPrd(args = []) {
|
|
|
138
138
|
}
|
|
139
139
|
// PRD is invalid
|
|
140
140
|
console.log("\x1b[31m✗ PRD structure is invalid:\x1b[0m");
|
|
141
|
-
validation.errors.slice(0, 5).forEach(err => {
|
|
141
|
+
validation.errors.slice(0, 5).forEach((err) => {
|
|
142
142
|
console.log(` - ${err}`);
|
|
143
143
|
});
|
|
144
144
|
if (validation.errors.length > 5) {
|
package/dist/commands/help.js
CHANGED
|
@@ -21,6 +21,7 @@ COMMANDS:
|
|
|
21
21
|
notify [msg] Send notification to host from sandbox
|
|
22
22
|
action [name] Execute host actions from config.json
|
|
23
23
|
chat <sub> Chat client integration (Telegram, etc.)
|
|
24
|
+
slack <sub> Slack app setup and management
|
|
24
25
|
help Show this help message
|
|
25
26
|
|
|
26
27
|
prd <subcommand> (Alias) Manage PRD entries - same as add/list/status/toggle/clean
|
|
@@ -75,6 +76,11 @@ CHAT SUBCOMMANDS:
|
|
|
75
76
|
chat test [id] Test connection by sending a message
|
|
76
77
|
chat help Show chat help message
|
|
77
78
|
|
|
79
|
+
SLACK SUBCOMMANDS:
|
|
80
|
+
slack setup Create a new Slack app for this Ralph instance
|
|
81
|
+
slack status Show current Slack configuration
|
|
82
|
+
slack help Show slack help message
|
|
83
|
+
|
|
78
84
|
NOTIFY OPTIONS:
|
|
79
85
|
[message] Message to send as notification
|
|
80
86
|
--action, -a <name> Execute specific daemon action (default: notify)
|
|
@@ -118,6 +124,7 @@ EXAMPLES:
|
|
|
118
124
|
ralph notify "Task done!" # Send notification from sandbox to host
|
|
119
125
|
ralph chat start # Start Telegram chat daemon
|
|
120
126
|
ralph chat test 123456 # Test chat connection
|
|
127
|
+
ralph slack setup # Create new Slack app for this project
|
|
121
128
|
ralph action --list # List available host actions
|
|
122
129
|
ralph action build # Execute 'build' action on host
|
|
123
130
|
|
|
@@ -173,7 +180,13 @@ DAEMON CONFIGURATION:
|
|
|
173
180
|
3. From sandbox, notify: ralph notify "Task complete!"
|
|
174
181
|
|
|
175
182
|
CHAT CONFIGURATION:
|
|
176
|
-
Enable
|
|
183
|
+
Enable chat integration to control ralph remotely (Telegram, Slack, Discord).
|
|
184
|
+
|
|
185
|
+
For Slack (recommended for teams):
|
|
186
|
+
ralph slack setup # Interactive wizard creates your Slack app
|
|
187
|
+
ralph chat start # Start the chat daemon
|
|
188
|
+
|
|
189
|
+
For Telegram:
|
|
177
190
|
{
|
|
178
191
|
"chat": {
|
|
179
192
|
"enabled": true,
|
|
@@ -185,11 +198,14 @@ CHAT CONFIGURATION:
|
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
Setup:
|
|
201
|
+
Telegram Setup:
|
|
189
202
|
1. Create bot with @BotFather on Telegram
|
|
190
203
|
2. Add bot token to config.json
|
|
191
204
|
3. Start chat daemon: ralph chat start
|
|
192
|
-
4. Send commands to your bot:
|
|
205
|
+
4. Send commands to your bot: /run, /status, /add <task>
|
|
206
|
+
|
|
207
|
+
Important: Each Ralph instance needs its own Slack app.
|
|
208
|
+
Run 'ralph slack setup' in each project directory.
|
|
193
209
|
`;
|
|
194
210
|
export function help(_args) {
|
|
195
211
|
console.log(HELP_TEXT.trim());
|