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.
Files changed (69) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +9 -9
  3. package/dist/commands/chat.js +13 -12
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/daemon.js +4 -3
  6. package/dist/commands/docker.js +102 -66
  7. package/dist/commands/fix-config.js +2 -1
  8. package/dist/commands/fix-prd.js +2 -2
  9. package/dist/commands/init.js +78 -17
  10. package/dist/commands/listen.js +3 -1
  11. package/dist/commands/notify.js +1 -1
  12. package/dist/commands/once.js +17 -9
  13. package/dist/commands/prd.js +4 -1
  14. package/dist/commands/run.js +40 -25
  15. package/dist/commands/slack.js +2 -2
  16. package/dist/config/responder-presets.json +69 -0
  17. package/dist/index.js +1 -1
  18. package/dist/providers/discord.d.ts +28 -0
  19. package/dist/providers/discord.js +227 -14
  20. package/dist/providers/slack.d.ts +41 -1
  21. package/dist/providers/slack.js +389 -8
  22. package/dist/providers/telegram.d.ts +30 -0
  23. package/dist/providers/telegram.js +185 -5
  24. package/dist/responders/claude-code-responder.d.ts +48 -0
  25. package/dist/responders/claude-code-responder.js +203 -0
  26. package/dist/responders/cli-responder.d.ts +62 -0
  27. package/dist/responders/cli-responder.js +298 -0
  28. package/dist/responders/llm-responder.d.ts +135 -0
  29. package/dist/responders/llm-responder.js +582 -0
  30. package/dist/templates/macos-scripts.js +2 -4
  31. package/dist/templates/prompts.js +4 -2
  32. package/dist/tui/ConfigEditor.js +19 -5
  33. package/dist/tui/components/ArrayEditor.js +1 -1
  34. package/dist/tui/components/EditorPanel.js +10 -6
  35. package/dist/tui/components/HelpPanel.d.ts +1 -1
  36. package/dist/tui/components/HelpPanel.js +1 -1
  37. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  38. package/dist/tui/components/KeyValueEditor.js +54 -9
  39. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  40. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  41. package/dist/tui/components/ObjectEditor.js +1 -1
  42. package/dist/tui/components/Preview.js +1 -1
  43. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  44. package/dist/tui/components/RespondersEditor.js +437 -0
  45. package/dist/tui/components/SectionNav.js +27 -3
  46. package/dist/utils/chat-client.d.ts +4 -0
  47. package/dist/utils/chat-client.js +12 -5
  48. package/dist/utils/config.d.ts +84 -0
  49. package/dist/utils/config.js +78 -1
  50. package/dist/utils/daemon-client.d.ts +21 -0
  51. package/dist/utils/daemon-client.js +28 -1
  52. package/dist/utils/llm-client.d.ts +82 -0
  53. package/dist/utils/llm-client.js +185 -0
  54. package/dist/utils/message-queue.js +6 -6
  55. package/dist/utils/notification.d.ts +6 -1
  56. package/dist/utils/notification.js +103 -2
  57. package/dist/utils/prd-validator.js +60 -19
  58. package/dist/utils/prompt.js +22 -12
  59. package/dist/utils/responder-logger.d.ts +47 -0
  60. package/dist/utils/responder-logger.js +129 -0
  61. package/dist/utils/responder-presets.d.ts +92 -0
  62. package/dist/utils/responder-presets.js +156 -0
  63. package/dist/utils/responder.d.ts +88 -0
  64. package/dist/utils/responder.js +207 -0
  65. package/dist/utils/stream-json.js +6 -6
  66. package/docs/CHAT-RESPONDERS.md +785 -0
  67. package/docs/DEVELOPMENT.md +25 -0
  68. package/docs/chat-architecture.md +251 -0
  69. package/package.json +11 -1
