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.
Files changed (80) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +47 -20
  3. package/dist/commands/chat.d.ts +1 -1
  4. package/dist/commands/chat.js +325 -62
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/daemon.d.ts +2 -5
  7. package/dist/commands/daemon.js +118 -49
  8. package/dist/commands/docker.js +110 -73
  9. package/dist/commands/fix-config.js +2 -1
  10. package/dist/commands/fix-prd.js +2 -2
  11. package/dist/commands/help.js +19 -3
  12. package/dist/commands/init.js +78 -17
  13. package/dist/commands/listen.js +116 -5
  14. package/dist/commands/logo.d.ts +5 -0
  15. package/dist/commands/logo.js +41 -0
  16. package/dist/commands/notify.js +1 -1
  17. package/dist/commands/once.js +19 -9
  18. package/dist/commands/prd.js +20 -2
  19. package/dist/commands/run.js +111 -27
  20. package/dist/commands/slack.d.ts +10 -0
  21. package/dist/commands/slack.js +333 -0
  22. package/dist/config/responder-presets.json +69 -0
  23. package/dist/index.js +6 -1
  24. package/dist/providers/discord.d.ts +82 -0
  25. package/dist/providers/discord.js +697 -0
  26. package/dist/providers/slack.d.ts +79 -0
  27. package/dist/providers/slack.js +715 -0
  28. package/dist/providers/telegram.d.ts +30 -0
  29. package/dist/providers/telegram.js +190 -7
  30. package/dist/responders/claude-code-responder.d.ts +48 -0
  31. package/dist/responders/claude-code-responder.js +203 -0
  32. package/dist/responders/cli-responder.d.ts +62 -0
  33. package/dist/responders/cli-responder.js +298 -0
  34. package/dist/responders/llm-responder.d.ts +135 -0
  35. package/dist/responders/llm-responder.js +582 -0
  36. package/dist/templates/macos-scripts.js +2 -4
  37. package/dist/templates/prompts.js +4 -2
  38. package/dist/tui/ConfigEditor.js +42 -5
  39. package/dist/tui/components/ArrayEditor.js +1 -1
  40. package/dist/tui/components/EditorPanel.js +10 -6
  41. package/dist/tui/components/HelpPanel.d.ts +1 -1
  42. package/dist/tui/components/HelpPanel.js +1 -1
  43. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  44. package/dist/tui/components/KeyValueEditor.js +69 -5
  45. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  46. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  47. package/dist/tui/components/ObjectEditor.js +1 -1
  48. package/dist/tui/components/Preview.js +1 -1
  49. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  50. package/dist/tui/components/RespondersEditor.js +437 -0
  51. package/dist/tui/components/SectionNav.js +27 -3
  52. package/dist/tui/utils/presets.js +15 -2
  53. package/dist/utils/chat-client.d.ts +33 -4
  54. package/dist/utils/chat-client.js +20 -1
  55. package/dist/utils/config.d.ts +100 -1
  56. package/dist/utils/config.js +78 -1
  57. package/dist/utils/daemon-actions.d.ts +19 -0
  58. package/dist/utils/daemon-actions.js +111 -0
  59. package/dist/utils/daemon-client.d.ts +21 -0
  60. package/dist/utils/daemon-client.js +28 -1
  61. package/dist/utils/llm-client.d.ts +82 -0
  62. package/dist/utils/llm-client.js +185 -0
  63. package/dist/utils/message-queue.js +6 -6
  64. package/dist/utils/notification.d.ts +10 -2
  65. package/dist/utils/notification.js +111 -4
  66. package/dist/utils/prd-validator.js +60 -19
  67. package/dist/utils/prompt.js +22 -12
  68. package/dist/utils/responder-logger.d.ts +47 -0
  69. package/dist/utils/responder-logger.js +129 -0
  70. package/dist/utils/responder-presets.d.ts +92 -0
  71. package/dist/utils/responder-presets.js +156 -0
  72. package/dist/utils/responder.d.ts +88 -0
  73. package/dist/utils/responder.js +207 -0
  74. package/dist/utils/stream-json.js +6 -6
  75. package/docs/CHAT-CLIENTS.md +520 -0
  76. package/docs/CHAT-RESPONDERS.md +785 -0
  77. package/docs/DEVELOPMENT.md +25 -0
  78. package/docs/USEFUL_ACTIONS.md +815 -0
  79. package/docs/chat-architecture.md +251 -0
  80. package/package.json +14 -1
