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.
- package/README.md +30 -0
- package/dist/commands/action.js +47 -20
- package/dist/commands/chat.d.ts +1 -1
- package/dist/commands/chat.js +325 -62
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.d.ts +2 -5
- package/dist/commands/daemon.js +118 -49
- package/dist/commands/docker.js +110 -73
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/help.js +19 -3
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +116 -5
- package/dist/commands/logo.d.ts +5 -0
- package/dist/commands/logo.js +41 -0
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +19 -9
- package/dist/commands/prd.js +20 -2
- package/dist/commands/run.js +111 -27
- package/dist/commands/slack.d.ts +10 -0
- package/dist/commands/slack.js +333 -0
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +6 -1
- package/dist/providers/discord.d.ts +82 -0
- package/dist/providers/discord.js +697 -0
- package/dist/providers/slack.d.ts +79 -0
- package/dist/providers/slack.js +715 -0
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +190 -7
- package/dist/responders/claude-code-responder.d.ts +48 -0
- package/dist/responders/claude-code-responder.js +203 -0
- package/dist/responders/cli-responder.d.ts +62 -0
- package/dist/responders/cli-responder.js +298 -0
- package/dist/responders/llm-responder.d.ts +135 -0
- package/dist/responders/llm-responder.js +582 -0
- package/dist/templates/macos-scripts.js +2 -4
- package/dist/templates/prompts.js +4 -2
- package/dist/tui/ConfigEditor.js +42 -5
- package/dist/tui/components/ArrayEditor.js +1 -1
- package/dist/tui/components/EditorPanel.js +10 -6
- package/dist/tui/components/HelpPanel.d.ts +1 -1
- package/dist/tui/components/HelpPanel.js +1 -1
- package/dist/tui/components/JsonSnippetEditor.js +8 -5
- package/dist/tui/components/KeyValueEditor.js +69 -5
- package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
- package/dist/tui/components/LLMProvidersEditor.js +357 -0
- package/dist/tui/components/ObjectEditor.js +1 -1
- package/dist/tui/components/Preview.js +1 -1
- package/dist/tui/components/RespondersEditor.d.ts +22 -0
- package/dist/tui/components/RespondersEditor.js +437 -0
- package/dist/tui/components/SectionNav.js +27 -3
- package/dist/tui/utils/presets.js +15 -2
- package/dist/utils/chat-client.d.ts +33 -4
- package/dist/utils/chat-client.js +20 -1
- package/dist/utils/config.d.ts +100 -1
- package/dist/utils/config.js +78 -1
- package/dist/utils/daemon-actions.d.ts +19 -0
- package/dist/utils/daemon-actions.js +111 -0
- package/dist/utils/daemon-client.d.ts +21 -0
- package/dist/utils/daemon-client.js +28 -1
- package/dist/utils/llm-client.d.ts +82 -0
- package/dist/utils/llm-client.js +185 -0
- package/dist/utils/message-queue.js +6 -6
- package/dist/utils/notification.d.ts +10 -2
- package/dist/utils/notification.js +111 -4
- package/dist/utils/prd-validator.js +60 -19
- package/dist/utils/prompt.js +22 -12
- package/dist/utils/responder-logger.d.ts +47 -0
- package/dist/utils/responder-logger.js +129 -0
- package/dist/utils/responder-presets.d.ts +92 -0
- package/dist/utils/responder-presets.js +156 -0
- package/dist/utils/responder.d.ts +88 -0
- package/dist/utils/responder.js +207 -0
- package/dist/utils/stream-json.js +6 -6
- package/docs/CHAT-CLIENTS.md +520 -0
- package/docs/CHAT-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/USEFUL_ACTIONS.md +815 -0
- package/docs/chat-architecture.md +251 -0
- 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, {
|
|
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 = [
|
|
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" ||
|
|
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
|
|
151
|
-
|
|
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) ||
|
|
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 = [
|
|
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" ||
|
|
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" ||
|
|
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
|
}
|
package/dist/utils/prompt.js
CHANGED
|
@@ -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") {
|
|
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") {
|
|
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 === " ") {
|
|
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") {
|
|
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) &&
|
|
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 ? "" :
|
|
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 ? "" :
|
|
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") {
|
|
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") {
|
|
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 === " ") {
|
|
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") {
|
|
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") {
|
|
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;
|