ralph-cli-sandboxed 0.4.1 → 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 +9 -9
- package/dist/commands/chat.js +13 -12
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/docker.js +102 -66
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +3 -1
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +17 -9
- package/dist/commands/prd.js +4 -1
- package/dist/commands/run.js +40 -25
- package/dist/commands/slack.js +2 -2
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +1 -1
- package/dist/providers/discord.d.ts +28 -0
- package/dist/providers/discord.js +227 -14
- package/dist/providers/slack.d.ts +41 -1
- package/dist/providers/slack.js +389 -8
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +185 -5
- 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 +19 -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 +54 -9
- 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/utils/chat-client.d.ts +4 -0
- package/dist/utils/chat-client.js +12 -5
- package/dist/utils/config.d.ts +84 -0
- package/dist/utils/config.js +78 -1
- 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 +6 -1
- package/dist/utils/notification.js +103 -2
- 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-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -505,6 +505,36 @@ Features:
|
|
|
505
505
|
|
|
506
506
|
See [docs/DOCKER.md](docs/DOCKER.md) for detailed Docker configuration, customization, and troubleshooting.
|
|
507
507
|
|
|
508
|
+
## Chat Integration
|
|
509
|
+
|
|
510
|
+
Ralph can be controlled via chat platforms (Slack, Telegram, Discord) and includes intelligent chat responders powered by LLMs.
|
|
511
|
+
|
|
512
|
+
### Chat Commands
|
|
513
|
+
|
|
514
|
+
Control Ralph remotely via chat:
|
|
515
|
+
- `/ralph run` - Start ralph automation
|
|
516
|
+
- `/ralph status` - Check PRD status
|
|
517
|
+
- `/ralph stop` - Stop running automation
|
|
518
|
+
|
|
519
|
+
### Chat Responders
|
|
520
|
+
|
|
521
|
+
Responders handle messages and can answer questions about your codebase:
|
|
522
|
+
|
|
523
|
+
| Trigger | Type | Description |
|
|
524
|
+
|---------|------|-------------|
|
|
525
|
+
| `@qa` | LLM | Answer questions about the codebase |
|
|
526
|
+
| `@review` | LLM | Review code changes (supports `@review diff`, `@review last`) |
|
|
527
|
+
| `@code` | Claude Code | Make file modifications |
|
|
528
|
+
| `!lint` | CLI | Run custom commands |
|
|
529
|
+
|
|
530
|
+
**Features:**
|
|
531
|
+
- **Automatic file detection**: Mention file paths (e.g., `src/config.ts:42`) and they're automatically included in context
|
|
532
|
+
- **Git diff keywords**: Use `diff`, `staged`, `last`, `HEAD~N` to include git changes
|
|
533
|
+
- **Multi-turn conversations**: Continue discussions in Slack/Discord threads
|
|
534
|
+
- **Auto-notifications**: Results from `ralph run` are automatically sent to connected chat
|
|
535
|
+
|
|
536
|
+
See [docs/CHAT-CLIENTS.md](docs/CHAT-CLIENTS.md) for chat platform setup and [docs/CHAT-RESPONDERS.md](docs/CHAT-RESPONDERS.md) for responder configuration.
|
|
537
|
+
|
|
508
538
|
## How It Works
|
|
509
539
|
|
|
510
540
|
1. **Read PRD**: Claude reads your requirements from `prd.json`
|
package/dist/commands/action.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { loadConfig, isRunningInContainer } from "../utils/config.js";
|
|
4
|
-
import { getMessagesPath, sendMessage, waitForResponse
|
|
4
|
+
import { getMessagesPath, sendMessage, waitForResponse } from "../utils/message-queue.js";
|
|
5
5
|
import { getDefaultActions, getBuiltInActionNames } from "../utils/daemon-actions.js";
|
|
6
6
|
/**
|
|
7
7
|
* Execute an action from config.json - works both inside and outside containers.
|
|
@@ -60,23 +60,23 @@ export async function action(args) {
|
|
|
60
60
|
console.log("No actions available.");
|
|
61
61
|
console.log("");
|
|
62
62
|
console.log("Configure actions in .ralph/config.json:");
|
|
63
|
-
console.log(
|
|
63
|
+
console.log(" {");
|
|
64
64
|
console.log(' "daemon": {');
|
|
65
65
|
console.log(' "actions": {');
|
|
66
66
|
console.log(' "build": {');
|
|
67
67
|
console.log(' "command": "./scripts/build.sh",');
|
|
68
68
|
console.log(' "description": "Build the project"');
|
|
69
|
-
console.log(
|
|
70
|
-
console.log(
|
|
71
|
-
console.log(
|
|
72
|
-
console.log(
|
|
69
|
+
console.log(" }");
|
|
70
|
+
console.log(" }");
|
|
71
|
+
console.log(" }");
|
|
72
|
+
console.log(" }");
|
|
73
73
|
}
|
|
74
74
|
else {
|
|
75
75
|
console.log("Available actions:");
|
|
76
76
|
console.log("");
|
|
77
77
|
// Show built-in actions first
|
|
78
|
-
const builtInList = actionNames.filter(name => builtInNames.has(name) && !configuredActions[name]);
|
|
79
|
-
const customList = actionNames.filter(name => !builtInNames.has(name) || configuredActions[name]);
|
|
78
|
+
const builtInList = actionNames.filter((name) => builtInNames.has(name) && !configuredActions[name]);
|
|
79
|
+
const customList = actionNames.filter((name) => !builtInNames.has(name) || configuredActions[name]);
|
|
80
80
|
for (const name of builtInList) {
|
|
81
81
|
const action = allActions[name];
|
|
82
82
|
const desc = action.description || action.command;
|
|
@@ -187,7 +187,7 @@ async function executeDirectly(command, args, debug) {
|
|
|
187
187
|
// Build full command with arguments
|
|
188
188
|
let fullCommand = command;
|
|
189
189
|
if (args.length > 0) {
|
|
190
|
-
fullCommand = `${command} ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(" ")}`;
|
|
190
|
+
fullCommand = `${command} ${args.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(" ")}`;
|
|
191
191
|
}
|
|
192
192
|
if (debug) {
|
|
193
193
|
console.log(`[action] Executing: ${fullCommand}`);
|
package/dist/commands/chat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createTelegramClient } from "../providers/telegram.js";
|
|
|
10
10
|
import { createSlackClient } from "../providers/slack.js";
|
|
11
11
|
import { createDiscordClient } from "../providers/discord.js";
|
|
12
12
|
import { generateProjectId, formatStatusMessage, formatStatusForChat, } from "../utils/chat-client.js";
|
|
13
|
-
import { getMessagesPath, sendMessage, waitForResponse
|
|
13
|
+
import { getMessagesPath, sendMessage, waitForResponse } from "../utils/message-queue.js";
|
|
14
14
|
const CHAT_STATE_FILE = "chat-state.json";
|
|
15
15
|
/**
|
|
16
16
|
* Load chat state from .ralph/chat-state.json
|
|
@@ -307,15 +307,11 @@ async function handleCommand(command, client, config, state, debug) {
|
|
|
307
307
|
}
|
|
308
308
|
break;
|
|
309
309
|
}
|
|
310
|
-
case "stop": {
|
|
311
|
-
await client.sendMessage(chatId, `${state.projectName}: Stop command received (not implemented yet)`);
|
|
312
|
-
break;
|
|
313
|
-
}
|
|
314
310
|
case "action": {
|
|
315
311
|
// Reload config to pick up new actions
|
|
316
312
|
const freshConfig = loadConfig();
|
|
317
313
|
const actions = freshConfig.daemon?.actions || {};
|
|
318
|
-
const actionNames = Object.keys(actions).filter(name => name !== "notify" && name !== "telegram_notify");
|
|
314
|
+
const actionNames = Object.keys(actions).filter((name) => name !== "notify" && name !== "telegram_notify");
|
|
319
315
|
if (args.length === 0) {
|
|
320
316
|
// List available actions
|
|
321
317
|
const usage = client.provider === "slack" ? "/ralph action <name>" : "/action <name>";
|
|
@@ -631,7 +627,9 @@ function showStatus(config) {
|
|
|
631
627
|
}
|
|
632
628
|
console.log("");
|
|
633
629
|
if (config.chat.provider === "slack") {
|
|
634
|
-
if (config.chat.slack?.botToken &&
|
|
630
|
+
if (config.chat.slack?.botToken &&
|
|
631
|
+
config.chat.slack?.appToken &&
|
|
632
|
+
config.chat.slack?.signingSecret) {
|
|
635
633
|
console.log("Slack: configured");
|
|
636
634
|
if (config.chat.slack.allowedChannelIds && config.chat.slack.allowedChannelIds.length > 0) {
|
|
637
635
|
console.log(`Allowed channels: ${config.chat.slack.allowedChannelIds.join(", ")}`);
|
|
@@ -660,7 +658,8 @@ function showStatus(config) {
|
|
|
660
658
|
else {
|
|
661
659
|
console.log("Allowed guilds: all (no restrictions)");
|
|
662
660
|
}
|
|
663
|
-
if (config.chat.discord.allowedChannelIds &&
|
|
661
|
+
if (config.chat.discord.allowedChannelIds &&
|
|
662
|
+
config.chat.discord.allowedChannelIds.length > 0) {
|
|
664
663
|
console.log(`Allowed channels: ${config.chat.discord.allowedChannelIds.join(", ")}`);
|
|
665
664
|
}
|
|
666
665
|
else {
|
|
@@ -698,12 +697,14 @@ async function testChat(config, chatId) {
|
|
|
698
697
|
let client;
|
|
699
698
|
let targetChatId;
|
|
700
699
|
if (provider === "slack") {
|
|
701
|
-
if (!config.chat.slack?.botToken ||
|
|
700
|
+
if (!config.chat.slack?.botToken ||
|
|
701
|
+
!config.chat.slack?.appToken ||
|
|
702
|
+
!config.chat.slack?.signingSecret) {
|
|
702
703
|
console.error("Error: Slack configuration incomplete");
|
|
703
704
|
console.error("Required: botToken, appToken, signingSecret");
|
|
704
705
|
process.exit(1);
|
|
705
706
|
}
|
|
706
|
-
targetChatId = chatId ||
|
|
707
|
+
targetChatId = chatId || config.chat.slack.allowedChannelIds?.[0];
|
|
707
708
|
if (!targetChatId) {
|
|
708
709
|
console.error("Error: No channel ID specified and no allowed channel IDs configured");
|
|
709
710
|
console.error("Usage: ralph chat test <channel_id>");
|
|
@@ -722,7 +723,7 @@ async function testChat(config, chatId) {
|
|
|
722
723
|
console.error("Error: Discord bot token not configured");
|
|
723
724
|
process.exit(1);
|
|
724
725
|
}
|
|
725
|
-
targetChatId = chatId ||
|
|
726
|
+
targetChatId = chatId || config.chat.discord.allowedChannelIds?.[0];
|
|
726
727
|
if (!targetChatId) {
|
|
727
728
|
console.error("Error: No channel ID specified and no allowed channel IDs configured");
|
|
728
729
|
console.error("Usage: ralph chat test <channel_id>");
|
|
@@ -741,7 +742,7 @@ async function testChat(config, chatId) {
|
|
|
741
742
|
console.error("Error: Telegram bot token not configured");
|
|
742
743
|
process.exit(1);
|
|
743
744
|
}
|
|
744
|
-
targetChatId = chatId ||
|
|
745
|
+
targetChatId = chatId || config.chat.telegram.allowedChatIds?.[0];
|
|
745
746
|
if (!targetChatId) {
|
|
746
747
|
console.error("Error: No chat ID specified and no allowed chat IDs configured");
|
|
747
748
|
console.error("Usage: ralph chat test <chat_id>");
|
package/dist/commands/config.js
CHANGED
|
@@ -44,8 +44,9 @@ SECTIONS:
|
|
|
44
44
|
Docker Ports, volumes, environment, packages
|
|
45
45
|
Daemon Actions, socket path
|
|
46
46
|
Claude MCP servers, skills
|
|
47
|
-
Chat Telegram integration
|
|
47
|
+
Chat Telegram, Slack, Discord integration
|
|
48
48
|
Notify Notification settings
|
|
49
|
+
LLM LLM provider configuration (Anthropic, OpenAI, Ollama)
|
|
49
50
|
`;
|
|
50
51
|
console.log(helpText.trim());
|
|
51
52
|
}
|
package/dist/commands/daemon.js
CHANGED
|
@@ -198,9 +198,10 @@ async function executeAction(action, args = []) {
|
|
|
198
198
|
else {
|
|
199
199
|
// Command can use $RALPH_MESSAGE env var
|
|
200
200
|
// Also pass args for backwards compatibility
|
|
201
|
-
fullCommand =
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
fullCommand =
|
|
202
|
+
args.length > 0
|
|
203
|
+
? `${action.command} ${args.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(" ")}`
|
|
204
|
+
: action.command;
|
|
204
205
|
}
|
|
205
206
|
const proc = spawn(fullCommand, [], {
|
|
206
207
|
stdio: ["ignore", "pipe", "pipe"],
|
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/*
|
|
@@ -242,9 +242,9 @@ CMD ["zsh"]
|
|
|
242
242
|
}
|
|
243
243
|
function generateFirewallScript(customDomains = []) {
|
|
244
244
|
// Generate custom domains section if any are configured
|
|
245
|
-
let customDomainsSection =
|
|
245
|
+
let customDomainsSection = "";
|
|
246
246
|
if (customDomains.length > 0) {
|
|
247
|
-
const domainList = customDomains.join(
|
|
247
|
+
const domainList = customDomains.join(" ");
|
|
248
248
|
customDomainsSection = `
|
|
249
249
|
# Custom allowed domains (from config)
|
|
250
250
|
for ip in $(dig +short ${domainList}); do
|
|
@@ -254,8 +254,8 @@ done
|
|
|
254
254
|
}
|
|
255
255
|
// Generate echo line with custom domains if configured
|
|
256
256
|
const allowedList = customDomains.length > 0
|
|
257
|
-
? `GitHub, npm, Anthropic API, local network, ${customDomains.join(
|
|
258
|
-
:
|
|
257
|
+
? `GitHub, npm, Anthropic API, local network, ${customDomains.join(", ")}`
|
|
258
|
+
: "GitHub, npm, Anthropic API, local network";
|
|
259
259
|
return `#!/bin/bash
|
|
260
260
|
# Firewall initialization script for Ralph sandbox
|
|
261
261
|
# Based on Claude Code devcontainer firewall
|
|
@@ -338,26 +338,26 @@ echo "Allowed: ${allowedList}"
|
|
|
338
338
|
}
|
|
339
339
|
function generateDockerCompose(imageName, dockerConfig) {
|
|
340
340
|
// Build ports section if configured
|
|
341
|
-
let portsSection =
|
|
341
|
+
let portsSection = "";
|
|
342
342
|
if (dockerConfig?.ports && dockerConfig.ports.length > 0) {
|
|
343
|
-
const portLines = dockerConfig.ports.map(port => ` - "${port}"`).join(
|
|
343
|
+
const portLines = dockerConfig.ports.map((port) => ` - "${port}"`).join("\n");
|
|
344
344
|
portsSection = ` ports:\n${portLines}\n`;
|
|
345
345
|
}
|
|
346
346
|
// Build volumes array: base volumes + custom volumes
|
|
347
347
|
const baseVolumes = [
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
" # Mount project root (two levels up from .ralph/docker/)",
|
|
349
|
+
" - ../..:/workspace",
|
|
350
350
|
" # Mount host's ~/.claude for Pro/Max OAuth credentials",
|
|
351
|
-
|
|
351
|
+
" - ${HOME}/.claude:/home/node/.claude",
|
|
352
352
|
` - ${imageName}-history:/commandhistory`,
|
|
353
353
|
];
|
|
354
354
|
if (dockerConfig?.volumes && dockerConfig.volumes.length > 0) {
|
|
355
|
-
const customVolumeLines = dockerConfig.volumes.map(vol => ` - ${vol}`);
|
|
355
|
+
const customVolumeLines = dockerConfig.volumes.map((vol) => ` - ${vol}`);
|
|
356
356
|
baseVolumes.push(...customVolumeLines);
|
|
357
357
|
}
|
|
358
|
-
const volumesSection = baseVolumes.join(
|
|
358
|
+
const volumesSection = baseVolumes.join("\n");
|
|
359
359
|
// Build environment section if configured
|
|
360
|
-
let environmentSection =
|
|
360
|
+
let environmentSection = "";
|
|
361
361
|
const envEntries = [];
|
|
362
362
|
// Add user-configured environment variables
|
|
363
363
|
if (dockerConfig?.environment && Object.keys(dockerConfig.environment).length > 0) {
|
|
@@ -366,7 +366,7 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
366
366
|
}
|
|
367
367
|
}
|
|
368
368
|
if (envEntries.length > 0) {
|
|
369
|
-
environmentSection = ` environment:\n${envEntries.join(
|
|
369
|
+
environmentSection = ` environment:\n${envEntries.join("\n")}\n`;
|
|
370
370
|
}
|
|
371
371
|
else {
|
|
372
372
|
// Keep the commented placeholder for users who don't have config
|
|
@@ -375,12 +375,12 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
375
375
|
# - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}\n`;
|
|
376
376
|
}
|
|
377
377
|
// Build command section if configured
|
|
378
|
-
let commandSection =
|
|
379
|
-
let streamJsonNote =
|
|
378
|
+
let commandSection = "";
|
|
379
|
+
let streamJsonNote = "";
|
|
380
380
|
if (dockerConfig?.asciinema?.enabled && dockerConfig?.asciinema?.autoRecord) {
|
|
381
381
|
// Wrap with asciinema recording
|
|
382
|
-
const outputDir = dockerConfig.asciinema.outputDir ||
|
|
383
|
-
const innerCommand = dockerConfig.startCommand ||
|
|
382
|
+
const outputDir = dockerConfig.asciinema.outputDir || ".recordings";
|
|
383
|
+
const innerCommand = dockerConfig.startCommand || "zsh";
|
|
384
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`;
|
|
385
385
|
// Add note about stream-json if enabled
|
|
386
386
|
if (dockerConfig.asciinema.streamJson?.enabled) {
|
|
@@ -402,14 +402,14 @@ function generateDockerCompose(imageName, dockerConfig) {
|
|
|
402
402
|
}
|
|
403
403
|
// Build restart policy section
|
|
404
404
|
// Priority: restartCount (on-failure with max retries) > autoStart (unless-stopped)
|
|
405
|
-
let restartSection =
|
|
405
|
+
let restartSection = "";
|
|
406
406
|
if (dockerConfig?.restartCount !== undefined && dockerConfig.restartCount > 0) {
|
|
407
407
|
// Use on-failure policy with max retry count
|
|
408
408
|
restartSection = ` restart: on-failure:${dockerConfig.restartCount}\n`;
|
|
409
409
|
}
|
|
410
410
|
else if (dockerConfig?.autoStart) {
|
|
411
411
|
// Use unless-stopped for auto-restart on daemon start
|
|
412
|
-
restartSection =
|
|
412
|
+
restartSection = " restart: unless-stopped\n";
|
|
413
413
|
}
|
|
414
414
|
return `# Ralph CLI Docker Compose
|
|
415
415
|
# Generated by ralph-cli
|
|
@@ -440,10 +440,12 @@ dist
|
|
|
440
440
|
`;
|
|
441
441
|
// Generate stream wrapper script for clean asciinema recordings
|
|
442
442
|
function generateStreamScript(outputDir, saveRawJson) {
|
|
443
|
-
const saveJsonSection = saveRawJson
|
|
443
|
+
const saveJsonSection = saveRawJson
|
|
444
|
+
? `
|
|
444
445
|
# Save raw JSON for later analysis
|
|
445
446
|
JSON_LOG="$OUTPUT_DIR/session-$TIMESTAMP.jsonl"
|
|
446
|
-
TEE_CMD="tee \\"$JSON_LOG\\""`
|
|
447
|
+
TEE_CMD="tee \\"$JSON_LOG\\""`
|
|
448
|
+
: `
|
|
447
449
|
TEE_CMD="cat"`;
|
|
448
450
|
return `#!/bin/bash
|
|
449
451
|
# Ralph stream wrapper - formats Claude stream-json output for clean terminal display
|
|
@@ -520,12 +522,12 @@ function generateMcpJson(mcpServers) {
|
|
|
520
522
|
}
|
|
521
523
|
// Generate skill file content with YAML frontmatter
|
|
522
524
|
function generateSkillFile(skill) {
|
|
523
|
-
const lines = [
|
|
525
|
+
const lines = ["---", `description: ${skill.description}`];
|
|
524
526
|
if (skill.userInvocable === false) {
|
|
525
|
-
lines.push(
|
|
527
|
+
lines.push("user-invocable: false");
|
|
526
528
|
}
|
|
527
|
-
lines.push(
|
|
528
|
-
return lines.join(
|
|
529
|
+
lines.push("---", "", skill.instructions, "");
|
|
530
|
+
return lines.join("\n");
|
|
529
531
|
}
|
|
530
532
|
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider, dockerConfig, claudeConfig) {
|
|
531
533
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
@@ -536,14 +538,17 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
536
538
|
}
|
|
537
539
|
const customDomains = dockerConfig?.firewall?.allowedDomains || [];
|
|
538
540
|
const files = [
|
|
539
|
-
{
|
|
541
|
+
{
|
|
542
|
+
name: "Dockerfile",
|
|
543
|
+
content: generateDockerfile(language, javaVersion, cliProvider, dockerConfig),
|
|
544
|
+
},
|
|
540
545
|
{ name: "init-firewall.sh", content: generateFirewallScript(customDomains) },
|
|
541
546
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
|
|
542
547
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
543
548
|
];
|
|
544
549
|
// Add stream script if streamJson is enabled
|
|
545
550
|
if (dockerConfig?.asciinema?.enabled && dockerConfig.asciinema.streamJson?.enabled) {
|
|
546
|
-
const outputDir = dockerConfig.asciinema.outputDir ||
|
|
551
|
+
const outputDir = dockerConfig.asciinema.outputDir || ".recordings";
|
|
547
552
|
const saveRawJson = dockerConfig.asciinema.streamJson.saveRawJson !== false; // default true
|
|
548
553
|
files.push({ name: "ralph-stream.sh", content: generateStreamScript(outputDir, saveRawJson) });
|
|
549
554
|
}
|
|
@@ -566,28 +571,28 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
566
571
|
const projectRoot = process.cwd();
|
|
567
572
|
// Generate .mcp.json if MCP servers are configured
|
|
568
573
|
if (claudeConfig?.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
|
|
569
|
-
const mcpJsonPath = join(projectRoot,
|
|
574
|
+
const mcpJsonPath = join(projectRoot, ".mcp.json");
|
|
570
575
|
if (existsSync(mcpJsonPath) && !force) {
|
|
571
|
-
const overwrite = await promptConfirm(
|
|
576
|
+
const overwrite = await promptConfirm(".mcp.json already exists. Overwrite?");
|
|
572
577
|
if (!overwrite) {
|
|
573
|
-
console.log(
|
|
578
|
+
console.log("Skipped .mcp.json");
|
|
574
579
|
}
|
|
575
580
|
else {
|
|
576
581
|
writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
|
|
577
|
-
console.log(
|
|
582
|
+
console.log("Created .mcp.json");
|
|
578
583
|
}
|
|
579
584
|
}
|
|
580
585
|
else {
|
|
581
586
|
writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
|
|
582
|
-
console.log(
|
|
587
|
+
console.log("Created .mcp.json");
|
|
583
588
|
}
|
|
584
589
|
}
|
|
585
590
|
// Generate skill files if skills are configured
|
|
586
591
|
if (claudeConfig?.skills && claudeConfig.skills.length > 0) {
|
|
587
|
-
const commandsDir = join(projectRoot,
|
|
592
|
+
const commandsDir = join(projectRoot, ".claude", "commands");
|
|
588
593
|
if (!existsSync(commandsDir)) {
|
|
589
594
|
mkdirSync(commandsDir, { recursive: true });
|
|
590
|
-
console.log(
|
|
595
|
+
console.log("Created .claude/commands/");
|
|
591
596
|
}
|
|
592
597
|
for (const skill of claudeConfig.skills) {
|
|
593
598
|
const skillPath = join(commandsDir, `${skill.name}.md`);
|
|
@@ -605,8 +610,8 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
605
610
|
// Save config hash for change detection
|
|
606
611
|
const configForHash = {
|
|
607
612
|
language,
|
|
608
|
-
checkCommand:
|
|
609
|
-
testCommand:
|
|
613
|
+
checkCommand: "",
|
|
614
|
+
testCommand: "",
|
|
610
615
|
javaVersion,
|
|
611
616
|
cliProvider,
|
|
612
617
|
docker: dockerConfig,
|
|
@@ -627,12 +632,18 @@ async function buildImage(ralphDir) {
|
|
|
627
632
|
if (hasConfigChanged(ralphDir, config)) {
|
|
628
633
|
const regenerate = await promptConfirm("Config has changed since last docker init. Regenerate Docker files?");
|
|
629
634
|
if (regenerate) {
|
|
630
|
-
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);
|
|
631
639
|
console.log("");
|
|
632
640
|
}
|
|
633
641
|
}
|
|
634
642
|
console.log("Building Docker image...\n");
|
|
635
|
-
const imageName = config.imageName ||
|
|
643
|
+
const imageName = config.imageName ||
|
|
644
|
+
`ralph-${basename(process.cwd())
|
|
645
|
+
.toLowerCase()
|
|
646
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
636
647
|
return new Promise((resolve, reject) => {
|
|
637
648
|
// Use --no-cache and --pull to ensure we always get the latest CLI versions
|
|
638
649
|
// Use -p to set unique project name per ralph project
|
|
@@ -718,8 +729,7 @@ function startBackgroundServices(config) {
|
|
|
718
729
|
services.push("daemon");
|
|
719
730
|
}
|
|
720
731
|
// Start chat if telegram is configured and not explicitly disabled
|
|
721
|
-
const telegramEnabled = config.chat?.telegram?.botToken &&
|
|
722
|
-
config.chat.telegram.enabled !== false;
|
|
732
|
+
const telegramEnabled = config.chat?.telegram?.botToken && config.chat.telegram.enabled !== false;
|
|
723
733
|
if (telegramEnabled) {
|
|
724
734
|
const logPath = join(ralphDir, "chat.log");
|
|
725
735
|
const logFd = openSync(logPath, "w");
|
|
@@ -760,8 +770,8 @@ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvi
|
|
|
760
770
|
if (dockerfileExists) {
|
|
761
771
|
const configForHash = {
|
|
762
772
|
language,
|
|
763
|
-
checkCommand:
|
|
764
|
-
testCommand:
|
|
773
|
+
checkCommand: "",
|
|
774
|
+
testCommand: "",
|
|
765
775
|
javaVersion,
|
|
766
776
|
cliProvider,
|
|
767
777
|
docker: dockerConfig,
|
|
@@ -877,7 +887,18 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
877
887
|
// Remove containers, volumes, networks, and local images
|
|
878
888
|
// Use -p to target only this project's resources
|
|
879
889
|
await new Promise((resolve) => {
|
|
880
|
-
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
|
+
], {
|
|
881
902
|
cwd: dockerDir,
|
|
882
903
|
stdio: "inherit",
|
|
883
904
|
});
|
|
@@ -904,7 +925,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
904
925
|
output += data.toString();
|
|
905
926
|
});
|
|
906
927
|
proc.on("close", async () => {
|
|
907
|
-
const containerIds = output
|
|
928
|
+
const containerIds = output
|
|
929
|
+
.trim()
|
|
930
|
+
.split("\n")
|
|
931
|
+
.filter((id) => id.length > 0);
|
|
908
932
|
if (containerIds.length > 0) {
|
|
909
933
|
// Force remove these containers
|
|
910
934
|
await new Promise((innerResolve) => {
|
|
@@ -944,7 +968,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
944
968
|
output += data.toString();
|
|
945
969
|
});
|
|
946
970
|
proc.on("close", async () => {
|
|
947
|
-
const volumeNames = output
|
|
971
|
+
const volumeNames = output
|
|
972
|
+
.trim()
|
|
973
|
+
.split("\n")
|
|
974
|
+
.filter((name) => name.length > 0);
|
|
948
975
|
if (volumeNames.length > 0) {
|
|
949
976
|
// Force remove these volumes
|
|
950
977
|
await new Promise((innerResolve) => {
|
|
@@ -985,7 +1012,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
985
1012
|
output += data.toString();
|
|
986
1013
|
});
|
|
987
1014
|
proc.on("close", async () => {
|
|
988
|
-
const podIds = output
|
|
1015
|
+
const podIds = output
|
|
1016
|
+
.trim()
|
|
1017
|
+
.split("\n")
|
|
1018
|
+
.filter((id) => id.length > 0);
|
|
989
1019
|
if (podIds.length > 0) {
|
|
990
1020
|
// Force remove these pods (this also removes their containers)
|
|
991
1021
|
await new Promise((innerResolve) => {
|
|
@@ -1026,7 +1056,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
1026
1056
|
export async function dockerInit(silent = false) {
|
|
1027
1057
|
const config = loadConfig();
|
|
1028
1058
|
const ralphDir = getRalphDir();
|
|
1029
|
-
const imageName = config.imageName ??
|
|
1059
|
+
const imageName = config.imageName ??
|
|
1060
|
+
`ralph-${basename(process.cwd())
|
|
1061
|
+
.toLowerCase()
|
|
1062
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
1030
1063
|
console.log(`\nGenerating Docker files for: ${config.language}`);
|
|
1031
1064
|
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
1032
1065
|
console.log(`Java version: ${config.javaVersion}`);
|
|
@@ -1112,7 +1145,10 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
1112
1145
|
}
|
|
1113
1146
|
const config = loadConfig();
|
|
1114
1147
|
// Get image name from config or generate default
|
|
1115
|
-
const imageName = config.imageName ||
|
|
1148
|
+
const imageName = config.imageName ||
|
|
1149
|
+
`ralph-${basename(process.cwd())
|
|
1150
|
+
.toLowerCase()
|
|
1151
|
+
.replace(/[^a-z0-9-]/g, "-")}`;
|
|
1116
1152
|
const hasFlag = (flag) => subArgs.includes(flag);
|
|
1117
1153
|
switch (subcommand) {
|
|
1118
1154
|
case "build":
|
|
@@ -1134,8 +1170,8 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
1134
1170
|
default: {
|
|
1135
1171
|
// Default to init if no subcommand or unrecognized subcommand
|
|
1136
1172
|
const force = subcommand === "init"
|
|
1137
|
-
?
|
|
1138
|
-
:
|
|
1173
|
+
? subArgs[0] === "-y" || subArgs[0] === "--yes"
|
|
1174
|
+
: subcommand === "-y" || subcommand === "--yes";
|
|
1139
1175
|
console.log(`Generating Docker files for: ${config.language}`);
|
|
1140
1176
|
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
1141
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) {
|