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
@@ -1,10 +1,11 @@
1
1
  import { existsSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from "fs";
2
2
  import { join, basename, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage } from "../templates/prompts.js";
5
- import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection } from "../templates/macos-scripts.js";
6
- import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
4
+ import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage, } from "../templates/prompts.js";
5
+ import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection, } from "../templates/macos-scripts.js";
6
+ import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows, } from "../utils/prompt.js";
7
7
  import { dockerInit } from "./docker.js";
8
+ import { getBundleDisplayOptions, displayOptionToBundleId, bundleToRespondersConfig, getPresetDisplayOptions, displayOptionToPresetId, presetsToRespondersConfig, } from "../utils/responder-presets.js";
8
9
  // Get package root directory (works for both dev and installed package)
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -41,6 +42,7 @@ export async function init(args) {
41
42
  let selectedKey;
42
43
  let selectedTechnologies = [];
43
44
  let selectedSkills = [];
45
+ let selectedResponders = {};
44
46
  let checkCommand;
45
47
  let testCommand;
46
48
  if (useDefaults) {
@@ -62,7 +64,7 @@ export async function init(args) {
62
64
  else {
63
65
  // Step 1: Select CLI provider (first)
64
66
  const providerKeys = Object.keys(CLI_PROVIDERS);
65
- const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
67
+ const providerNames = providerKeys.map((k) => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
66
68
  const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
67
69
  const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
68
70
  selectedCliProviderKey = providerKeys[selectedProviderIndex];
@@ -73,9 +75,13 @@ export async function init(args) {
73
75
  const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
74
76
  const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
75
77
  const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
76
- const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
78
+ const customYoloArgs = customYoloArgsInput.trim()
79
+ ? customYoloArgsInput.trim().split(/\s+/)
80
+ : [];
77
81
  const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
78
- const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
82
+ const customPromptArgs = customPromptArgsInput.trim()
83
+ ? customPromptArgsInput.trim().split(/\s+/)
84
+ : [];
79
85
  cliConfig = {
80
86
  command: customCommand || "claude",
81
87
  args: customArgs,
@@ -94,7 +100,7 @@ export async function init(args) {
94
100
  console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
95
101
  // Step 2: Select language (second)
96
102
  const languageKeys = Object.keys(LANGUAGES);
97
- const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
103
+ const languageNames = languageKeys.map((k) => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
98
104
  const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
99
105
  const selectedIndex = languageNames.indexOf(selectedName);
100
106
  selectedKey = languageKeys[selectedIndex];
@@ -102,11 +108,11 @@ export async function init(args) {
102
108
  console.log(`\nSelected language: ${config.name}`);
103
109
  // Step 3: Select technology stack if available (third)
104
110
  if (config.technologies && config.technologies.length > 0) {
105
- const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
106
- const techNames = config.technologies.map(t => t.name);
111
+ const techOptions = config.technologies.map((t) => `${t.name} - ${t.description}`);
112
+ const techNames = config.technologies.map((t) => t.name);
107
113
  selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
108
114
  // Convert display names back to just technology names for predefined options
109
- selectedTechnologies = selectedTechnologies.map(sel => {
115
+ selectedTechnologies = selectedTechnologies.map((sel) => {
110
116
  const idx = techOptions.indexOf(sel);
111
117
  return idx >= 0 ? techNames[idx] : sel;
112
118
  });
@@ -120,10 +126,10 @@ export async function init(args) {
120
126
  // Step 4: Select skills if available for this language
121
127
  const availableSkills = getSkillsForLanguage(selectedKey);
122
128
  if (availableSkills.length > 0) {
123
- const skillOptions = availableSkills.map(s => `${s.name} - ${s.description}`);
129
+ const skillOptions = availableSkills.map((s) => `${s.name} - ${s.description}`);
124
130
  const selectedSkillNames = await promptMultiSelectWithArrows("Select AI coding rules/skills to enable (optional):", skillOptions);
125
131
  // Convert selected display names to SkillConfig objects
126
- selectedSkills = selectedSkillNames.map(sel => {
132
+ selectedSkills = selectedSkillNames.map((sel) => {
127
133
  const idx = skillOptions.indexOf(sel);
128
134
  const skill = availableSkills[idx];
129
135
  return {
@@ -134,18 +140,52 @@ export async function init(args) {
134
140
  };
135
141
  });
136
142
  if (selectedSkills.length > 0) {
137
- console.log(`\nSelected skills: ${selectedSkills.map(s => s.name).join(", ")}`);
143
+ console.log(`\nSelected skills: ${selectedSkills.map((s) => s.name).join(", ")}`);
138
144
  }
139
145
  else {
140
146
  console.log("\nNo skills selected.");
141
147
  }
142
148
  }
149
+ // Step 5: Select chat responder presets (optional)
150
+ const setupResponders = await promptConfirm("\nWould you like to set up chat responders?", false);
151
+ if (setupResponders) {
152
+ // First, ask if they want a bundle or individual presets
153
+ const selectionType = await promptSelectWithArrows("How would you like to configure responders?", [
154
+ "Use a preset bundle (recommended)",
155
+ "Select individual presets",
156
+ "Skip - configure later",
157
+ ]);
158
+ if (selectionType === "Use a preset bundle (recommended)") {
159
+ const bundleOptions = getBundleDisplayOptions();
160
+ const selectedBundle = await promptSelectWithArrows("Select a responder bundle:", bundleOptions);
161
+ const bundleId = displayOptionToBundleId(selectedBundle);
162
+ if (bundleId) {
163
+ selectedResponders = bundleToRespondersConfig(bundleId);
164
+ console.log(`\nConfigured responders from bundle: ${Object.keys(selectedResponders).join(", ")}`);
165
+ }
166
+ }
167
+ else if (selectionType === "Select individual presets") {
168
+ const presetOptions = getPresetDisplayOptions();
169
+ const selectedPresets = await promptMultiSelectWithArrows("Select responder presets to enable:", presetOptions);
170
+ // Convert display options back to preset IDs
171
+ const presetIds = selectedPresets
172
+ .map(displayOptionToPresetId)
173
+ .filter((id) => id !== undefined);
174
+ if (presetIds.length > 0) {
175
+ selectedResponders = presetsToRespondersConfig(presetIds);
176
+ console.log(`\nConfigured responders: ${Object.keys(selectedResponders).join(", ")}`);
177
+ }
178
+ }
179
+ else {
180
+ console.log("\nSkipping responders - you can configure them later in config.json");
181
+ }
182
+ }
143
183
  // Allow custom commands for "none" language
144
184
  checkCommand = config.checkCommand;
145
185
  testCommand = config.testCommand;
146
186
  if (selectedKey === "none") {
147
- checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
148
- testCommand = await promptInput("Enter your test command: ") || testCommand;
187
+ checkCommand = (await promptInput("\nEnter your type/build check command: ")) || checkCommand;
188
+ testCommand = (await promptInput("Enter your test command: ")) || testCommand;
149
189
  }
150
190
  }
151
191
  const finalConfig = {
@@ -154,7 +194,9 @@ export async function init(args) {
154
194
  testCommand,
155
195
  };
156
196
  // Generate image name from directory name
157
- const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
197
+ const projectName = basename(cwd)
198
+ .toLowerCase()
199
+ .replace(/[^a-z0-9-]/g, "-");
158
200
  const imageName = `ralph-${projectName}`;
159
201
  // Generate macOS development actions for Swift + SwiftUI projects
160
202
  const macOsActions = {};
@@ -197,6 +239,14 @@ export async function init(args) {
197
239
  // CLI configuration
198
240
  cli: cliConfig,
199
241
  cliProvider: selectedCliProviderKey,
242
+ // LLM providers for chat responders (empty by default, uses defaults from env vars)
243
+ // Example:
244
+ // llmProviders: {
245
+ // "claude": { type: "anthropic", model: "claude-sonnet-4-20250514" },
246
+ // "gpt4": { type: "openai", model: "gpt-4o" },
247
+ // "local": { type: "ollama", model: "llama3", baseUrl: "http://localhost:11434" }
248
+ // }
249
+ llmProviders: {},
200
250
  // Optional fields with defaults/empty values for discoverability
201
251
  notifyCommand: "",
202
252
  technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
@@ -244,6 +294,17 @@ export async function init(args) {
244
294
  botToken: "",
245
295
  allowedChatIds: [],
246
296
  },
297
+ // Chat responders - handle incoming messages based on triggers
298
+ // Special "default" responder handles messages that don't match any trigger
299
+ // Trigger patterns: "@name" for mentions, "keyword" for prefix matching
300
+ // Use "ralph init" to select from preset bundles, or configure manually:
301
+ // responders: {
302
+ // "default": { type: "llm", provider: "anthropic", systemPrompt: "You are a helpful assistant for {{project}}." },
303
+ // "qa": { type: "llm", trigger: "@qa", provider: "anthropic", systemPrompt: "Answer questions about the codebase." },
304
+ // "code": { type: "claude-code", trigger: "@code" },
305
+ // "lint": { type: "cli", trigger: "!lint", command: "npm run lint" }
306
+ // }
307
+ responders: selectedResponders,
247
308
  },
248
309
  // Daemon configuration for sandbox-to-host communication
249
310
  daemon: {
@@ -332,7 +393,7 @@ docker/.config-hash
332
393
  const swiftProjectName = basename(cwd)
333
394
  .replace(/[^a-zA-Z0-9]+/g, " ")
334
395
  .split(" ")
335
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
396
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
336
397
  .join("") || "App";
337
398
  if (!existsSync(genXcodePath)) {
338
399
  writeFileSync(genXcodePath, generateGenXcodeScript(swiftProjectName));
@@ -3,9 +3,81 @@
3
3
  * This enables Telegram/chat commands to execute inside the container.
4
4
  */
5
5
  import { spawn } from "child_process";
6
- import { existsSync, watch } from "fs";
6
+ import { existsSync, readFileSync, unlinkSync, watch } from "fs";
7
7
  import { isRunningInContainer } from "../utils/config.js";
8
8
  import { getMessagesPath, getPendingMessages, respondToMessage, cleanupOldMessages, } from "../utils/message-queue.js";
9
+ const RUN_PID_FILE = "/workspace/.ralph/run.pid";
10
+ /**
11
+ * Check if a ralph run process is currently running.
12
+ * Returns the PID if running, null otherwise.
13
+ */
14
+ function getRunningPid() {
15
+ if (!existsSync(RUN_PID_FILE)) {
16
+ return null;
17
+ }
18
+ try {
19
+ const pid = parseInt(readFileSync(RUN_PID_FILE, "utf-8").trim(), 10);
20
+ if (isNaN(pid)) {
21
+ return null;
22
+ }
23
+ // Check if process is still alive
24
+ try {
25
+ process.kill(pid, 0); // Signal 0 just checks if process exists
26
+ return pid;
27
+ }
28
+ catch {
29
+ // Process doesn't exist, clean up stale PID file
30
+ try {
31
+ unlinkSync(RUN_PID_FILE);
32
+ }
33
+ catch {
34
+ // Ignore cleanup errors
35
+ }
36
+ return null;
37
+ }
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ /**
44
+ * Stop a running ralph run process by PID.
45
+ * Returns true if successfully stopped, false otherwise.
46
+ */
47
+ function stopRunningProcess(pid) {
48
+ try {
49
+ // Kill the process group (negative PID) to also kill child processes (claude)
50
+ try {
51
+ process.kill(-pid, "SIGTERM");
52
+ }
53
+ catch {
54
+ // If process group kill fails, try killing just the process
55
+ process.kill(pid, "SIGTERM");
56
+ }
57
+ // Give it a moment to terminate gracefully
58
+ setTimeout(() => {
59
+ try {
60
+ // Check if still alive and force kill if necessary
61
+ process.kill(pid, 0);
62
+ process.kill(-pid, "SIGKILL");
63
+ }
64
+ catch {
65
+ // Already dead, good
66
+ }
67
+ }, 2000);
68
+ // Clean up PID file
69
+ try {
70
+ unlinkSync(RUN_PID_FILE);
71
+ }
72
+ catch {
73
+ // Ignore
74
+ }
75
+ return { success: true };
76
+ }
77
+ catch (err) {
78
+ return { success: false, error: `Failed to stop process: ${err}` };
79
+ }
80
+ }
9
81
  /**
10
82
  * Execute a shell command and return the result.
11
83
  */
@@ -90,6 +162,16 @@ async function processMessage(message, messagesPath, debug) {
90
162
  break;
91
163
  }
92
164
  case "run": {
165
+ // Check if ralph run is already running
166
+ const existingPid = getRunningPid();
167
+ if (existingPid) {
168
+ console.log(`[listen] Ralph run already running (PID ${existingPid})`);
169
+ respondToMessage(messagesPath, message.id, {
170
+ success: false,
171
+ error: `Ralph run is already running (PID ${existingPid}). Use /stop to terminate it first.`,
172
+ });
173
+ return;
174
+ }
93
175
  // Start ralph run in background
94
176
  // Support optional category filter: run [category]
95
177
  const runArgs = ["run"];
@@ -108,10 +190,38 @@ async function processMessage(message, messagesPath, debug) {
108
190
  proc.unref();
109
191
  respondToMessage(messagesPath, message.id, {
110
192
  success: true,
111
- output: message.args?.length ? `Ralph run started (category: ${message.args[0]})` : "Ralph run started",
193
+ output: message.args?.length
194
+ ? `Ralph run started (category: ${message.args[0]})`
195
+ : "Ralph run started",
112
196
  });
113
197
  break;
114
198
  }
199
+ case "stop": {
200
+ // Stop a running ralph run process
201
+ const runningPid = getRunningPid();
202
+ if (!runningPid) {
203
+ respondToMessage(messagesPath, message.id, {
204
+ success: true,
205
+ output: "No ralph run process is currently running.",
206
+ });
207
+ return;
208
+ }
209
+ console.log(`[listen] Stopping ralph run (PID ${runningPid})...`);
210
+ const stopResult = stopRunningProcess(runningPid);
211
+ if (stopResult.success) {
212
+ respondToMessage(messagesPath, message.id, {
213
+ success: true,
214
+ output: `Stopped ralph run (PID ${runningPid})`,
215
+ });
216
+ }
217
+ else {
218
+ respondToMessage(messagesPath, message.id, {
219
+ success: false,
220
+ error: stopResult.error,
221
+ });
222
+ }
223
+ break;
224
+ }
115
225
  case "status": {
116
226
  // Get PRD status
117
227
  const result = await executeCommand("ralph status");
@@ -164,7 +274,7 @@ async function processMessage(message, messagesPath, debug) {
164
274
  default:
165
275
  respondToMessage(messagesPath, message.id, {
166
276
  success: false,
167
- error: `Unknown action: ${action}. Supported: exec, run, status, ping, claude`,
277
+ error: `Unknown action: ${action}. Supported: exec, run, stop, status, ping, claude`,
168
278
  });
169
279
  }
170
280
  }
@@ -179,7 +289,7 @@ async function startListening(debug) {
179
289
  console.log(`Messages file: ${messagesPath}`);
180
290
  console.log("");
181
291
  console.log("Listening for commands from host...");
182
- console.log("Supported actions: exec, run, status, ping, claude");
292
+ console.log("Supported actions: exec, run, stop, status, ping, claude");
183
293
  console.log("");
184
294
  console.log("Press Ctrl+C to stop.");
185
295
  // Process any pending messages on startup
@@ -253,7 +363,8 @@ DESCRIPTION:
253
363
 
254
364
  SUPPORTED ACTIONS:
255
365
  exec [cmd] Execute a shell command in the sandbox
256
- run Start ralph run
366
+ run Start ralph run (fails if already running)
367
+ stop Stop a running ralph run process
257
368
  status Get PRD status
258
369
  ping Health check
259
370
  claude [prompt] Run Claude Code with prompt (YOLO mode)
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Hidden command to display the Ralph CLI ASCII art logo.
3
+ * Easter egg - not shown in help.
4
+ */
5
+ export declare function logo(): Promise<void>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Hidden command to display the Ralph CLI ASCII art logo.
3
+ * Easter egg - not shown in help.
4
+ */
5
+ // Yellow gradient colors (Ralph Wiggum style)
6
+ const GRADIENT = [
7
+ "\x1b[38;2;255;245;157m", // #FFF59D - pale yellow
8
+ "\x1b[38;2;255;238;88m", // #FFEE58 - light yellow
9
+ "\x1b[38;2;255;235;59m", // #FFEB3B - Simpsons yellow
10
+ "\x1b[38;2;253;216;53m", // #FDD835 - medium yellow
11
+ "\x1b[38;2;251;192;45m", // #FBC02D - golden yellow
12
+ "\x1b[38;2;249;168;37m", // #F9A825 - deep gold
13
+ ];
14
+ const RESET = "\x1b[0m";
15
+ const GRAY = "\x1b[38;5;248m";
16
+ const LOGO_LINES = [
17
+ "██████╗ █████╗ ██╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗",
18
+ "██╔══██╗██╔══██╗██║ ██╔══██╗██║ ██║ ██╔════╝██║ ██║",
19
+ "██████╔╝███████║██║ ██████╔╝███████║ ██║ ██║ ██║",
20
+ "██╔══██╗██╔══██║██║ ██╔═══╝ ██╔══██║ ██║ ██║ ██║",
21
+ "██║ ██║██║ ██║███████╗██║ ██║ ██║ ╚██████╗███████╗██║",
22
+ "╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝",
23
+ ];
24
+ export async function logo() {
25
+ console.log("");
26
+ for (let i = 0; i < LOGO_LINES.length; i++) {
27
+ const color = GRADIENT[i] || GRADIENT[GRADIENT.length - 1];
28
+ // Add "sandboxed" in yellow on line 3 (index 2)
29
+ const suffix = i === 2 ? ` ${GRADIENT[i]}sandboxed${RESET}` : "";
30
+ console.log(`${color}${LOGO_LINES[i]}${RESET}${suffix}`);
31
+ }
32
+ // Get version
33
+ try {
34
+ const pkg = await import("../../package.json", { with: { type: "json" } });
35
+ console.log(`${GRAY}v${pkg.default.version}${RESET}`);
36
+ }
37
+ catch {
38
+ console.log(`${GRAY}ralph-cli${RESET}`);
39
+ }
40
+ console.log("");
41
+ }
@@ -1,7 +1,7 @@
1
1
  import { isRunningInContainer } from "../utils/config.js";
2
2
  import { sendNotification } from "../utils/notification.js";
3
3
  import { loadConfig } from "../utils/config.js";
4
- import { getMessagesPath, sendMessage, waitForResponse, } from "../utils/message-queue.js";
4
+ import { getMessagesPath, sendMessage, waitForResponse } from "../utils/message-queue.js";
5
5
  import { existsSync } from "fs";
6
6
  /**
7
7
  * Send a notification - works both inside and outside containers.
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import { existsSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
- import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
4
+ import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer, } from "../utils/config.js";
5
5
  import { resolvePromptVariables, getCliProviders } from "../templates/prompts.js";
6
6
  import { getStreamJsonParser } from "../utils/stream-json.js";
7
7
  import { sendNotificationWithDaemonEvents } from "../utils/notification.js";
@@ -58,10 +58,7 @@ export async function once(args) {
58
58
  // Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
59
59
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
60
60
  const promptArgs = cliConfig.promptArgs ?? ["-p"];
61
- const cliArgs = [
62
- ...(cliConfig.args ?? []),
63
- ...yoloArgs,
64
- ];
61
+ const cliArgs = [...(cliConfig.args ?? []), ...yoloArgs];
65
62
  // Build the prompt value based on whether fileArgs is configured
66
63
  // fileArgs (e.g., ["--read"] for Aider) means files are passed as separate arguments
67
64
  // Otherwise, use @file syntax embedded in the prompt (Claude Code style)
@@ -98,7 +95,7 @@ export async function once(args) {
98
95
  }
99
96
  cliArgs.push(...promptArgs, promptValue);
100
97
  if (debug) {
101
- console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
98
+ console.log(`[debug] ${cliConfig.command} ${cliArgs.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}\n`);
102
99
  if (jsonLogPath) {
103
100
  console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
104
101
  }
@@ -106,7 +103,12 @@ export async function once(args) {
106
103
  // Create provider-specific stream-json parser
107
104
  const streamJsonParser = getStreamJsonParser(config.cliProvider, debug);
108
105
  // Notification options for this run
109
- const notifyOptions = { command: config.notifyCommand, debug, daemonConfig: config.daemon };
106
+ const notifyOptions = {
107
+ command: config.notifyCommand,
108
+ debug,
109
+ daemonConfig: config.daemon,
110
+ chatConfig: config.chat,
111
+ };
110
112
  return new Promise((resolve, reject) => {
111
113
  let output = ""; // Accumulate output for PRD complete detection
112
114
  if (streamJsonEnabled) {
@@ -180,7 +182,11 @@ export async function once(args) {
180
182
  // Send notification based on outcome
181
183
  if (code !== 0) {
182
184
  console.error(`\n${cliConfig.command} exited with code ${code}`);
183
- await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
185
+ const errorMessage = `Iteration failed with exit code ${code}`;
186
+ await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
187
+ ...notifyOptions,
188
+ errorMessage,
189
+ });
184
190
  }
185
191
  else if (output.includes("<promise>COMPLETE</promise>")) {
186
192
  await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
@@ -208,7 +214,11 @@ export async function once(args) {
208
214
  // Send notification based on outcome
209
215
  if (code !== 0) {
210
216
  console.error(`\n${cliConfig.command} exited with code ${code}`);
211
- await sendNotificationWithDaemonEvents("error", `Ralph: Iteration failed with exit code ${code}`, notifyOptions);
217
+ const errorMessage = `Iteration failed with exit code ${code}`;
218
+ await sendNotificationWithDaemonEvents("error", `Ralph: ${errorMessage}`, {
219
+ ...notifyOptions,
220
+ errorMessage,
221
+ });
212
222
  }
213
223
  else if (output.includes("<promise>COMPLETE</promise>")) {
214
224
  await sendNotificationWithDaemonEvents("prd_complete", undefined, notifyOptions);
@@ -12,7 +12,22 @@ function loadPrd() {
12
12
  if (!existsSync(path)) {
13
13
  throw new Error(".ralph/prd.json not found. Run 'ralph init' first.");
14
14
  }
15
- return JSON.parse(readFileSync(path, "utf-8"));
15
+ const content = readFileSync(path, "utf-8");
16
+ try {
17
+ return JSON.parse(content);
18
+ }
19
+ catch (err) {
20
+ const message = err instanceof SyntaxError ? err.message : "Invalid JSON";
21
+ console.error(`Error parsing .ralph/prd.json: ${message}`);
22
+ console.error("");
23
+ console.error("Common issues:");
24
+ console.error(" - Trailing comma before ] or }");
25
+ console.error(" - Missing comma between entries");
26
+ console.error(" - Unescaped quotes in strings");
27
+ console.error("");
28
+ console.error("Run 'ralph fix-prd' to attempt automatic repair.");
29
+ process.exit(1);
30
+ }
16
31
  }
17
32
  function savePrd(entries) {
18
33
  writeFileSync(getPrdPath(), JSON.stringify(entries, null, 2) + "\n");
@@ -219,7 +234,10 @@ export function parseListArgs(args) {
219
234
  else if (args[i] === "--passes" || args[i] === "--passed") {
220
235
  passesFilter = true;
221
236
  }
222
- else if (args[i] === "--no-passes" || args[i] === "--no-passed" || args[i] === "--not-passed" || args[i] === "--not-passes") {
237
+ else if (args[i] === "--no-passes" ||
238
+ args[i] === "--no-passed" ||
239
+ args[i] === "--not-passed" ||
240
+ args[i] === "--not-passes") {
223
241
  passesFilter = false;
224
242
  }
225
243
  }