ralph-cli-sandboxed 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/action.js +47 -20
  3. package/dist/commands/chat.d.ts +1 -1
  4. package/dist/commands/chat.js +325 -62
  5. package/dist/commands/config.js +2 -1
  6. package/dist/commands/daemon.d.ts +2 -5
  7. package/dist/commands/daemon.js +118 -49
  8. package/dist/commands/docker.js +110 -73
  9. package/dist/commands/fix-config.js +2 -1
  10. package/dist/commands/fix-prd.js +2 -2
  11. package/dist/commands/help.js +19 -3
  12. package/dist/commands/init.js +78 -17
  13. package/dist/commands/listen.js +116 -5
  14. package/dist/commands/logo.d.ts +5 -0
  15. package/dist/commands/logo.js +41 -0
  16. package/dist/commands/notify.js +1 -1
  17. package/dist/commands/once.js +19 -9
  18. package/dist/commands/prd.js +20 -2
  19. package/dist/commands/run.js +111 -27
  20. package/dist/commands/slack.d.ts +10 -0
  21. package/dist/commands/slack.js +333 -0
  22. package/dist/config/responder-presets.json +69 -0
  23. package/dist/index.js +6 -1
  24. package/dist/providers/discord.d.ts +82 -0
  25. package/dist/providers/discord.js +697 -0
  26. package/dist/providers/slack.d.ts +79 -0
  27. package/dist/providers/slack.js +715 -0
  28. package/dist/providers/telegram.d.ts +30 -0
  29. package/dist/providers/telegram.js +190 -7
  30. package/dist/responders/claude-code-responder.d.ts +48 -0
  31. package/dist/responders/claude-code-responder.js +203 -0
  32. package/dist/responders/cli-responder.d.ts +62 -0
  33. package/dist/responders/cli-responder.js +298 -0
  34. package/dist/responders/llm-responder.d.ts +135 -0
  35. package/dist/responders/llm-responder.js +582 -0
  36. package/dist/templates/macos-scripts.js +2 -4
  37. package/dist/templates/prompts.js +4 -2
  38. package/dist/tui/ConfigEditor.js +42 -5
  39. package/dist/tui/components/ArrayEditor.js +1 -1
  40. package/dist/tui/components/EditorPanel.js +10 -6
  41. package/dist/tui/components/HelpPanel.d.ts +1 -1
  42. package/dist/tui/components/HelpPanel.js +1 -1
  43. package/dist/tui/components/JsonSnippetEditor.js +8 -5
  44. package/dist/tui/components/KeyValueEditor.js +69 -5
  45. package/dist/tui/components/LLMProvidersEditor.d.ts +22 -0
  46. package/dist/tui/components/LLMProvidersEditor.js +357 -0
  47. package/dist/tui/components/ObjectEditor.js +1 -1
  48. package/dist/tui/components/Preview.js +1 -1
  49. package/dist/tui/components/RespondersEditor.d.ts +22 -0
  50. package/dist/tui/components/RespondersEditor.js +437 -0
  51. package/dist/tui/components/SectionNav.js +27 -3
  52. package/dist/tui/utils/presets.js +15 -2
  53. package/dist/utils/chat-client.d.ts +33 -4
  54. package/dist/utils/chat-client.js +20 -1
  55. package/dist/utils/config.d.ts +100 -1
  56. package/dist/utils/config.js +78 -1
  57. package/dist/utils/daemon-actions.d.ts +19 -0
  58. package/dist/utils/daemon-actions.js +111 -0
  59. package/dist/utils/daemon-client.d.ts +21 -0
  60. package/dist/utils/daemon-client.js +28 -1
  61. package/dist/utils/llm-client.d.ts +82 -0
  62. package/dist/utils/llm-client.js +185 -0
  63. package/dist/utils/message-queue.js +6 -6
  64. package/dist/utils/notification.d.ts +10 -2
  65. package/dist/utils/notification.js +111 -4
  66. package/dist/utils/prd-validator.js +60 -19
  67. package/dist/utils/prompt.js +22 -12
  68. package/dist/utils/responder-logger.d.ts +47 -0
  69. package/dist/utils/responder-logger.js +129 -0
  70. package/dist/utils/responder-presets.d.ts +92 -0
  71. package/dist/utils/responder-presets.js +156 -0
  72. package/dist/utils/responder.d.ts +88 -0
  73. package/dist/utils/responder.js +207 -0
  74. package/dist/utils/stream-json.js +6 -6
  75. package/docs/CHAT-CLIENTS.md +520 -0
  76. package/docs/CHAT-RESPONDERS.md +785 -0
  77. package/docs/DEVELOPMENT.md +25 -0
  78. package/docs/USEFUL_ACTIONS.md +815 -0
  79. package/docs/chat-architecture.md +251 -0
  80. package/package.json +14 -1
@@ -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: ["docker.ports", "docker.volumes", "docker.environment", "docker.packages", "docker.git", "docker.buildCommands", "docker.startCommand", "docker.firewall", "docker.autoStart", "docker.restartCount"],
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: ["notifications.provider", "notifications.ntfy", "notifications.pushover", "notifications.gotify", "notifications.command", "notifyCommand"],
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 (coming soon)",
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 (coming soon)",
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: "telegram";
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 type ChatProvider = "telegram";
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 = ["run", "status", "add", "exec", "stop", "help", "start", "action", "claude"];
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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