@@ -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('sha256').update(content).digest('hex').substring(0, 16);
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 + '\n');
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, 'utf-8').trim();
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('\n') + '\n';
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('\n');
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('\n');
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(' \\\n && ')}
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 || '.recordings';
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 " ____ _ _ ____ _ _ "' >> /home/node/.zshrc && \\
195
- echo ' echo "| _ \\\\ / \\\\ | | | _ \\\\| | | |"' >> /home/node/.zshrc && \\
196
- echo ' echo "| |_) | / _ \\\\ | | | |_) | |_| |"' >> /home/node/.zshrc && \\
197
- echo ' echo "| _ < / ___ \\\\| |___| __/| _ |"' >> /home/node/.zshrc && \\
198
- echo ' echo "|_| \\\\_\\\\/_/ \\\\_\\\\_____|_| |_| |_|"' >> /home/node/.zshrc && \\
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 "CLI - Version $RALPH_VERSION"' >> /home/node/.zshrc && \\
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
- : 'GitHub, npm, Anthropic API, local network';
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('\n');
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
- ' # Mount project root (two levels up from .ralph/docker/)',
348
- ' - ../..:/workspace',
348
+ " # Mount project root (two levels up from .ralph/docker/)",
349
+ " - ../..:/workspace",
349
350
  " # Mount host's ~/.claude for Pro/Max OAuth credentials",
350
- ' - ${HOME}/.claude:/home/node/.claude',
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('\n');
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('\n')}\n`;
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 || '.recordings';
382
- const innerCommand = dockerConfig.startCommand || 'zsh';
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 = ' restart: unless-stopped\n';
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 = ['---', `description: ${skill.description}`];
525
+ const lines = ["---", `description: ${skill.description}`];
523
526
  if (skill.userInvocable === false) {
524
- lines.push('user-invocable: false');
527
+ lines.push("user-invocable: false");
525
528
  }
526
- lines.push('---', '', skill.instructions, '');
527
- return lines.join('\n');
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
- { name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider, dockerConfig) },
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 || '.recordings';
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, '.mcp.json');
574
+ const mcpJsonPath = join(projectRoot, ".mcp.json");
569
575
  if (existsSync(mcpJsonPath) && !force) {
570
- const overwrite = await promptConfirm('.mcp.json already exists. Overwrite?');
576
+ const overwrite = await promptConfirm(".mcp.json already exists. Overwrite?");
571
577
  if (!overwrite) {
572
- console.log('Skipped .mcp.json');
578
+ console.log("Skipped .mcp.json");
573
579
  }
574
580
  else {
575
581
  writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
576
- console.log('Created .mcp.json');
582
+ console.log("Created .mcp.json");
577
583
  }
578
584
  }
579
585
  else {
580
586
  writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
581
- console.log('Created .mcp.json');
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, '.claude', 'commands');
592
+ const commandsDir = join(projectRoot, ".claude", "commands");
587
593
  if (!existsSync(commandsDir)) {
588
594
  mkdirSync(commandsDir, { recursive: true });
589
- console.log('Created .claude/commands/');
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 || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`, true, config.javaVersion, config.cliProvider, config.docker, config.claude);
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 || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
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", ["compose", "-p", imageName, "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
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.trim().split("\n").filter((id) => id.length > 0);
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.trim().split("\n").filter((name) => name.length > 0);
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.trim().split("\n").filter((id) => id.length > 0);
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 ?? `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
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 || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
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
- ? (subArgs[0] === "-y" || subArgs[0] === "--yes")
1137
- : (subcommand === "-y" || subcommand === "--yes");
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)) && typeof corruptContent === "string") {
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;
@@ -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) {
@@ -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 Telegram chat integration to control ralph from your phone:
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: abc run, abc status, abc add <task>
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());