@@ -0,0 +1,298 @@
1
+ /**
2
+ * CLI Responder - Executes configured CLI commands with user messages.
3
+ * Useful for integrating with aider, custom scripts, or other AI CLIs.
4
+ */
5
+ import { spawn } from "child_process";
6
+ import { truncateResponse } from "./llm-responder.js";
7
+ /**
8
+ * Default timeout for CLI execution (2 minutes).
9
+ */
10
+ const DEFAULT_TIMEOUT = 120000;
11
+ /**
12
+ * Default max length for chat responses (characters).
13
+ */
14
+ const DEFAULT_MAX_LENGTH = 2000;
15
+ /**
16
+ * Interval for sending progress updates (milliseconds).
17
+ */
18
+ const PROGRESS_INTERVAL = 5000;
19
+ /**
20
+ * Replaces {{message}} placeholder in command string with the actual message.
21
+ * Escapes the message to prevent shell injection.
22
+ */
23
+ export function replaceMessagePlaceholder(command, message) {
24
+ // Escape single quotes in the message for safe shell interpolation
25
+ const escapedMessage = message.replace(/'/g, "'\\''");
26
+ return command.replace(/\{\{message\}\}/g, escapedMessage);
27
+ }
28
+ /**
29
+ * Parses a command string into command and arguments.
30
+ * Handles quoted strings and basic shell syntax.
31
+ */
32
+ export function parseCommand(commandString) {
33
+ const tokens = [];
34
+ let current = "";
35
+ let inSingleQuote = false;
36
+ let inDoubleQuote = false;
37
+ let escapeNext = false;
38
+ for (let i = 0; i < commandString.length; i++) {
39
+ const char = commandString[i];
40
+ if (escapeNext) {
41
+ current += char;
42
+ escapeNext = false;
43
+ continue;
44
+ }
45
+ if (char === "\\") {
46
+ escapeNext = true;
47
+ continue;
48
+ }
49
+ if (char === "'" && !inDoubleQuote) {
50
+ inSingleQuote = !inSingleQuote;
51
+ continue;
52
+ }
53
+ if (char === '"' && !inSingleQuote) {
54
+ inDoubleQuote = !inDoubleQuote;
55
+ continue;
56
+ }
57
+ if (char === " " && !inSingleQuote && !inDoubleQuote) {
58
+ if (current.length > 0) {
59
+ tokens.push(current);
60
+ current = "";
61
+ }
62
+ continue;
63
+ }
64
+ current += char;
65
+ }
66
+ if (current.length > 0) {
67
+ tokens.push(current);
68
+ }
69
+ const [command, ...args] = tokens;
70
+ return { command: command || "", args };
71
+ }
72
+ /**
73
+ * Executes a CLI command with the given message.
74
+ *
75
+ * The command string can include {{message}} placeholder which will be replaced
76
+ * with the user's message. If no placeholder is present, the message is appended
77
+ * as an argument.
78
+ *
79
+ * @param message The user message to include in the command
80
+ * @param responderConfig The responder configuration
81
+ * @param options Optional execution options
82
+ * @returns The responder result with response or error
83
+ */
84
+ export async function executeCLIResponder(message, responderConfig, options) {
85
+ const timeout = options?.timeout ?? responderConfig.timeout ?? DEFAULT_TIMEOUT;
86
+ const maxLength = options?.maxLength ?? responderConfig.maxLength ?? DEFAULT_MAX_LENGTH;
87
+ const cwd = options?.cwd ?? process.cwd();
88
+ const onProgress = options?.onProgress;
89
+ const additionalEnv = options?.env ?? {};
90
+ // Get command from config
91
+ const commandTemplate = responderConfig.command;
92
+ if (!commandTemplate) {
93
+ return {
94
+ success: false,
95
+ response: "",
96
+ error: "CLI responder requires a 'command' field in configuration",
97
+ };
98
+ }
99
+ // Replace {{message}} placeholder or append message as argument
100
+ let commandString;
101
+ if (commandTemplate.includes("{{message}}")) {
102
+ commandString = replaceMessagePlaceholder(commandTemplate, message);
103
+ }
104
+ else {
105
+ // Append message as a quoted argument
106
+ const escapedMessage = message.replace(/'/g, "'\\''");
107
+ commandString = `${commandTemplate} '${escapedMessage}'`;
108
+ }
109
+ // Parse the command string
110
+ const { command, args } = parseCommand(commandString);
111
+ if (!command) {
112
+ return {
113
+ success: false,
114
+ response: "",
115
+ error: "Failed to parse command from configuration",
116
+ };
117
+ }
118
+ return new Promise((resolve) => {
119
+ let stdout = "";
120
+ let stderr = "";
121
+ let killed = false;
122
+ let lastProgressSent = 0;
123
+ let progressTimer = null;
124
+ // Spawn the process
125
+ let proc;
126
+ try {
127
+ proc = spawn(command, args, {
128
+ cwd,
129
+ stdio: ["ignore", "pipe", "pipe"],
130
+ shell: false,
131
+ env: { ...process.env, ...additionalEnv },
132
+ });
133
+ }
134
+ catch (err) {
135
+ const error = err instanceof Error ? err.message : String(err);
136
+ resolve({
137
+ success: false,
138
+ response: "",
139
+ error: `Failed to spawn command "${command}": ${error}`,
140
+ });
141
+ return;
142
+ }
143
+ // Handle timeout
144
+ const timeoutTimer = setTimeout(() => {
145
+ killed = true;
146
+ proc.kill("SIGTERM");
147
+ // Give it a moment to terminate gracefully, then force kill
148
+ setTimeout(() => {
149
+ try {
150
+ proc.kill("SIGKILL");
151
+ }
152
+ catch {
153
+ // Already dead
154
+ }
155
+ }, 2000);
156
+ if (progressTimer) {
157
+ clearInterval(progressTimer);
158
+ }
159
+ resolve({
160
+ success: false,
161
+ response: stdout,
162
+ error: `Command timed out after ${Math.round(timeout / 1000)} seconds`,
163
+ });
164
+ }, timeout);
165
+ // Set up progress updates
166
+ if (onProgress) {
167
+ progressTimer = setInterval(() => {
168
+ const now = Date.now();
169
+ if (now - lastProgressSent >= PROGRESS_INTERVAL && stdout.length > 0) {
170
+ // Send a progress indicator
171
+ const lines = stdout.split("\n");
172
+ const lastLine = lines[lines.length - 1] || lines[lines.length - 2] || "";
173
+ const truncatedLine = lastLine.length > 100 ? lastLine.substring(0, 100) + "..." : lastLine;
174
+ onProgress(`⏳ Running... ${truncatedLine}`);
175
+ lastProgressSent = now;
176
+ }
177
+ }, PROGRESS_INTERVAL);
178
+ }
179
+ // Capture stdout
180
+ proc.stdout?.on("data", (data) => {
181
+ stdout += data.toString();
182
+ });
183
+ // Capture stderr
184
+ proc.stderr?.on("data", (data) => {
185
+ stderr += data.toString();
186
+ });
187
+ // Handle process completion
188
+ proc.on("close", (code) => {
189
+ if (killed)
190
+ return;
191
+ clearTimeout(timeoutTimer);
192
+ if (progressTimer) {
193
+ clearInterval(progressTimer);
194
+ }
195
+ if (code === 0 || code === null) {
196
+ // Success - format and truncate output
197
+ const output = formatCLIOutput(stdout, stderr);
198
+ const { text, truncated, originalLength } = truncateResponse(output, maxLength);
199
+ resolve({
200
+ success: true,
201
+ response: text,
202
+ truncated,
203
+ originalLength: truncated ? originalLength : undefined,
204
+ });
205
+ }
206
+ else {
207
+ // Failure - include stderr in error message
208
+ const errorMsg = stderr.trim() || `Command exited with code ${code}`;
209
+ const output = formatCLIOutput(stdout, "");
210
+ const { text, truncated, originalLength } = truncateResponse(output, maxLength);
211
+ resolve({
212
+ success: false,
213
+ response: text,
214
+ error: errorMsg,
215
+ truncated,
216
+ originalLength: truncated ? originalLength : undefined,
217
+ });
218
+ }
219
+ });
220
+ // Handle spawn errors
221
+ proc.on("error", (err) => {
222
+ if (killed)
223
+ return;
224
+ clearTimeout(timeoutTimer);
225
+ if (progressTimer) {
226
+ clearInterval(progressTimer);
227
+ }
228
+ resolve({
229
+ success: false,
230
+ response: "",
231
+ error: `Command error: ${err.message}`,
232
+ });
233
+ });
234
+ });
235
+ }
236
+ /**
237
+ * Formats CLI output for chat display.
238
+ * Cleans up ANSI codes, excessive whitespace, and combines stdout/stderr.
239
+ */
240
+ function formatCLIOutput(stdout, stderr) {
241
+ // Combine stdout and stderr
242
+ let output = stdout;
243
+ if (stderr.trim()) {
244
+ output = output.trim() + "\n\n[stderr]\n" + stderr.trim();
245
+ }
246
+ // Remove ANSI escape codes
247
+ let cleaned = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
248
+ // Remove carriage returns (used for progress overwriting)
249
+ cleaned = cleaned.replace(/\r/g, "");
250
+ // Collapse multiple blank lines into one
251
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
252
+ // Trim leading/trailing whitespace
253
+ cleaned = cleaned.trim();
254
+ // If output is empty, provide a default message
255
+ if (!cleaned) {
256
+ return "(Command completed with no output)";
257
+ }
258
+ return cleaned;
259
+ }
260
+ /**
261
+ * Creates a reusable CLI responder function.
262
+ * This is useful for handling multiple messages with the same configuration.
263
+ *
264
+ * @param responderConfig The responder configuration
265
+ * @returns A function that executes the responder with a message
266
+ */
267
+ export function createCLIResponder(responderConfig) {
268
+ return async (message, options) => {
269
+ return executeCLIResponder(message, responderConfig, options);
270
+ };
271
+ }
272
+ /**
273
+ * Validates that a responder configuration is valid for CLI execution.
274
+ *
275
+ * @param responderConfig The responder configuration to validate
276
+ * @returns An error message if invalid, or null if valid
277
+ */
278
+ export function validateCLIResponder(responderConfig) {
279
+ if (responderConfig.type !== "cli") {
280
+ return `Responder type is "${responderConfig.type}", expected "cli"`;
281
+ }
282
+ if (!responderConfig.command) {
283
+ return "CLI responder requires a 'command' field";
284
+ }
285
+ if (typeof responderConfig.command !== "string") {
286
+ return "CLI responder 'command' field must be a string";
287
+ }
288
+ // Check that timeout is reasonable if specified
289
+ if (responderConfig.timeout !== undefined) {
290
+ if (responderConfig.timeout < 1000) {
291
+ return `Timeout ${responderConfig.timeout}ms is too short (minimum: 1000ms)`;
292
+ }
293
+ if (responderConfig.timeout > 600000) {
294
+ return `Timeout ${responderConfig.timeout}ms is too long (maximum: 600000ms / 10 minutes)`;
295
+ }
296
+ }
297
+ return null;
298
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * LLM Responder - Sends messages to LLM providers and returns responses.
3
+ * Used by chat clients to respond to messages matched by the responder matcher.
4
+ */
5
+ import { ResponderConfig, RalphConfig } from "../utils/config.js";
6
+ /**
7
+ * Result of executing a responder.
8
+ */
9
+ export interface ResponderResult {
10
+ /** Whether the responder executed successfully */
11
+ success: boolean;
12
+ /** The response text (may be truncated) */
13
+ response: string;
14
+ /** Error message if success is false */
15
+ error?: string;
16
+ /** Whether the response was truncated */
17
+ truncated?: boolean;
18
+ /** Original response length before truncation */
19
+ originalLength?: number;
20
+ }
21
+ /**
22
+ * A message in conversation history.
23
+ */
24
+ export interface ConversationMessage {
25
+ role: "user" | "assistant";
26
+ content: string;
27
+ }
28
+ /**
29
+ * Options for executing an LLM responder.
30
+ */
31
+ export interface LLMResponderOptions {
32
+ /** Override the project name for {{project}} placeholder */
33
+ projectName?: string;
34
+ /** Override default max tokens */
35
+ maxTokens?: number;
36
+ /** Override default temperature */
37
+ temperature?: number;
38
+ /** Responder name for logging */
39
+ responderName?: string;
40
+ /** Responder trigger for logging */
41
+ trigger?: string;
42
+ /** Thread context length for logging */
43
+ threadContextLength?: number;
44
+ /** Enable debug logging to console */
45
+ debug?: boolean;
46
+ /** Previous conversation messages for multi-turn chat */
47
+ conversationHistory?: ConversationMessage[];
48
+ }
49
+ /**
50
+ * Replaces {{project}} placeholder in system prompt with actual project name.
51
+ */
52
+ export declare function applyProjectPlaceholder(systemPrompt: string, projectName: string): string;
53
+ /**
54
+ * Gets the project name from the current working directory.
55
+ */
56
+ export declare function getProjectName(): string;
57
+ /**
58
+ * Result of processing a git diff request.
59
+ */
60
+ export interface GitDiffResult {
61
+ message: string;
62
+ diffIncluded: boolean;
63
+ gitCommand?: string;
64
+ diffLength?: number;
65
+ }
66
+ /**
67
+ * Detects if the message contains a git diff request and fetches the diff.
68
+ * Returns the message with diff content prepended, or the original message if no diff requested.
69
+ */
70
+ export declare function processGitDiffRequest(message: string): GitDiffResult;
71
+ /**
72
+ * Result of detecting and reading files from a message.
73
+ */
74
+ export interface FileDetectionResult {
75
+ /** Files that were found and read */
76
+ filesRead: Array<{
77
+ path: string;
78
+ content: string;
79
+ lineNumber?: number;
80
+ truncated: boolean;
81
+ }>;
82
+ /** Files that were mentioned but not found */
83
+ filesNotFound: string[];
84
+ /** Total content length of all files */
85
+ totalLength: number;
86
+ }
87
+ /**
88
+ * Detects file paths in a message and reads their contents.
89
+ * Supports formats like:
90
+ * - src/utils/config.ts
91
+ * - src/utils/config.ts:42 (with line number)
92
+ * - ./relative/path.js
93
+ * - package.json
94
+ */
95
+ export declare function detectAndReadFiles(message: string): FileDetectionResult;
96
+ /**
97
+ * Formats detected files as context to prepend to the user message.
98
+ */
99
+ export declare function formatFileContext(fileResult: FileDetectionResult): string;
100
+ /**
101
+ * Truncates a response to the specified max length.
102
+ * Adds a truncation indicator if the response was shortened.
103
+ */
104
+ export declare function truncateResponse(response: string, maxLength: number): {
105
+ text: string;
106
+ truncated: boolean;
107
+ originalLength: number;
108
+ };
109
+ /**
110
+ * Executes an LLM responder with the given message.
111
+ *
112
+ * @param message The user message to send to the LLM
113
+ * @param responderConfig The responder configuration
114
+ * @param config Optional Ralph config (loaded automatically if not provided)
115
+ * @param options Optional execution options
116
+ * @returns The responder result with response or error
117
+ */
118
+ export declare function executeLLMResponder(message: string, responderConfig: ResponderConfig, config?: RalphConfig, options?: LLMResponderOptions): Promise<ResponderResult>;
119
+ /**
120
+ * Creates a reusable LLM responder function with pre-loaded configuration.
121
+ * This is useful for handling multiple messages without reloading config each time.
122
+ *
123
+ * @param responderConfig The responder configuration
124
+ * @param config The Ralph configuration
125
+ * @returns A function that executes the responder with a message
126
+ */
127
+ export declare function createLLMResponder(responderConfig: ResponderConfig, config: RalphConfig): (message: string, options?: LLMResponderOptions) => Promise<ResponderResult>;
128
+ /**
129
+ * Validates that a responder configuration is valid for LLM execution.
130
+ *
131
+ * @param responderConfig The responder configuration to validate
132
+ * @param config The Ralph configuration for looking up providers
133
+ * @returns An error message if invalid, or null if valid
134
+ */
135
+ export declare function validateLLMResponder(responderConfig: ResponderConfig, config: RalphConfig): string | null;