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
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
/**
|
|
6
|
+
* Responder type options for dropdown.
|
|
7
|
+
*/
|
|
8
|
+
const RESPONDER_TYPES = ["llm", "claude-code", "cli"];
|
|
9
|
+
/**
|
|
10
|
+
* Type descriptions for display.
|
|
11
|
+
*/
|
|
12
|
+
const TYPE_DESCRIPTIONS = {
|
|
13
|
+
llm: "LLM provider",
|
|
14
|
+
"claude-code": "Claude Code agent",
|
|
15
|
+
cli: "CLI command",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Default timeouts by responder type.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_TIMEOUTS = {
|
|
21
|
+
llm: 60000,
|
|
22
|
+
"claude-code": 300000,
|
|
23
|
+
cli: 60000,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Suggested responder names/presets.
|
|
27
|
+
*/
|
|
28
|
+
const SUGGESTED_NAMES = ["default", "qa", "reviewer", "code", "lint"];
|
|
29
|
+
/**
|
|
30
|
+
* RespondersEditor component for editing chat responder configurations.
|
|
31
|
+
* Provides a user-friendly interface for adding, editing, and removing responders.
|
|
32
|
+
*/
|
|
33
|
+
export function RespondersEditor({ label, responders, onConfirm, onCancel, isFocused = true, maxHeight = 15, }) {
|
|
34
|
+
const [editResponders, setEditResponders] = useState({ ...responders });
|
|
35
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
36
|
+
const [mode, setMode] = useState("list");
|
|
37
|
+
const [editText, setEditText] = useState("");
|
|
38
|
+
const [editingResponder, setEditingResponder] = useState(null);
|
|
39
|
+
const [typeIndex, setTypeIndex] = useState(0);
|
|
40
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
41
|
+
// Get sorted responder names
|
|
42
|
+
const responderNames = useMemo(() => Object.keys(editResponders).sort(), [editResponders]);
|
|
43
|
+
// Total options includes all responders plus "+ Add responder" option
|
|
44
|
+
const totalOptions = responderNames.length + 1;
|
|
45
|
+
// Calculate visible range for scrolling
|
|
46
|
+
const visibleCount = Math.min(maxHeight - 6, totalOptions); // Reserve lines for header, footer, hints
|
|
47
|
+
const visibleResponders = useMemo(() => {
|
|
48
|
+
const endIndex = Math.min(scrollOffset + visibleCount, responderNames.length);
|
|
49
|
+
return responderNames.slice(scrollOffset, endIndex);
|
|
50
|
+
}, [scrollOffset, visibleCount, responderNames]);
|
|
51
|
+
// Auto-scroll to keep highlighted item visible
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
if (highlightedIndex < scrollOffset) {
|
|
54
|
+
setScrollOffset(highlightedIndex);
|
|
55
|
+
}
|
|
56
|
+
else if (highlightedIndex >= scrollOffset + visibleCount) {
|
|
57
|
+
setScrollOffset(Math.max(0, highlightedIndex - visibleCount + 1));
|
|
58
|
+
}
|
|
59
|
+
}, [highlightedIndex, scrollOffset, visibleCount]);
|
|
60
|
+
// Navigation handlers
|
|
61
|
+
const handleNavigateUp = useCallback(() => {
|
|
62
|
+
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : totalOptions - 1));
|
|
63
|
+
}, [totalOptions]);
|
|
64
|
+
const handleNavigateDown = useCallback(() => {
|
|
65
|
+
setHighlightedIndex((prev) => (prev < totalOptions - 1 ? prev + 1 : 0));
|
|
66
|
+
}, [totalOptions]);
|
|
67
|
+
// Delete the highlighted responder
|
|
68
|
+
const handleDelete = useCallback(() => {
|
|
69
|
+
if (highlightedIndex < responderNames.length) {
|
|
70
|
+
const nameToDelete = responderNames[highlightedIndex];
|
|
71
|
+
const newResponders = { ...editResponders };
|
|
72
|
+
delete newResponders[nameToDelete];
|
|
73
|
+
setEditResponders(newResponders);
|
|
74
|
+
// Adjust highlighted index if needed
|
|
75
|
+
const newNames = Object.keys(newResponders);
|
|
76
|
+
if (highlightedIndex >= newNames.length && newNames.length > 0) {
|
|
77
|
+
setHighlightedIndex(newNames.length - 1);
|
|
78
|
+
}
|
|
79
|
+
else if (newNames.length === 0) {
|
|
80
|
+
setHighlightedIndex(0);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [highlightedIndex, responderNames, editResponders]);
|
|
84
|
+
// Start editing or adding a responder
|
|
85
|
+
const handleStartEdit = useCallback(() => {
|
|
86
|
+
if (highlightedIndex < responderNames.length) {
|
|
87
|
+
// Edit existing responder
|
|
88
|
+
const name = responderNames[highlightedIndex];
|
|
89
|
+
const config = editResponders[name];
|
|
90
|
+
setEditingResponder({ name, config: { ...config } });
|
|
91
|
+
setMode("edit-responder");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Add new responder - start with name input
|
|
95
|
+
setEditText("");
|
|
96
|
+
setMode("add-name");
|
|
97
|
+
}
|
|
98
|
+
}, [highlightedIndex, responderNames, editResponders]);
|
|
99
|
+
// Handle name submission when adding new responder
|
|
100
|
+
const handleNameSubmit = useCallback(() => {
|
|
101
|
+
const trimmedName = editText.trim();
|
|
102
|
+
if (trimmedName) {
|
|
103
|
+
// Check if name already exists
|
|
104
|
+
if (editResponders[trimmedName]) {
|
|
105
|
+
// Edit existing instead
|
|
106
|
+
const config = editResponders[trimmedName];
|
|
107
|
+
setEditingResponder({ name: trimmedName, config: { ...config } });
|
|
108
|
+
setMode("edit-responder");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Create new responder with default values
|
|
112
|
+
const defaultTrigger = trimmedName === "default" ? undefined : `@${trimmedName}`;
|
|
113
|
+
setEditingResponder({
|
|
114
|
+
name: trimmedName,
|
|
115
|
+
config: {
|
|
116
|
+
type: "llm",
|
|
117
|
+
trigger: defaultTrigger,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
setTypeIndex(0);
|
|
121
|
+
setMode("select-type");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
setMode("list");
|
|
126
|
+
}
|
|
127
|
+
setEditText("");
|
|
128
|
+
}, [editText, editResponders]);
|
|
129
|
+
// Handle type selection
|
|
130
|
+
const handleTypeSelect = useCallback(() => {
|
|
131
|
+
if (editingResponder) {
|
|
132
|
+
const selectedType = RESPONDER_TYPES[typeIndex];
|
|
133
|
+
setEditingResponder({
|
|
134
|
+
...editingResponder,
|
|
135
|
+
config: {
|
|
136
|
+
...editingResponder.config,
|
|
137
|
+
type: selectedType,
|
|
138
|
+
timeout: DEFAULT_TIMEOUTS[selectedType],
|
|
139
|
+
maxLength: 2000,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
setMode("edit-responder");
|
|
143
|
+
}
|
|
144
|
+
}, [editingResponder, typeIndex]);
|
|
145
|
+
// Save current editing responder
|
|
146
|
+
const saveEditingResponder = useCallback(() => {
|
|
147
|
+
if (editingResponder) {
|
|
148
|
+
const newResponders = {
|
|
149
|
+
...editResponders,
|
|
150
|
+
[editingResponder.name]: { ...editingResponder.config },
|
|
151
|
+
};
|
|
152
|
+
// Clean up undefined values
|
|
153
|
+
const config = newResponders[editingResponder.name];
|
|
154
|
+
if (!config.trigger)
|
|
155
|
+
delete config.trigger;
|
|
156
|
+
if (!config.provider)
|
|
157
|
+
delete config.provider;
|
|
158
|
+
if (!config.systemPrompt)
|
|
159
|
+
delete config.systemPrompt;
|
|
160
|
+
if (!config.command)
|
|
161
|
+
delete config.command;
|
|
162
|
+
if (!config.timeout)
|
|
163
|
+
delete config.timeout;
|
|
164
|
+
if (!config.maxLength)
|
|
165
|
+
delete config.maxLength;
|
|
166
|
+
setEditResponders(newResponders);
|
|
167
|
+
setEditingResponder(null);
|
|
168
|
+
setMode("list");
|
|
169
|
+
setEditText("");
|
|
170
|
+
// Update highlighted index to the new/edited responder
|
|
171
|
+
const sortedNames = Object.keys(newResponders).sort();
|
|
172
|
+
const newIndex = sortedNames.indexOf(editingResponder.name);
|
|
173
|
+
if (newIndex >= 0) {
|
|
174
|
+
setHighlightedIndex(newIndex);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}, [editingResponder, editResponders]);
|
|
178
|
+
// Cancel editing
|
|
179
|
+
const handleCancel = useCallback(() => {
|
|
180
|
+
setMode("list");
|
|
181
|
+
setEditText("");
|
|
182
|
+
setEditingResponder(null);
|
|
183
|
+
}, []);
|
|
184
|
+
// Handle text field submission
|
|
185
|
+
const handleTextSubmit = useCallback(() => {
|
|
186
|
+
if (!editingResponder)
|
|
187
|
+
return;
|
|
188
|
+
const trimmedValue = editText.trim();
|
|
189
|
+
switch (mode) {
|
|
190
|
+
case "edit-trigger":
|
|
191
|
+
setEditingResponder({
|
|
192
|
+
...editingResponder,
|
|
193
|
+
config: {
|
|
194
|
+
...editingResponder.config,
|
|
195
|
+
trigger: trimmedValue || undefined,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
199
|
+
case "edit-provider":
|
|
200
|
+
setEditingResponder({
|
|
201
|
+
...editingResponder,
|
|
202
|
+
config: {
|
|
203
|
+
...editingResponder.config,
|
|
204
|
+
provider: trimmedValue || undefined,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
case "edit-system":
|
|
209
|
+
setEditingResponder({
|
|
210
|
+
...editingResponder,
|
|
211
|
+
config: {
|
|
212
|
+
...editingResponder.config,
|
|
213
|
+
systemPrompt: trimmedValue || undefined,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
break;
|
|
217
|
+
case "edit-command":
|
|
218
|
+
setEditingResponder({
|
|
219
|
+
...editingResponder,
|
|
220
|
+
config: {
|
|
221
|
+
...editingResponder.config,
|
|
222
|
+
command: trimmedValue || undefined,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
case "edit-timeout":
|
|
227
|
+
setEditingResponder({
|
|
228
|
+
...editingResponder,
|
|
229
|
+
config: {
|
|
230
|
+
...editingResponder.config,
|
|
231
|
+
timeout: trimmedValue ? parseInt(trimmedValue, 10) || undefined : undefined,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
case "edit-maxlength":
|
|
236
|
+
setEditingResponder({
|
|
237
|
+
...editingResponder,
|
|
238
|
+
config: {
|
|
239
|
+
...editingResponder.config,
|
|
240
|
+
maxLength: trimmedValue ? parseInt(trimmedValue, 10) || undefined : undefined,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
setMode("edit-responder");
|
|
246
|
+
setEditText("");
|
|
247
|
+
}, [editingResponder, editText, mode]);
|
|
248
|
+
// Handle keyboard input for list mode
|
|
249
|
+
useInput((input, key) => {
|
|
250
|
+
if (!isFocused || mode !== "list")
|
|
251
|
+
return;
|
|
252
|
+
if (input === "j" || key.downArrow) {
|
|
253
|
+
handleNavigateDown();
|
|
254
|
+
}
|
|
255
|
+
else if (input === "k" || key.upArrow) {
|
|
256
|
+
handleNavigateUp();
|
|
257
|
+
}
|
|
258
|
+
else if (key.return || input === "e") {
|
|
259
|
+
handleStartEdit();
|
|
260
|
+
}
|
|
261
|
+
else if (input === "d" || key.delete) {
|
|
262
|
+
handleDelete();
|
|
263
|
+
}
|
|
264
|
+
else if (key.escape) {
|
|
265
|
+
onCancel();
|
|
266
|
+
}
|
|
267
|
+
else if (input === "s" || input === "S") {
|
|
268
|
+
onConfirm(editResponders);
|
|
269
|
+
}
|
|
270
|
+
}, { isActive: isFocused && mode === "list" });
|
|
271
|
+
// Handle keyboard input for type selection
|
|
272
|
+
useInput((input, key) => {
|
|
273
|
+
if (!isFocused || mode !== "select-type")
|
|
274
|
+
return;
|
|
275
|
+
if (input === "j" || key.downArrow) {
|
|
276
|
+
setTypeIndex((prev) => (prev < RESPONDER_TYPES.length - 1 ? prev + 1 : 0));
|
|
277
|
+
}
|
|
278
|
+
else if (input === "k" || key.upArrow) {
|
|
279
|
+
setTypeIndex((prev) => (prev > 0 ? prev - 1 : RESPONDER_TYPES.length - 1));
|
|
280
|
+
}
|
|
281
|
+
else if (key.return) {
|
|
282
|
+
handleTypeSelect();
|
|
283
|
+
}
|
|
284
|
+
else if (key.escape) {
|
|
285
|
+
handleCancel();
|
|
286
|
+
}
|
|
287
|
+
}, { isActive: isFocused && mode === "select-type" });
|
|
288
|
+
// Handle keyboard input for text editing modes
|
|
289
|
+
useInput((_input, key) => {
|
|
290
|
+
const textModes = [
|
|
291
|
+
"add-name",
|
|
292
|
+
"edit-trigger",
|
|
293
|
+
"edit-provider",
|
|
294
|
+
"edit-system",
|
|
295
|
+
"edit-command",
|
|
296
|
+
"edit-timeout",
|
|
297
|
+
"edit-maxlength",
|
|
298
|
+
];
|
|
299
|
+
if (!isFocused || !textModes.includes(mode))
|
|
300
|
+
return;
|
|
301
|
+
if (key.escape) {
|
|
302
|
+
if (mode === "add-name") {
|
|
303
|
+
handleCancel();
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
setMode("edit-responder");
|
|
307
|
+
setEditText("");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}, {
|
|
311
|
+
isActive: isFocused &&
|
|
312
|
+
[
|
|
313
|
+
"add-name",
|
|
314
|
+
"edit-trigger",
|
|
315
|
+
"edit-provider",
|
|
316
|
+
"edit-system",
|
|
317
|
+
"edit-command",
|
|
318
|
+
"edit-timeout",
|
|
319
|
+
"edit-maxlength",
|
|
320
|
+
].includes(mode),
|
|
321
|
+
});
|
|
322
|
+
// Handle keyboard input for edit-responder mode (viewing a responder)
|
|
323
|
+
useInput((input, key) => {
|
|
324
|
+
if (!isFocused || mode !== "edit-responder" || !editingResponder)
|
|
325
|
+
return;
|
|
326
|
+
if (key.escape) {
|
|
327
|
+
handleCancel();
|
|
328
|
+
}
|
|
329
|
+
else if (input === "t" || input === "T") {
|
|
330
|
+
// Edit type
|
|
331
|
+
setTypeIndex(RESPONDER_TYPES.indexOf(editingResponder.config.type));
|
|
332
|
+
setMode("select-type");
|
|
333
|
+
}
|
|
334
|
+
else if (input === "g" || input === "G") {
|
|
335
|
+
// Edit trigger
|
|
336
|
+
setEditText(editingResponder.config.trigger || "");
|
|
337
|
+
setMode("edit-trigger");
|
|
338
|
+
}
|
|
339
|
+
else if (input === "p" || input === "P") {
|
|
340
|
+
// Edit provider (only for llm type)
|
|
341
|
+
if (editingResponder.config.type === "llm") {
|
|
342
|
+
setEditText(editingResponder.config.provider || "");
|
|
343
|
+
setMode("edit-provider");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else if (input === "y" || input === "Y") {
|
|
347
|
+
// Edit system prompt (only for llm type)
|
|
348
|
+
if (editingResponder.config.type === "llm") {
|
|
349
|
+
setEditText(editingResponder.config.systemPrompt || "");
|
|
350
|
+
setMode("edit-system");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (input === "c" || input === "C") {
|
|
354
|
+
// Edit command (only for cli type)
|
|
355
|
+
if (editingResponder.config.type === "cli") {
|
|
356
|
+
setEditText(editingResponder.config.command || "");
|
|
357
|
+
setMode("edit-command");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (input === "o" || input === "O") {
|
|
361
|
+
// Edit timeout
|
|
362
|
+
setEditText(editingResponder.config.timeout?.toString() || "");
|
|
363
|
+
setMode("edit-timeout");
|
|
364
|
+
}
|
|
365
|
+
else if (input === "l" || input === "L") {
|
|
366
|
+
// Edit max length
|
|
367
|
+
setEditText(editingResponder.config.maxLength?.toString() || "");
|
|
368
|
+
setMode("edit-maxlength");
|
|
369
|
+
}
|
|
370
|
+
else if (input === "s" || input === "S") {
|
|
371
|
+
// Save and close
|
|
372
|
+
saveEditingResponder();
|
|
373
|
+
}
|
|
374
|
+
}, { isActive: isFocused && mode === "edit-responder" });
|
|
375
|
+
// Render type selection mode
|
|
376
|
+
if (mode === "select-type") {
|
|
377
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Select Responder Type" }), editingResponder && _jsxs(Text, { dimColor: true, children: [" for \"", editingResponder.name, "\""] })] }), RESPONDER_TYPES.map((type, index) => {
|
|
378
|
+
const isHighlighted = index === typeIndex;
|
|
379
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: type }), _jsxs(Text, { dimColor: true, children: [" - ", TYPE_DESCRIPTIONS[type]] })] }, type));
|
|
380
|
+
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "j/k: navigate | Enter: select | Esc: cancel" }) })] }));
|
|
381
|
+
}
|
|
382
|
+
// Render name input mode
|
|
383
|
+
if (mode === "add-name") {
|
|
384
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Add New Responder" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Name: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleNameSubmit, focus: isFocused, placeholder: "e.g., qa, reviewer, code" })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Suggested: ", SUGGESTED_NAMES.join(", ")] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next | Esc: cancel" }) })] }));
|
|
385
|
+
}
|
|
386
|
+
// Render trigger input mode
|
|
387
|
+
if (mode === "edit-trigger" && editingResponder) {
|
|
388
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit Trigger Pattern" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Use @name for mentions, leave empty for default handler" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Trigger: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: "e.g., @qa, @review (or empty for default)" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
389
|
+
}
|
|
390
|
+
// Render provider input mode
|
|
391
|
+
if (mode === "edit-provider" && editingResponder) {
|
|
392
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit LLM Provider" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Name from llmProviders config (e.g., anthropic, openai)" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Provider: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: "e.g., anthropic" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
393
|
+
}
|
|
394
|
+
// Render system prompt input mode
|
|
395
|
+
if (mode === "edit-system" && editingResponder) {
|
|
396
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit System Prompt" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Supports ", "{{project}}", " placeholder for project context"] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "System: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: "You are a helpful assistant..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
397
|
+
}
|
|
398
|
+
// Render command input mode
|
|
399
|
+
if (mode === "edit-command" && editingResponder) {
|
|
400
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit CLI Command" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Supports ", "{{message}}", " placeholder for the user message"] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Command: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: "e.g., npm run lint" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
401
|
+
}
|
|
402
|
+
// Render timeout input mode
|
|
403
|
+
if (mode === "edit-timeout" && editingResponder) {
|
|
404
|
+
const defaultTimeout = DEFAULT_TIMEOUTS[editingResponder.config.type];
|
|
405
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit Timeout" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Timeout in milliseconds (default: ", defaultTimeout, ")"] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Timeout: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: defaultTimeout.toString() })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
406
|
+
}
|
|
407
|
+
// Render max length input mode
|
|
408
|
+
if (mode === "edit-maxlength" && editingResponder) {
|
|
409
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Edit Max Response Length" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Maximum characters to send back to chat (default: 2000)" }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Max Length: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused, placeholder: "2000" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save | Esc: cancel" }) })] }));
|
|
410
|
+
}
|
|
411
|
+
// Render edit-responder mode (viewing/editing a single responder)
|
|
412
|
+
if (mode === "edit-responder" && editingResponder) {
|
|
413
|
+
const config = editingResponder.config;
|
|
414
|
+
const isLLM = config.type === "llm";
|
|
415
|
+
const isCLI = config.type === "cli";
|
|
416
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "cyan", children: ["Edit Responder: ", editingResponder.name] }) }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[T] Type: " }), _jsx(Text, { children: config.type })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[G] Trigger: " }), _jsx(Text, { dimColor: !config.trigger, children: config.trigger || "(default handler)" })] }), isLLM && (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[P] Provider: " }), _jsx(Text, { dimColor: !config.provider, children: config.provider || "(not set)" })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[Y] System: " }), _jsx(Text, { dimColor: !config.systemPrompt, children: config.systemPrompt
|
|
417
|
+
? config.systemPrompt.length > 40
|
|
418
|
+
? config.systemPrompt.substring(0, 40) + "..."
|
|
419
|
+
: config.systemPrompt
|
|
420
|
+
: "(not set)" })] })] })), isCLI && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[C] Command: " }), _jsx(Text, { dimColor: !config.command, children: config.command || "(not set)" })] })), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[O] Timeout: " }), _jsxs(Text, { dimColor: !config.timeout, children: [config.timeout || DEFAULT_TIMEOUTS[config.type], "ms"] })] }), _jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "[L] Max Length: " }), _jsx(Text, { dimColor: !config.maxLength, children: config.maxLength || 2000 })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["[T]ype [G]trigger", isLLM ? " [P]rovider [Y]system" : "", isCLI ? " [C]ommand" : "", " [O]timeout [L]ength"] }), _jsx(Text, { dimColor: true, children: "S: save | Esc: cancel" })] })] }));
|
|
421
|
+
}
|
|
422
|
+
// Calculate scroll indicators
|
|
423
|
+
const canScrollUp = scrollOffset > 0;
|
|
424
|
+
const canScrollDown = scrollOffset + visibleCount < responderNames.length;
|
|
425
|
+
const hasOverflow = responderNames.length > visibleCount;
|
|
426
|
+
// Render list mode
|
|
427
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: label }), _jsxs(Text, { dimColor: true, children: [" (", responderNames.length, " responders)"] })] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), responderNames.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No responders configured" }) })) : (visibleResponders.map((name) => {
|
|
428
|
+
const actualIndex = responderNames.indexOf(name);
|
|
429
|
+
const isHighlighted = actualIndex === highlightedIndex;
|
|
430
|
+
const config = editResponders[name];
|
|
431
|
+
// Format display: type abbreviation and trigger
|
|
432
|
+
const typeAbbrev = config.type === "claude-code" ? "claude" : config.type;
|
|
433
|
+
const trigger = config.trigger || "(default)";
|
|
434
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : "yellow", inverse: isHighlighted, children: name.padEnd(12) }), _jsx(Text, { color: "magenta", children: typeAbbrev.padEnd(8) }), _jsx(Text, { dimColor: true, children: trigger })] }, name));
|
|
435
|
+
})), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? " ▼ more" : "" }) })), _jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === responderNames.length ? "green" : undefined, children: highlightedIndex === responderNames.length ? "▸ " : " " }), _jsx(Text, { bold: highlightedIndex === responderNames.length, color: highlightedIndex === responderNames.length ? "green" : "gray", inverse: highlightedIndex === responderNames.length, children: "+ Add responder" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Enter/e: edit | d: delete" }), _jsx(Text, { dimColor: true, children: "s: save all | Esc: cancel" })] })] }));
|
|
436
|
+
}
|
|
437
|
+
export default RespondersEditor;
|
|
@@ -16,7 +16,18 @@ export const CONFIG_SECTIONS = [
|
|
|
16
16
|
id: "docker",
|
|
17
17
|
label: "Docker",
|
|
18
18
|
icon: "🐳",
|
|
19
|
-
fields: [
|
|
19
|
+
fields: [
|
|
20
|
+
"docker.ports",
|
|
21
|
+
"docker.volumes",
|
|
22
|
+
"docker.environment",
|
|
23
|
+
"docker.packages",
|
|
24
|
+
"docker.git",
|
|
25
|
+
"docker.buildCommands",
|
|
26
|
+
"docker.startCommand",
|
|
27
|
+
"docker.firewall",
|
|
28
|
+
"docker.autoStart",
|
|
29
|
+
"docker.restartCount",
|
|
30
|
+
],
|
|
20
31
|
},
|
|
21
32
|
{
|
|
22
33
|
id: "daemon",
|
|
@@ -34,13 +45,26 @@ export const CONFIG_SECTIONS = [
|
|
|
34
45
|
id: "chat",
|
|
35
46
|
label: "Chat",
|
|
36
47
|
icon: "💬",
|
|
37
|
-
fields: ["chat.enabled", "chat.provider", "chat.telegram"],
|
|
48
|
+
fields: ["chat.enabled", "chat.provider", "chat.telegram", "chat.slack", "chat.discord", "chat.responders"],
|
|
38
49
|
},
|
|
39
50
|
{
|
|
40
51
|
id: "notifications",
|
|
41
52
|
label: "Notifications",
|
|
42
53
|
icon: "🔔",
|
|
43
|
-
fields: [
|
|
54
|
+
fields: [
|
|
55
|
+
"notifications.provider",
|
|
56
|
+
"notifications.ntfy",
|
|
57
|
+
"notifications.pushover",
|
|
58
|
+
"notifications.gotify",
|
|
59
|
+
"notifications.command",
|
|
60
|
+
"notifyCommand",
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "llm",
|
|
65
|
+
label: "LLM Providers",
|
|
66
|
+
icon: "🧠",
|
|
67
|
+
fields: ["llmProviders"],
|
|
44
68
|
},
|
|
45
69
|
];
|
|
46
70
|
/**
|
|
@@ -24,21 +24,34 @@ export const CHAT_PRESETS = [
|
|
|
24
24
|
{
|
|
25
25
|
id: "slack",
|
|
26
26
|
name: "Slack",
|
|
27
|
-
description: "Slack App for chat control
|
|
27
|
+
description: "Slack App for chat control via Socket Mode",
|
|
28
28
|
category: "chat",
|
|
29
29
|
fields: {
|
|
30
30
|
"chat.enabled": true,
|
|
31
31
|
"chat.provider": "slack",
|
|
32
|
+
"chat.slack": {
|
|
33
|
+
enabled: true,
|
|
34
|
+
botToken: "", // xoxb-... from OAuth & Permissions
|
|
35
|
+
appToken: "", // xapp-... from Basic Information > App-Level Tokens
|
|
36
|
+
signingSecret: "", // From Basic Information > App Credentials
|
|
37
|
+
allowedChannelIds: [],
|
|
38
|
+
},
|
|
32
39
|
},
|
|
33
40
|
},
|
|
34
41
|
{
|
|
35
42
|
id: "discord",
|
|
36
43
|
name: "Discord",
|
|
37
|
-
description: "Discord Bot for chat control
|
|
44
|
+
description: "Discord Bot for chat control via Discord Gateway",
|
|
38
45
|
category: "chat",
|
|
39
46
|
fields: {
|
|
40
47
|
"chat.enabled": true,
|
|
41
48
|
"chat.provider": "discord",
|
|
49
|
+
"chat.discord": {
|
|
50
|
+
enabled: true,
|
|
51
|
+
botToken: "", // Bot token from Discord Developer Portal > Bot > Token
|
|
52
|
+
allowedGuildIds: [], // Server/guild IDs to restrict access (optional)
|
|
53
|
+
allowedChannelIds: [], // Channel IDs to restrict access (optional)
|
|
54
|
+
},
|
|
42
55
|
},
|
|
43
56
|
},
|
|
44
57
|
];
|
|
@@ -29,10 +29,10 @@ export interface ChatCommand {
|
|
|
29
29
|
export interface ChatClientConfig {
|
|
30
30
|
/** Whether the chat client is enabled */
|
|
31
31
|
enabled: boolean;
|
|
32
|
-
/** The chat provider type (e.g., "telegram") */
|
|
33
|
-
provider:
|
|
32
|
+
/** The chat provider type (e.g., "telegram", "slack", "discord") */
|
|
33
|
+
provider: ChatProvider;
|
|
34
34
|
/** Provider-specific settings */
|
|
35
|
-
settings: TelegramSettings;
|
|
35
|
+
settings: TelegramSettings | SlackSettings | DiscordSettings;
|
|
36
36
|
}
|
|
37
37
|
export interface TelegramSettings {
|
|
38
38
|
/** Telegram Bot API token */
|
|
@@ -40,7 +40,25 @@ export interface TelegramSettings {
|
|
|
40
40
|
/** Allowed chat IDs (for security - only respond in these chats) */
|
|
41
41
|
allowedChatIds?: string[];
|
|
42
42
|
}
|
|
43
|
-
export
|
|
43
|
+
export interface SlackSettings {
|
|
44
|
+
/** Slack Bot Token (xoxb-...) - for Web API calls */
|
|
45
|
+
botToken: string;
|
|
46
|
+
/** Slack App Token (xapp-...) - for Socket Mode connection */
|
|
47
|
+
appToken: string;
|
|
48
|
+
/** Slack Signing Secret - for verifying request signatures */
|
|
49
|
+
signingSecret: string;
|
|
50
|
+
/** Allowed channel IDs (for security - only respond in these channels) */
|
|
51
|
+
allowedChannelIds?: string[];
|
|
52
|
+
}
|
|
53
|
+
export interface DiscordSettings {
|
|
54
|
+
/** Discord Bot Token (from Developer Portal) */
|
|
55
|
+
botToken: string;
|
|
56
|
+
/** Allowed guild (server) IDs (for security - only respond in these servers) */
|
|
57
|
+
allowedGuildIds?: string[];
|
|
58
|
+
/** Allowed channel IDs (for security - only respond in these channels) */
|
|
59
|
+
allowedChannelIds?: string[];
|
|
60
|
+
}
|
|
61
|
+
export type ChatProvider = "telegram" | "slack" | "discord";
|
|
44
62
|
/**
|
|
45
63
|
* Callback for handling incoming chat commands.
|
|
46
64
|
*/
|
|
@@ -55,6 +73,10 @@ export type ChatMessageHandler = (message: ChatMessage) => Promise<void>;
|
|
|
55
73
|
export interface SendMessageOptions {
|
|
56
74
|
/** Inline keyboard buttons (Telegram-specific) */
|
|
57
75
|
inlineKeyboard?: InlineButton[][];
|
|
76
|
+
/** Thread timestamp for reply context (Slack-specific) */
|
|
77
|
+
threadTs?: string;
|
|
78
|
+
/** Message ID to reply to (Telegram-specific) */
|
|
79
|
+
replyToMessageId?: number;
|
|
58
80
|
}
|
|
59
81
|
/**
|
|
60
82
|
* Inline button for chat messages.
|
|
@@ -127,6 +149,13 @@ export declare function generateProjectId(): string;
|
|
|
127
149
|
* - "/add Fix the login bug" -> { command: "add", args: ["Fix", "the", "login", "bug"] }
|
|
128
150
|
*/
|
|
129
151
|
export declare function parseCommand(text: string, message: ChatMessage): ChatCommand | null;
|
|
152
|
+
/**
|
|
153
|
+
* Escape HTML special characters in a string.
|
|
154
|
+
* This is required for Telegram messages since they use parse_mode: "HTML".
|
|
155
|
+
* Characters like <, >, & in command output (e.g., git status showing <file>)
|
|
156
|
+
* would otherwise be interpreted as HTML tags and cause API errors.
|
|
157
|
+
*/
|
|
158
|
+
export declare function escapeHtml(text: string): string;
|
|
130
159
|
/**
|
|
131
160
|
* Strip ANSI escape codes from a string.
|
|
132
161
|
* This is useful for cleaning output before sending to chat services
|
|
@@ -28,7 +28,17 @@ export function parseCommand(text, message) {
|
|
|
28
28
|
if (!trimmed)
|
|
29
29
|
return null;
|
|
30
30
|
// Valid commands
|
|
31
|
-
const validCommands = [
|
|
31
|
+
const validCommands = [
|
|
32
|
+
"run",
|
|
33
|
+
"status",
|
|
34
|
+
"add",
|
|
35
|
+
"exec",
|
|
36
|
+
"stop",
|
|
37
|
+
"help",
|
|
38
|
+
"start",
|
|
39
|
+
"action",
|
|
40
|
+
"claude",
|
|
41
|
+
];
|
|
32
42
|
// Check for slash command format: /command [args...]
|
|
33
43
|
if (trimmed.startsWith("/")) {
|
|
34
44
|
const parts = trimmed.slice(1).split(/\s+/);
|
|
@@ -57,6 +67,15 @@ export function parseCommand(text, message) {
|
|
|
57
67
|
}
|
|
58
68
|
return null;
|
|
59
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Escape HTML special characters in a string.
|
|
72
|
+
* This is required for Telegram messages since they use parse_mode: "HTML".
|
|
73
|
+
* Characters like <, >, & in command output (e.g., git status showing <file>)
|
|
74
|
+
* would otherwise be interpreted as HTML tags and cause API errors.
|
|
75
|
+
*/
|
|
76
|
+
export function escapeHtml(text) {
|
|
77
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
78
|
+
}
|
|
60
79
|
/**
|
|
61
80
|
* Strip ANSI escape codes from a string.
|
|
62
81
|
* This is useful for cleaning output before sending to chat services
|