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
@@ -86,14 +86,14 @@ export async function waitForResponse(messagesPath, messageId, timeout = 10000,
86
86
  const startTime = Date.now();
87
87
  while (Date.now() - startTime < timeout) {
88
88
  const messages = readMessages(messagesPath);
89
- const message = messages.find(m => m.id === messageId);
89
+ const message = messages.find((m) => m.id === messageId);
90
90
  if (message?.status === "done" && message.response) {
91
91
  // Clean up processed message
92
- const remaining = messages.filter(m => m.id !== messageId);
92
+ const remaining = messages.filter((m) => m.id !== messageId);
93
93
  writeMessages(messagesPath, remaining);
94
94
  return message.response;
95
95
  }
96
- await new Promise(resolve => setTimeout(resolve, pollInterval));
96
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
97
97
  }
98
98
  return null;
99
99
  }
@@ -102,14 +102,14 @@ export async function waitForResponse(messagesPath, messageId, timeout = 10000,
102
102
  */
103
103
  export function getPendingMessages(messagesPath, from) {
104
104
  const messages = readMessages(messagesPath);
105
- return messages.filter(m => m.from === from && m.status === "pending");
105
+ return messages.filter((m) => m.from === from && m.status === "pending");
106
106
  }
107
107
  /**
108
108
  * Mark a message as done with a response.
109
109
  */
110
110
  export function respondToMessage(messagesPath, messageId, response) {
111
111
  const messages = readMessages(messagesPath);
112
- const message = messages.find(m => m.id === messageId);
112
+ const message = messages.find((m) => m.id === messageId);
113
113
  if (!message) {
114
114
  return false;
115
115
  }
@@ -124,7 +124,7 @@ export function respondToMessage(messagesPath, messageId, response) {
124
124
  export function cleanupOldMessages(messagesPath, maxAge = 60000) {
125
125
  const messages = readMessages(messagesPath);
126
126
  const now = Date.now();
127
- const remaining = messages.filter(m => now - m.timestamp < maxAge);
127
+ const remaining = messages.filter((m) => now - m.timestamp < maxAge);
128
128
  const removed = messages.length - remaining.length;
129
129
  if (removed > 0) {
130
130
  writeMessages(messagesPath, remaining);
@@ -1,4 +1,4 @@
1
- import { DaemonEventType, DaemonConfig } from "./config.js";
1
+ import { DaemonEventType, DaemonConfig, ChatConfig } from "./config.js";
2
2
  export interface NotificationOptions {
3
3
  /** The notification command from config (e.g., "ntfy pub mytopic" or "notify-send") */
4
4
  command?: string;
@@ -8,8 +8,12 @@ export interface NotificationOptions {
8
8
  useDaemon?: boolean;
9
9
  /** Daemon configuration for event-based notifications */
10
10
  daemonConfig?: DaemonConfig;
11
+ /** Chat configuration for automatic chat notifications */
12
+ chatConfig?: ChatConfig;
11
13
  /** Task name for task_complete events (used in message placeholders) */
12
14
  taskName?: string;
15
+ /** Error message for error events (used in {{error}} placeholder) */
16
+ errorMessage?: string;
13
17
  }
14
18
  export type NotificationEvent = "prd_complete" | "iteration_complete" | "run_stopped" | "task_complete" | "error";
15
19
  /**
@@ -39,15 +43,19 @@ export declare function createNotifier(options: NotificationOptions): (event: No
39
43
  *
40
44
  * @param event The daemon event type
41
45
  * @param options Notification options containing daemon config
42
- * @param context Additional context (e.g., task name for task_complete)
46
+ * @param context Additional context (e.g., task name for task_complete, error message for error)
43
47
  */
44
48
  export declare function triggerDaemonEvents(event: DaemonEventType, options?: NotificationOptions, context?: {
45
49
  taskName?: string;
50
+ errorMessage?: string;
46
51
  }): Promise<void>;
47
52
  /**
48
53
  * Send notification and also trigger any configured daemon events.
49
54
  * This is the recommended function to use for comprehensive notification handling.
50
55
  *
56
+ * When running in a container with chat providers configured (Slack, Telegram, Discord),
57
+ * notifications are automatically sent to those channels.
58
+ *
51
59
  * @param event The notification event type
52
60
  * @param message Optional custom message
53
61
  * @param options Notification options
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
- import { isRunningInContainer } from "./config.js";
3
- import { isDaemonAvailable, sendDaemonNotification, sendDaemonRequest } from "./daemon-client.js";
2
+ import { isRunningInContainer, } from "./config.js";
3
+ import { isDaemonAvailable, sendDaemonNotification, sendDaemonRequest, sendSlackNotification, sendTelegramNotification, sendDiscordNotification, } from "./daemon-client.js";
4
4
  /**
5
5
  * Send a notification using the configured notify command.
6
6
  *
@@ -120,7 +120,7 @@ function mapEventToDaemonEvent(event) {
120
120
  *
121
121
  * @param event The daemon event type
122
122
  * @param options Notification options containing daemon config
123
- * @param context Additional context (e.g., task name for task_complete)
123
+ * @param context Additional context (e.g., task name for task_complete, error message for error)
124
124
  */
125
125
  export async function triggerDaemonEvents(event, options, context) {
126
126
  const { daemonConfig, debug } = options ?? {};
@@ -150,6 +150,9 @@ export async function triggerDaemonEvents(event, options, context) {
150
150
  if (context?.taskName) {
151
151
  message = message.replace(/\{\{task\}\}/g, context.taskName);
152
152
  }
153
+ if (context?.errorMessage) {
154
+ message = message.replace(/\{\{error\}\}/g, context.errorMessage);
155
+ }
153
156
  // Build args array
154
157
  const args = [...(handler.args || [])];
155
158
  if (message) {
@@ -176,20 +179,124 @@ export async function triggerDaemonEvents(event, options, context) {
176
179
  }
177
180
  }
178
181
  }
182
+ /**
183
+ * Send notifications to connected chat providers (Slack, Telegram, Discord).
184
+ * This automatically sends notifications to all enabled chat providers when
185
+ * running inside a container with the daemon available.
186
+ *
187
+ * @param message The notification message
188
+ * @param options Notification options containing chat config
189
+ */
190
+ async function sendChatNotifications(message, options) {
191
+ const { chatConfig, debug } = options ?? {};
192
+ // Only send chat notifications when in container with daemon available
193
+ if (!isRunningInContainer() || !isDaemonAvailable()) {
194
+ return;
195
+ }
196
+ // Check if Slack is configured and enabled
197
+ const slackEnabled = chatConfig?.slack?.botToken &&
198
+ chatConfig?.slack?.appToken &&
199
+ chatConfig?.slack?.signingSecret &&
200
+ chatConfig?.slack?.enabled !== false;
201
+ if (slackEnabled) {
202
+ try {
203
+ if (debug) {
204
+ console.error("[notification] Sending Slack notification via daemon");
205
+ }
206
+ const response = await sendSlackNotification(message);
207
+ if (debug) {
208
+ if (response.success) {
209
+ console.error("[notification] Slack notification sent successfully");
210
+ }
211
+ else {
212
+ console.error(`[notification] Slack notification failed: ${response.error}`);
213
+ }
214
+ }
215
+ }
216
+ catch (err) {
217
+ if (debug) {
218
+ console.error(`[notification] Slack notification error: ${err instanceof Error ? err.message : "unknown"}`);
219
+ }
220
+ }
221
+ }
222
+ // Check if Telegram is configured and enabled
223
+ const telegramEnabled = chatConfig?.telegram?.botToken && chatConfig?.telegram?.enabled !== false;
224
+ if (telegramEnabled) {
225
+ try {
226
+ if (debug) {
227
+ console.error("[notification] Sending Telegram notification via daemon");
228
+ }
229
+ const response = await sendTelegramNotification(message);
230
+ if (debug) {
231
+ if (response.success) {
232
+ console.error("[notification] Telegram notification sent successfully");
233
+ }
234
+ else {
235
+ console.error(`[notification] Telegram notification failed: ${response.error}`);
236
+ }
237
+ }
238
+ }
239
+ catch (err) {
240
+ if (debug) {
241
+ console.error(`[notification] Telegram notification error: ${err instanceof Error ? err.message : "unknown"}`);
242
+ }
243
+ }
244
+ }
245
+ // Check if Discord is configured and enabled
246
+ const discordEnabled = chatConfig?.discord?.botToken && chatConfig?.discord?.enabled !== false;
247
+ if (discordEnabled) {
248
+ try {
249
+ if (debug) {
250
+ console.error("[notification] Sending Discord notification via daemon");
251
+ }
252
+ const response = await sendDiscordNotification(message);
253
+ if (debug) {
254
+ if (response.success) {
255
+ console.error("[notification] Discord notification sent successfully");
256
+ }
257
+ else {
258
+ console.error(`[notification] Discord notification failed: ${response.error}`);
259
+ }
260
+ }
261
+ }
262
+ catch (err) {
263
+ if (debug) {
264
+ console.error(`[notification] Discord notification error: ${err instanceof Error ? err.message : "unknown"}`);
265
+ }
266
+ }
267
+ }
268
+ }
179
269
  /**
180
270
  * Send notification and also trigger any configured daemon events.
181
271
  * This is the recommended function to use for comprehensive notification handling.
182
272
  *
273
+ * When running in a container with chat providers configured (Slack, Telegram, Discord),
274
+ * notifications are automatically sent to those channels.
275
+ *
183
276
  * @param event The notification event type
184
277
  * @param message Optional custom message
185
278
  * @param options Notification options
186
279
  */
187
280
  export async function sendNotificationWithDaemonEvents(event, message, options) {
281
+ // Generate default message based on event type if not provided
282
+ const defaultMessages = {
283
+ prd_complete: "Ralph: PRD Complete! All tasks finished.",
284
+ iteration_complete: "Ralph: Iteration complete.",
285
+ run_stopped: "Ralph: Run stopped.",
286
+ task_complete: "Ralph: Task complete.",
287
+ error: "Ralph: An error occurred.",
288
+ };
289
+ const finalMessage = message ?? defaultMessages[event];
188
290
  // Send the regular notification
189
291
  await sendNotification(event, message, options);
190
292
  // Also trigger daemon events if configured
191
293
  const daemonEvent = mapEventToDaemonEvent(event);
192
294
  if (daemonEvent) {
193
- await triggerDaemonEvents(daemonEvent, options, { taskName: options?.taskName });
295
+ await triggerDaemonEvents(daemonEvent, options, {
296
+ taskName: options?.taskName,
297
+ errorMessage: options?.errorMessage,
298
+ });
194
299
  }
300
+ // Send automatic chat notifications (Slack, Telegram, Discord)
301
+ await sendChatNotifications(finalMessage, options);
195
302
  }
@@ -45,7 +45,7 @@ export function validatePrd(content) {
45
45
  errors.push(`${prefix} missing or invalid 'passes' field (must be boolean)`);
46
46
  }
47
47
  // If no errors for this item, add to valid data
48
- if (errors.filter(e => e.startsWith(prefix)).length === 0) {
48
+ if (errors.filter((e) => e.startsWith(prefix)).length === 0) {
49
49
  data.push({
50
50
  category: entry.category,
51
51
  description: entry.description,
@@ -125,7 +125,16 @@ function extractFromItem(item) {
125
125
  return null;
126
126
  }
127
127
  // Find passes status - check various field names and values
128
- const passesFields = ["passes", "pass", "passed", "done", "complete", "completed", "status", "finished"];
128
+ const passesFields = [
129
+ "passes",
130
+ "pass",
131
+ "passed",
132
+ "done",
133
+ "complete",
134
+ "completed",
135
+ "status",
136
+ "finished",
137
+ ];
129
138
  let passes = false;
130
139
  for (const field of passesFields) {
131
140
  const value = obj[field];
@@ -135,7 +144,13 @@ function extractFromItem(item) {
135
144
  }
136
145
  if (typeof value === "string") {
137
146
  const lower = value.toLowerCase();
138
- if (lower === "true" || lower === "pass" || lower === "passed" || lower === "done" || lower === "complete" || lower === "completed" || lower === "finished") {
147
+ if (lower === "true" ||
148
+ lower === "pass" ||
149
+ lower === "passed" ||
150
+ lower === "done" ||
151
+ lower === "complete" ||
152
+ lower === "completed" ||
153
+ lower === "finished") {
139
154
  passes = true;
140
155
  break;
141
156
  }
@@ -147,12 +162,18 @@ function extractFromItem(item) {
147
162
  * Calculates similarity between two strings using Jaccard index on words.
148
163
  */
149
164
  function similarity(a, b) {
150
- const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 2));
151
- const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 2));
165
+ const wordsA = new Set(a
166
+ .toLowerCase()
167
+ .split(/\s+/)
168
+ .filter((w) => w.length > 2));
169
+ const wordsB = new Set(b
170
+ .toLowerCase()
171
+ .split(/\s+/)
172
+ .filter((w) => w.length > 2));
152
173
  if (wordsA.size === 0 || wordsB.size === 0) {
153
174
  return 0;
154
175
  }
155
- const intersection = new Set([...wordsA].filter(x => wordsB.has(x)));
176
+ const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
156
177
  const union = new Set([...wordsA, ...wordsB]);
157
178
  return intersection.size / union.size;
158
179
  }
@@ -162,7 +183,7 @@ function similarity(a, b) {
162
183
  */
163
184
  export function smartMerge(original, corrupted) {
164
185
  const passingItems = extractPassingItems(corrupted);
165
- const merged = original.map(entry => ({ ...entry })); // Deep copy
186
+ const merged = original.map((entry) => ({ ...entry })); // Deep copy
166
187
  let updated = 0;
167
188
  const warnings = [];
168
189
  for (const item of passingItems) {
@@ -173,7 +194,8 @@ export function smartMerge(original, corrupted) {
173
194
  let bestScore = 0;
174
195
  for (const entry of merged) {
175
196
  // Exact substring match
176
- if (entry.description.includes(item.description) || item.description.includes(entry.description)) {
197
+ if (entry.description.includes(item.description) ||
198
+ item.description.includes(entry.description)) {
177
199
  bestMatch = entry;
178
200
  bestScore = 1;
179
201
  break;
@@ -255,7 +277,7 @@ function attemptArrayRecovery(items) {
255
277
  const stepsFields = ["steps", "verification", "checks", "tasks"];
256
278
  for (const field of stepsFields) {
257
279
  if (Array.isArray(obj[field])) {
258
- const steps = obj[field].filter(s => typeof s === "string");
280
+ const steps = obj[field].filter((s) => typeof s === "string");
259
281
  if (steps.length > 0) {
260
282
  entry.steps = steps;
261
283
  break;
@@ -263,7 +285,16 @@ function attemptArrayRecovery(items) {
263
285
  }
264
286
  }
265
287
  // Passes mapping
266
- const passesFields = ["passes", "pass", "passed", "done", "complete", "completed", "status", "finished"];
288
+ const passesFields = [
289
+ "passes",
290
+ "pass",
291
+ "passed",
292
+ "done",
293
+ "complete",
294
+ "completed",
295
+ "status",
296
+ "finished",
297
+ ];
267
298
  for (const field of passesFields) {
268
299
  const value = obj[field];
269
300
  if (typeof value === "boolean") {
@@ -272,11 +303,21 @@ function attemptArrayRecovery(items) {
272
303
  }
273
304
  if (typeof value === "string") {
274
305
  const lower = value.toLowerCase();
275
- if (lower === "true" || lower === "pass" || lower === "passed" || lower === "done" || lower === "complete" || lower === "completed" || lower === "finished") {
306
+ if (lower === "true" ||
307
+ lower === "pass" ||
308
+ lower === "passed" ||
309
+ lower === "done" ||
310
+ lower === "complete" ||
311
+ lower === "completed" ||
312
+ lower === "finished") {
276
313
  entry.passes = true;
277
314
  break;
278
315
  }
279
- if (lower === "false" || lower === "fail" || lower === "failed" || lower === "pending" || lower === "incomplete") {
316
+ if (lower === "false" ||
317
+ lower === "fail" ||
318
+ lower === "failed" ||
319
+ lower === "pending" ||
320
+ lower === "incomplete") {
280
321
  entry.passes = false;
281
322
  break;
282
323
  }
@@ -320,7 +361,7 @@ export function findLatestBackup(prdPath) {
320
361
  }
321
362
  const files = readdirSync(dir);
322
363
  const backups = files
323
- .filter(f => f.startsWith("backup.prd.") && f.endsWith(".json"))
364
+ .filter((f) => f.startsWith("backup.prd.") && f.endsWith(".json"))
324
365
  .sort()
325
366
  .reverse();
326
367
  if (backups.length === 0) {
@@ -343,10 +384,10 @@ export function createTemplatePrd(backupPath) {
343
384
  description: "Fix the PRD entries",
344
385
  steps: [
345
386
  `Recreate PRD entries based on this corrupted backup content:\n\n@{${absolutePath}}`,
346
- "Write valid entries to .ralph/prd.json with format: category (string), description (string), steps (array of strings), passes (boolean)"
387
+ "Write valid entries to .ralph/prd.json with format: category (string), description (string), steps (array of strings), passes (boolean)",
347
388
  ],
348
389
  passes: false,
349
- }
390
+ },
350
391
  ];
351
392
  }
352
393
  return [
@@ -355,10 +396,10 @@ export function createTemplatePrd(backupPath) {
355
396
  description: "Add PRD entries",
356
397
  steps: [
357
398
  "Add requirements using 'ralph add' or edit .ralph/prd.json directly",
358
- "Verify format: category (string), description (string), steps (array of strings), passes (boolean)"
399
+ "Verify format: category (string), description (string), steps (array of strings), passes (boolean)",
359
400
  ],
360
401
  passes: false,
361
- }
402
+ },
362
403
  ];
363
404
  }
364
405
  /**
@@ -409,9 +450,9 @@ export function expandFileReferences(text, baseDir) {
409
450
  * Returns a new array with expanded content.
410
451
  */
411
452
  export function expandPrdFileReferences(entries, baseDir) {
412
- return entries.map(entry => ({
453
+ return entries.map((entry) => ({
413
454
  ...entry,
414
455
  description: expandFileReferences(entry.description, baseDir),
415
- steps: entry.steps.map(step => expandFileReferences(step, baseDir)),
456
+ steps: entry.steps.map((step) => expandFileReferences(step, baseDir)),
416
457
  }));
417
458
  }
@@ -46,19 +46,23 @@ export async function promptSelectWithArrows(message, options) {
46
46
  process.stdin.setEncoding("utf8");
47
47
  const onKeypress = (key) => {
48
48
  // Handle arrow keys (escape sequences)
49
- if (key === "\x1B[A" || key === "k") { // Up arrow or k
49
+ if (key === "\x1B[A" || key === "k") {
50
+ // Up arrow or k
50
51
  selectedIndex = (selectedIndex - 1 + options.length) % options.length;
51
52
  render();
52
53
  }
53
- else if (key === "\x1B[B" || key === "j") { // Down arrow or j
54
+ else if (key === "\x1B[B" || key === "j") {
55
+ // Down arrow or j
54
56
  selectedIndex = (selectedIndex + 1) % options.length;
55
57
  render();
56
58
  }
57
- else if (key === "\r" || key === "\n" || key === " ") { // Enter or space
59
+ else if (key === "\r" || key === "\n" || key === " ") {
60
+ // Enter or space
58
61
  cleanup();
59
62
  resolve(options[selectedIndex]);
60
63
  }
61
- else if (key === "\x03") { // Ctrl+C
64
+ else if (key === "\x03") {
65
+ // Ctrl+C
62
66
  cleanup();
63
67
  process.exit(0);
64
68
  }
@@ -156,7 +160,8 @@ export async function promptMultiSelect(message, options) {
156
160
  }
157
161
  else {
158
162
  // Text input - treat as custom technology
159
- if (!customTechs.includes(trimmed) && !selected.some(s => s.toLowerCase().includes(trimmed.toLowerCase()))) {
163
+ if (!customTechs.includes(trimmed) &&
164
+ !selected.some((s) => s.toLowerCase().includes(trimmed.toLowerCase()))) {
160
165
  customTechs.push(trimmed);
161
166
  console.log(`Added custom: ${trimmed}`);
162
167
  }
@@ -179,7 +184,7 @@ export async function promptMultiSelectWithArrows(message, options) {
179
184
  allOptions.forEach((opt, i) => {
180
185
  const isLastOption = i === allOptions.length - 1;
181
186
  const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
182
- const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
187
+ const checkbox = isLastOption ? "" : selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]";
183
188
  const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
184
189
  process.stdout.write(`\x1B[2K${cursor} ${checkbox} ${text}\n`);
185
190
  });
@@ -190,7 +195,7 @@ export async function promptMultiSelectWithArrows(message, options) {
190
195
  allOptions.forEach((opt, i) => {
191
196
  const isLastOption = i === allOptions.length - 1;
192
197
  const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
193
- const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
198
+ const checkbox = isLastOption ? "" : selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]";
194
199
  const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
195
200
  console.log(`${cursor} ${checkbox} ${text}`);
196
201
  });
@@ -202,15 +207,18 @@ export async function promptMultiSelectWithArrows(message, options) {
202
207
  process.stdin.resume();
203
208
  process.stdin.setEncoding("utf8");
204
209
  const onKeypress = (key) => {
205
- if (key === "\x1B[A" || key === "k") { // Up
210
+ if (key === "\x1B[A" || key === "k") {
211
+ // Up
206
212
  selectedIndex = (selectedIndex - 1 + allOptions.length) % allOptions.length;
207
213
  render();
208
214
  }
209
- else if (key === "\x1B[B" || key === "j") { // Down
215
+ else if (key === "\x1B[B" || key === "j") {
216
+ // Down
210
217
  selectedIndex = (selectedIndex + 1) % allOptions.length;
211
218
  render();
212
219
  }
213
- else if (key === " ") { // Space - toggle selection
220
+ else if (key === " ") {
221
+ // Space - toggle selection
214
222
  const isLastOption = selectedIndex === allOptions.length - 1;
215
223
  if (!isLastOption) {
216
224
  if (selected.has(selectedIndex)) {
@@ -222,12 +230,14 @@ export async function promptMultiSelectWithArrows(message, options) {
222
230
  render();
223
231
  }
224
232
  }
225
- else if (key === "\r" || key === "\n") { // Enter - confirm
233
+ else if (key === "\r" || key === "\n") {
234
+ // Enter - confirm
226
235
  cleanup();
227
236
  const result = options.filter((_, i) => selected.has(i));
228
237
  resolve(result);
229
238
  }
230
- else if (key === "\x03") { // Ctrl+C
239
+ else if (key === "\x03") {
240
+ // Ctrl+C
231
241
  cleanup();
232
242
  process.exit(0);
233
243
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Logger for responder calls.
3
+ * Logs LLM requests to .ralph/logs/ for debugging and auditing.
4
+ */
5
+ /**
6
+ * Log entry for a responder call.
7
+ */
8
+ export interface ResponderLogEntry {
9
+ timestamp: string;
10
+ responderName?: string;
11
+ responderType?: string;
12
+ trigger?: string;
13
+ gitCommand?: string;
14
+ gitDiffLength?: number;
15
+ filesRead?: string[];
16
+ filesNotFound?: string[];
17
+ filesTotalLength?: number;
18
+ threadContextLength?: number;
19
+ messageLength: number;
20
+ message: string;
21
+ systemPrompt?: string;
22
+ }
23
+ /**
24
+ * Log a responder call to the log file.
25
+ */
26
+ export declare function logResponderCall(entry: ResponderLogEntry): void;
27
+ /**
28
+ * Log a responder call to console (for debug mode).
29
+ */
30
+ export declare function logResponderCallToConsole(entry: ResponderLogEntry): void;
31
+ /**
32
+ * Create a log entry and optionally log to console.
33
+ */
34
+ export declare function createResponderLog(options: {
35
+ responderName?: string;
36
+ responderType?: string;
37
+ trigger?: string;
38
+ gitCommand?: string;
39
+ gitDiffLength?: number;
40
+ filesRead?: string[];
41
+ filesNotFound?: string[];
42
+ filesTotalLength?: number;
43
+ threadContextLength?: number;
44
+ message: string;
45
+ systemPrompt?: string;
46
+ debug?: boolean;
47
+ }): void;