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.
- package/README.md +30 -0
- package/dist/commands/action.js +9 -9
- package/dist/commands/chat.js +13 -12
- package/dist/commands/config.js +2 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/docker.js +102 -66
- package/dist/commands/fix-config.js +2 -1
- package/dist/commands/fix-prd.js +2 -2
- package/dist/commands/init.js +78 -17
- package/dist/commands/listen.js +3 -1
- package/dist/commands/notify.js +1 -1
- package/dist/commands/once.js +17 -9
- package/dist/commands/prd.js +4 -1
- package/dist/commands/run.js +40 -25
- package/dist/commands/slack.js +2 -2
- package/dist/config/responder-presets.json +69 -0
- package/dist/index.js +1 -1
- package/dist/providers/discord.d.ts +28 -0
- package/dist/providers/discord.js +227 -14
- package/dist/providers/slack.d.ts +41 -1
- package/dist/providers/slack.js +389 -8
- package/dist/providers/telegram.d.ts +30 -0
- package/dist/providers/telegram.js +185 -5
- 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 +19 -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 +54 -9
- 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/utils/chat-client.d.ts +4 -0
- package/dist/utils/chat-client.js +12 -5
- package/dist/utils/config.d.ts +84 -0
- package/dist/utils/config.js +78 -1
- 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 +6 -1
- package/dist/utils/notification.js +103 -2
- 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-RESPONDERS.md +785 -0
- package/docs/DEVELOPMENT.md +25 -0
- package/docs/chat-architecture.md +251 -0
- package/package.json +11 -1
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import https from "https";
|
|
6
6
|
import { parseCommand, escapeHtml, } from "../utils/chat-client.js";
|
|
7
|
+
import { ResponderMatcher } from "../utils/responder.js";
|
|
8
|
+
import { loadConfig } from "../utils/config.js";
|
|
9
|
+
import { executeLLMResponder } from "../responders/llm-responder.js";
|
|
10
|
+
import { executeClaudeCodeResponder } from "../responders/claude-code-responder.js";
|
|
11
|
+
import { executeCLIResponder } from "../responders/cli-responder.js";
|
|
7
12
|
export class TelegramChatClient {
|
|
8
13
|
provider = "telegram";
|
|
9
14
|
settings;
|
|
@@ -12,9 +17,134 @@ export class TelegramChatClient {
|
|
|
12
17
|
lastUpdateId = 0;
|
|
13
18
|
pollingTimeout = null;
|
|
14
19
|
debug;
|
|
20
|
+
responderMatcher = null;
|
|
21
|
+
respondersConfig = null;
|
|
22
|
+
botUserId = null;
|
|
23
|
+
botUsername = null;
|
|
15
24
|
constructor(settings, debug = false) {
|
|
16
25
|
this.settings = settings;
|
|
17
26
|
this.debug = debug;
|
|
27
|
+
// Initialize responders from config if available
|
|
28
|
+
this.initializeResponders();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Initialize responder matching from config.
|
|
32
|
+
*/
|
|
33
|
+
initializeResponders() {
|
|
34
|
+
try {
|
|
35
|
+
const config = loadConfig();
|
|
36
|
+
if (config.chat?.responders) {
|
|
37
|
+
this.respondersConfig = config.chat.responders;
|
|
38
|
+
this.responderMatcher = new ResponderMatcher(config.chat.responders);
|
|
39
|
+
if (this.debug) {
|
|
40
|
+
console.log(`[telegram] Initialized ${Object.keys(config.chat.responders).length} responders`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Config not available or responders not configured
|
|
46
|
+
if (this.debug) {
|
|
47
|
+
console.log("[telegram] No responders configured");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Execute a responder and return the result.
|
|
53
|
+
*/
|
|
54
|
+
async executeResponder(match, message) {
|
|
55
|
+
const { responder } = match;
|
|
56
|
+
switch (responder.type) {
|
|
57
|
+
case "llm":
|
|
58
|
+
return executeLLMResponder(message, responder);
|
|
59
|
+
case "claude-code":
|
|
60
|
+
return executeClaudeCodeResponder(message, responder);
|
|
61
|
+
case "cli":
|
|
62
|
+
return executeCLIResponder(message, responder);
|
|
63
|
+
default:
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
response: "",
|
|
67
|
+
error: `Unknown responder type: ${responder.type}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Handle a message that might match a responder.
|
|
73
|
+
* Returns true if a responder was matched and executed.
|
|
74
|
+
*/
|
|
75
|
+
async handleResponderMessage(message, messageId) {
|
|
76
|
+
if (!this.responderMatcher) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const match = this.responderMatcher.matchResponder(message.text);
|
|
80
|
+
if (!match) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (this.debug) {
|
|
84
|
+
console.log(`[telegram] Matched responder: ${match.name} (type: ${match.responder.type})`);
|
|
85
|
+
}
|
|
86
|
+
// Execute the responder
|
|
87
|
+
const result = await this.executeResponder(match, match.args || message.text);
|
|
88
|
+
// Send the response (reply to the original message for context)
|
|
89
|
+
if (result.success) {
|
|
90
|
+
await this.sendMessage(message.chatId, result.response, {
|
|
91
|
+
replyToMessageId: messageId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const errorMsg = result.error
|
|
96
|
+
? `Error: ${result.error}`
|
|
97
|
+
: "An error occurred while processing your message.";
|
|
98
|
+
await this.sendMessage(message.chatId, errorMsg, {
|
|
99
|
+
replyToMessageId: messageId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if the bot is mentioned in a message.
|
|
106
|
+
* Handles both @username mentions and direct replies to the bot.
|
|
107
|
+
*/
|
|
108
|
+
isBotMentioned(update) {
|
|
109
|
+
const msg = update.message;
|
|
110
|
+
if (!msg)
|
|
111
|
+
return false;
|
|
112
|
+
// Check if this is a reply to the bot's message
|
|
113
|
+
if (msg.reply_to_message?.from?.id === this.botUserId) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
// Check for @mention in message entities
|
|
117
|
+
if (msg.entities && this.botUsername) {
|
|
118
|
+
for (const entity of msg.entities) {
|
|
119
|
+
if (entity.type === "mention" && msg.text) {
|
|
120
|
+
const mention = msg.text.substring(entity.offset, entity.offset + entity.length);
|
|
121
|
+
if (mention.toLowerCase() === `@${this.botUsername.toLowerCase()}`) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (entity.type === "text_mention" && entity.user?.id === this.botUserId) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Remove bot mention from message text.
|
|
134
|
+
*/
|
|
135
|
+
removeBotMention(text) {
|
|
136
|
+
if (!this.botUsername) {
|
|
137
|
+
return text;
|
|
138
|
+
}
|
|
139
|
+
// Remove @username and any surrounding whitespace
|
|
140
|
+
const regex = new RegExp(`@${this.botUsername}\\s*`, "gi");
|
|
141
|
+
return text.replace(regex, "").trim();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if a chat is a group chat (group or supergroup).
|
|
145
|
+
*/
|
|
146
|
+
isGroupChat(chatType) {
|
|
147
|
+
return chatType === "group" || chatType === "supergroup";
|
|
18
148
|
}
|
|
19
149
|
/**
|
|
20
150
|
* Make a request to the Telegram Bot API.
|
|
@@ -128,6 +258,8 @@ export class TelegramChatClient {
|
|
|
128
258
|
}
|
|
129
259
|
if (update.message?.text) {
|
|
130
260
|
const chatId = String(update.message.chat.id);
|
|
261
|
+
const messageId = update.message.message_id;
|
|
262
|
+
const chatType = update.message.chat.type;
|
|
131
263
|
// Check if chat is allowed
|
|
132
264
|
if (!this.isChatAllowed(chatId)) {
|
|
133
265
|
if (this.debug) {
|
|
@@ -135,12 +267,22 @@ export class TelegramChatClient {
|
|
|
135
267
|
}
|
|
136
268
|
continue;
|
|
137
269
|
}
|
|
270
|
+
// In group chats, only process messages that mention the bot or are replies to the bot
|
|
271
|
+
const isGroup = this.isGroupChat(chatType);
|
|
272
|
+
const isMention = this.isBotMentioned(update);
|
|
273
|
+
// Clean the message text (remove bot mention if present)
|
|
274
|
+
let messageText = update.message.text;
|
|
275
|
+
if (isMention) {
|
|
276
|
+
messageText = this.removeBotMention(messageText);
|
|
277
|
+
}
|
|
138
278
|
const message = {
|
|
139
|
-
text:
|
|
279
|
+
text: messageText,
|
|
140
280
|
chatId,
|
|
141
281
|
senderId: update.message.from ? String(update.message.from.id) : undefined,
|
|
142
282
|
senderName: update.message.from
|
|
143
|
-
? [update.message.from.first_name, update.message.from.last_name]
|
|
283
|
+
? [update.message.from.first_name, update.message.from.last_name]
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.join(" ")
|
|
144
286
|
: undefined,
|
|
145
287
|
timestamp: new Date(update.message.date * 1000),
|
|
146
288
|
raw: update,
|
|
@@ -156,7 +298,7 @@ export class TelegramChatClient {
|
|
|
156
298
|
}
|
|
157
299
|
}
|
|
158
300
|
}
|
|
159
|
-
// Try to parse as a command
|
|
301
|
+
// Try to parse as a command first
|
|
160
302
|
const command = parseCommand(message.text, message);
|
|
161
303
|
if (command) {
|
|
162
304
|
try {
|
|
@@ -167,7 +309,39 @@ export class TelegramChatClient {
|
|
|
167
309
|
console.error(`[telegram] Command handler error: ${err}`);
|
|
168
310
|
}
|
|
169
311
|
// Send error message to chat
|
|
170
|
-
await this.sendMessage(chatId, `Error executing command: ${err instanceof Error ? err.message : "Unknown error"}
|
|
312
|
+
await this.sendMessage(chatId, `Error executing command: ${err instanceof Error ? err.message : "Unknown error"}`, {
|
|
313
|
+
replyToMessageId: messageId,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
continue; // Command handled, don't process as responder message
|
|
317
|
+
}
|
|
318
|
+
// For non-command messages, route through responders
|
|
319
|
+
// In group chats: only respond if bot is mentioned or message is a reply to bot
|
|
320
|
+
// In private chats: always respond if responders are configured
|
|
321
|
+
const shouldProcessAsResponder = !isGroup || isMention;
|
|
322
|
+
if (shouldProcessAsResponder && this.responderMatcher) {
|
|
323
|
+
// Check if there's a matching responder or a default responder
|
|
324
|
+
const hasDefaultResponder = this.responderMatcher.hasDefaultResponder();
|
|
325
|
+
const match = this.responderMatcher.matchResponder(message.text);
|
|
326
|
+
if (match) {
|
|
327
|
+
try {
|
|
328
|
+
const handled = await this.handleResponderMessage(message, messageId);
|
|
329
|
+
if (handled) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (this.debug) {
|
|
335
|
+
console.error(`[telegram] Responder error: ${err}`);
|
|
336
|
+
}
|
|
337
|
+
await this.sendMessage(chatId, `Error processing message: ${err instanceof Error ? err.message : "Unknown error"}`, { replyToMessageId: messageId });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (isMention && !hasDefaultResponder) {
|
|
342
|
+
// Bot was mentioned but no responder matched and no default responder
|
|
343
|
+
// Send a helpful message
|
|
344
|
+
await this.sendMessage(chatId, "I received your message, but no responders are configured. Use /help for available commands.", { replyToMessageId: messageId });
|
|
171
345
|
}
|
|
172
346
|
}
|
|
173
347
|
}
|
|
@@ -189,9 +363,11 @@ export class TelegramChatClient {
|
|
|
189
363
|
if (this.connected) {
|
|
190
364
|
throw new Error("Already connected");
|
|
191
365
|
}
|
|
192
|
-
// Verify bot token by calling getMe
|
|
366
|
+
// Verify bot token by calling getMe and store bot info
|
|
193
367
|
try {
|
|
194
368
|
const me = await this.apiRequest("getMe");
|
|
369
|
+
this.botUserId = me.id;
|
|
370
|
+
this.botUsername = me.username || null;
|
|
195
371
|
if (this.debug) {
|
|
196
372
|
console.log(`[telegram] Connected as @${me.username || me.first_name} (ID: ${me.id})`);
|
|
197
373
|
}
|
|
@@ -216,6 +392,10 @@ export class TelegramChatClient {
|
|
|
216
392
|
text: escapedText,
|
|
217
393
|
parse_mode: "HTML",
|
|
218
394
|
};
|
|
395
|
+
// Add reply_to_message_id for context in group chats
|
|
396
|
+
if (options?.replyToMessageId) {
|
|
397
|
+
body.reply_to_message_id = options.replyToMessageId;
|
|
398
|
+
}
|
|
219
399
|
// Convert generic InlineButton format to Telegram's InlineKeyboardMarkup
|
|
220
400
|
if (options?.inlineKeyboard && options.inlineKeyboard.length > 0) {
|
|
221
401
|
const inlineKeyboard = options.inlineKeyboard.map((row) => row.map((button) => ({
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Responder - Spawns Claude Code CLI with user prompts.
|
|
3
|
+
* Executes Claude Code in --dangerously-skip-permissions mode to run autonomously.
|
|
4
|
+
*/
|
|
5
|
+
import { ResponderConfig } from "../utils/config.js";
|
|
6
|
+
import { ResponderResult } from "./llm-responder.js";
|
|
7
|
+
/**
|
|
8
|
+
* Options for executing a Claude Code responder.
|
|
9
|
+
*/
|
|
10
|
+
export interface ClaudeCodeResponderOptions {
|
|
11
|
+
/** Callback for progress updates during execution */
|
|
12
|
+
onProgress?: (output: string) => void;
|
|
13
|
+
/** Override default timeout in milliseconds */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Maximum response length in characters */
|
|
16
|
+
maxLength?: number;
|
|
17
|
+
/** Working directory for Claude Code execution */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Executes Claude Code with the given prompt.
|
|
22
|
+
*
|
|
23
|
+
* Spawns the claude CLI with:
|
|
24
|
+
* - -p flag for non-interactive prompt mode
|
|
25
|
+
* - --dangerously-skip-permissions to skip all permission prompts
|
|
26
|
+
* - --print to get clean output
|
|
27
|
+
*
|
|
28
|
+
* @param prompt The user prompt to send to Claude Code
|
|
29
|
+
* @param responderConfig The responder configuration
|
|
30
|
+
* @param options Optional execution options
|
|
31
|
+
* @returns The responder result with response or error
|
|
32
|
+
*/
|
|
33
|
+
export declare function executeClaudeCodeResponder(prompt: string, responderConfig: ResponderConfig, options?: ClaudeCodeResponderOptions): Promise<ResponderResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a reusable Claude Code responder function.
|
|
36
|
+
* This is useful for handling multiple messages with the same configuration.
|
|
37
|
+
*
|
|
38
|
+
* @param responderConfig The responder configuration
|
|
39
|
+
* @returns A function that executes the responder with a prompt
|
|
40
|
+
*/
|
|
41
|
+
export declare function createClaudeCodeResponder(responderConfig: ResponderConfig): (prompt: string, options?: ClaudeCodeResponderOptions) => Promise<ResponderResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Validates that a responder configuration is valid for Claude Code execution.
|
|
44
|
+
*
|
|
45
|
+
* @param responderConfig The responder configuration to validate
|
|
46
|
+
* @returns An error message if invalid, or null if valid
|
|
47
|
+
*/
|
|
48
|
+
export declare function validateClaudeCodeResponder(responderConfig: ResponderConfig): string | null;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Responder - Spawns Claude Code CLI with user prompts.
|
|
3
|
+
* Executes Claude Code in --dangerously-skip-permissions mode to run autonomously.
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { truncateResponse } from "./llm-responder.js";
|
|
7
|
+
/**
|
|
8
|
+
* Default timeout for Claude Code execution (5 minutes).
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TIMEOUT = 300000;
|
|
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
|
+
* Executes Claude Code with the given prompt.
|
|
21
|
+
*
|
|
22
|
+
* Spawns the claude CLI with:
|
|
23
|
+
* - -p flag for non-interactive prompt mode
|
|
24
|
+
* - --dangerously-skip-permissions to skip all permission prompts
|
|
25
|
+
* - --print to get clean output
|
|
26
|
+
*
|
|
27
|
+
* @param prompt The user prompt to send to Claude Code
|
|
28
|
+
* @param responderConfig The responder configuration
|
|
29
|
+
* @param options Optional execution options
|
|
30
|
+
* @returns The responder result with response or error
|
|
31
|
+
*/
|
|
32
|
+
export async function executeClaudeCodeResponder(prompt, responderConfig, options) {
|
|
33
|
+
const timeout = options?.timeout ?? responderConfig.timeout ?? DEFAULT_TIMEOUT;
|
|
34
|
+
const maxLength = options?.maxLength ?? responderConfig.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
35
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
36
|
+
const onProgress = options?.onProgress;
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
let stdout = "";
|
|
39
|
+
let stderr = "";
|
|
40
|
+
let killed = false;
|
|
41
|
+
let lastProgressSent = 0;
|
|
42
|
+
let progressTimer = null;
|
|
43
|
+
// Build the command arguments
|
|
44
|
+
const args = ["-p", prompt, "--dangerously-skip-permissions", "--print"];
|
|
45
|
+
// Spawn claude process
|
|
46
|
+
let proc;
|
|
47
|
+
try {
|
|
48
|
+
proc = spawn("claude", args, {
|
|
49
|
+
cwd,
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
+
env: { ...process.env },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
56
|
+
resolve({
|
|
57
|
+
success: false,
|
|
58
|
+
response: "",
|
|
59
|
+
error: `Failed to spawn claude: ${error}`,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Handle timeout
|
|
64
|
+
const timeoutTimer = setTimeout(() => {
|
|
65
|
+
killed = true;
|
|
66
|
+
proc.kill("SIGTERM");
|
|
67
|
+
// Give it a moment to terminate gracefully, then force kill
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
try {
|
|
70
|
+
proc.kill("SIGKILL");
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Already dead
|
|
74
|
+
}
|
|
75
|
+
}, 2000);
|
|
76
|
+
if (progressTimer) {
|
|
77
|
+
clearInterval(progressTimer);
|
|
78
|
+
}
|
|
79
|
+
resolve({
|
|
80
|
+
success: false,
|
|
81
|
+
response: stdout,
|
|
82
|
+
error: `Claude Code timed out after ${Math.round(timeout / 1000)} seconds`,
|
|
83
|
+
});
|
|
84
|
+
}, timeout);
|
|
85
|
+
// Set up progress updates
|
|
86
|
+
if (onProgress) {
|
|
87
|
+
progressTimer = setInterval(() => {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
if (now - lastProgressSent >= PROGRESS_INTERVAL && stdout.length > 0) {
|
|
90
|
+
// Send a progress indicator
|
|
91
|
+
const lines = stdout.split("\n");
|
|
92
|
+
const lastLine = lines[lines.length - 1] || lines[lines.length - 2] || "";
|
|
93
|
+
const truncatedLine = lastLine.length > 100 ? lastLine.substring(0, 100) + "..." : lastLine;
|
|
94
|
+
onProgress(`⏳ Working... ${truncatedLine}`);
|
|
95
|
+
lastProgressSent = now;
|
|
96
|
+
}
|
|
97
|
+
}, PROGRESS_INTERVAL);
|
|
98
|
+
}
|
|
99
|
+
// Capture stdout
|
|
100
|
+
proc.stdout?.on("data", (data) => {
|
|
101
|
+
stdout += data.toString();
|
|
102
|
+
});
|
|
103
|
+
// Capture stderr
|
|
104
|
+
proc.stderr?.on("data", (data) => {
|
|
105
|
+
stderr += data.toString();
|
|
106
|
+
});
|
|
107
|
+
// Handle process completion
|
|
108
|
+
proc.on("close", (code) => {
|
|
109
|
+
if (killed)
|
|
110
|
+
return;
|
|
111
|
+
clearTimeout(timeoutTimer);
|
|
112
|
+
if (progressTimer) {
|
|
113
|
+
clearInterval(progressTimer);
|
|
114
|
+
}
|
|
115
|
+
if (code === 0 || code === null) {
|
|
116
|
+
// Success - format and truncate output
|
|
117
|
+
const output = formatClaudeCodeOutput(stdout);
|
|
118
|
+
const { text, truncated, originalLength } = truncateResponse(output, maxLength);
|
|
119
|
+
resolve({
|
|
120
|
+
success: true,
|
|
121
|
+
response: text,
|
|
122
|
+
truncated,
|
|
123
|
+
originalLength: truncated ? originalLength : undefined,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Failure
|
|
128
|
+
const errorMsg = stderr.trim() || `Claude Code exited with code ${code}`;
|
|
129
|
+
resolve({
|
|
130
|
+
success: false,
|
|
131
|
+
response: stdout,
|
|
132
|
+
error: errorMsg,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Handle spawn errors
|
|
137
|
+
proc.on("error", (err) => {
|
|
138
|
+
if (killed)
|
|
139
|
+
return;
|
|
140
|
+
clearTimeout(timeoutTimer);
|
|
141
|
+
if (progressTimer) {
|
|
142
|
+
clearInterval(progressTimer);
|
|
143
|
+
}
|
|
144
|
+
resolve({
|
|
145
|
+
success: false,
|
|
146
|
+
response: "",
|
|
147
|
+
error: `Claude Code error: ${err.message}`,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Formats Claude Code output for chat display.
|
|
154
|
+
* Cleans up ANSI codes, excessive whitespace, and formats for readability.
|
|
155
|
+
*/
|
|
156
|
+
function formatClaudeCodeOutput(output) {
|
|
157
|
+
// Remove ANSI escape codes
|
|
158
|
+
let cleaned = output.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
159
|
+
// Remove carriage returns (used for progress overwriting)
|
|
160
|
+
cleaned = cleaned.replace(/\r/g, "");
|
|
161
|
+
// Collapse multiple blank lines into one
|
|
162
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
|
|
163
|
+
// Trim leading/trailing whitespace
|
|
164
|
+
cleaned = cleaned.trim();
|
|
165
|
+
// If output is empty, provide a default message
|
|
166
|
+
if (!cleaned) {
|
|
167
|
+
return "(Claude Code completed with no output)";
|
|
168
|
+
}
|
|
169
|
+
return cleaned;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Creates a reusable Claude Code responder function.
|
|
173
|
+
* This is useful for handling multiple messages with the same configuration.
|
|
174
|
+
*
|
|
175
|
+
* @param responderConfig The responder configuration
|
|
176
|
+
* @returns A function that executes the responder with a prompt
|
|
177
|
+
*/
|
|
178
|
+
export function createClaudeCodeResponder(responderConfig) {
|
|
179
|
+
return async (prompt, options) => {
|
|
180
|
+
return executeClaudeCodeResponder(prompt, responderConfig, options);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Validates that a responder configuration is valid for Claude Code execution.
|
|
185
|
+
*
|
|
186
|
+
* @param responderConfig The responder configuration to validate
|
|
187
|
+
* @returns An error message if invalid, or null if valid
|
|
188
|
+
*/
|
|
189
|
+
export function validateClaudeCodeResponder(responderConfig) {
|
|
190
|
+
if (responderConfig.type !== "claude-code") {
|
|
191
|
+
return `Responder type is "${responderConfig.type}", expected "claude-code"`;
|
|
192
|
+
}
|
|
193
|
+
// Check that timeout is reasonable if specified
|
|
194
|
+
if (responderConfig.timeout !== undefined) {
|
|
195
|
+
if (responderConfig.timeout < 1000) {
|
|
196
|
+
return `Timeout ${responderConfig.timeout}ms is too short (minimum: 1000ms)`;
|
|
197
|
+
}
|
|
198
|
+
if (responderConfig.timeout > 600000) {
|
|
199
|
+
return `Timeout ${responderConfig.timeout}ms is too long (maximum: 600000ms / 10 minutes)`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { ResponderConfig } from "../utils/config.js";
|
|
6
|
+
import { ResponderResult } from "./llm-responder.js";
|
|
7
|
+
/**
|
|
8
|
+
* Options for executing a CLI responder.
|
|
9
|
+
*/
|
|
10
|
+
export interface CLIResponderOptions {
|
|
11
|
+
/** Callback for progress updates during execution */
|
|
12
|
+
onProgress?: (output: string) => void;
|
|
13
|
+
/** Override default timeout in milliseconds */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
/** Maximum response length in characters */
|
|
16
|
+
maxLength?: number;
|
|
17
|
+
/** Working directory for command execution */
|
|
18
|
+
cwd?: string;
|
|
19
|
+
/** Additional environment variables */
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Replaces {{message}} placeholder in command string with the actual message.
|
|
24
|
+
* Escapes the message to prevent shell injection.
|
|
25
|
+
*/
|
|
26
|
+
export declare function replaceMessagePlaceholder(command: string, message: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Parses a command string into command and arguments.
|
|
29
|
+
* Handles quoted strings and basic shell syntax.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseCommand(commandString: string): {
|
|
32
|
+
command: string;
|
|
33
|
+
args: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Executes a CLI command with the given message.
|
|
37
|
+
*
|
|
38
|
+
* The command string can include {{message}} placeholder which will be replaced
|
|
39
|
+
* with the user's message. If no placeholder is present, the message is appended
|
|
40
|
+
* as an argument.
|
|
41
|
+
*
|
|
42
|
+
* @param message The user message to include in the command
|
|
43
|
+
* @param responderConfig The responder configuration
|
|
44
|
+
* @param options Optional execution options
|
|
45
|
+
* @returns The responder result with response or error
|
|
46
|
+
*/
|
|
47
|
+
export declare function executeCLIResponder(message: string, responderConfig: ResponderConfig, options?: CLIResponderOptions): Promise<ResponderResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Creates a reusable CLI responder function.
|
|
50
|
+
* This is useful for handling multiple messages with the same configuration.
|
|
51
|
+
*
|
|
52
|
+
* @param responderConfig The responder configuration
|
|
53
|
+
* @returns A function that executes the responder with a message
|
|
54
|
+
*/
|
|
55
|
+
export declare function createCLIResponder(responderConfig: ResponderConfig): (message: string, options?: CLIResponderOptions) => Promise<ResponderResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Validates that a responder configuration is valid for CLI execution.
|
|
58
|
+
*
|
|
59
|
+
* @param responderConfig The responder configuration to validate
|
|
60
|
+
* @returns An error message if invalid, or null if valid
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateCLIResponder(responderConfig: ResponderConfig): string | null;
|