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
@@ -11,6 +11,8 @@ import { ArrayEditor } from "./components/ArrayEditor.js";
11
11
  import { ObjectEditor } from "./components/ObjectEditor.js";
12
12
  import { KeyValueEditor } from "./components/KeyValueEditor.js";
13
13
  import { JsonSnippetEditor } from "./components/JsonSnippetEditor.js";
14
+ import { LLMProvidersEditor } from "./components/LLMProvidersEditor.js";
15
+ import { RespondersEditor } from "./components/RespondersEditor.js";
14
16
  import { Preview } from "./components/Preview.js";
15
17
  import { HelpPanel } from "./components/HelpPanel.js";
16
18
  import { PresetSelector } from "./components/PresetSelector.js";
@@ -41,7 +43,7 @@ function setValueAtPath(obj, path, value) {
41
43
  export function ConfigEditor() {
42
44
  const { exit } = useApp();
43
45
  const terminalSize = useTerminalSize();
44
- const { config, loading, error, hasChanges, saveConfig, updateConfig, } = useConfig();
46
+ const { config, loading, error, hasChanges, saveConfig, updateConfig } = useConfig();
45
47
  // Calculate available height for scrollable content
46
48
  // Reserve lines for: header (2), status message (1), footer (2), borders (2)
47
49
  const availableHeight = Math.max(8, terminalSize.rows - 7);
@@ -272,17 +274,48 @@ export function ConfigEditor() {
272
274
  for (const [k, v] of Object.entries(objValue)) {
273
275
  stringEntries[k] = typeof v === "string" ? v : JSON.stringify(v);
274
276
  }
277
+ // Check if this is the llmProviders field
278
+ const isLLMProviders = selectedField === "llmProviders";
279
+ if (isLLMProviders) {
280
+ return (_jsx(LLMProvidersEditor, { label: currentFieldLabel, providers: currentFieldValue || {}, onConfirm: handleFieldConfirm, onCancel: handleFieldCancel, isFocused: true, maxHeight: editorMaxHeight }));
281
+ }
282
+ // Check if this is the chat.responders field
283
+ const isResponders = selectedField === "chat.responders";
284
+ if (isResponders) {
285
+ return (_jsx(RespondersEditor, { label: currentFieldLabel, responders: currentFieldValue || {}, onConfirm: handleFieldConfirm, onCancel: handleFieldCancel, isFocused: true, maxHeight: editorMaxHeight }));
286
+ }
275
287
  // Check if this is a notification provider config field
276
288
  const isNotificationProvider = selectedField &&
277
289
  (selectedField === "notifications.ntfy" ||
278
290
  selectedField === "notifications.pushover" ||
279
291
  selectedField === "notifications.gotify");
280
- if (isNotificationProvider) {
292
+ // Check if this is a chat provider config field
293
+ const isChatProvider = selectedField && (selectedField === "chat.slack" || selectedField === "chat.telegram");
294
+ if (isNotificationProvider || isChatProvider) {
281
295
  // Extract provider name from field path
282
296
  const providerName = selectedField.split(".").pop() || "";
283
297
  return (_jsx(KeyValueEditor, { label: currentFieldLabel, entries: stringEntries, providerName: providerName, onConfirm: (entries) => {
284
- // For notification providers, keep values as strings
285
- handleFieldConfirm(entries);
298
+ // Parse back array values (like allowedChatIds, allowedChannelIds)
299
+ const parsedEntries = {};
300
+ for (const [k, v] of Object.entries(entries)) {
301
+ // Check if this looks like a JSON array or object
302
+ if (v.startsWith("[") || v.startsWith("{")) {
303
+ try {
304
+ parsedEntries[k] = JSON.parse(v);
305
+ }
306
+ catch {
307
+ parsedEntries[k] = v;
308
+ }
309
+ }
310
+ else if (v === "true" || v === "false") {
311
+ // Parse boolean values
312
+ parsedEntries[k] = v === "true";
313
+ }
314
+ else {
315
+ parsedEntries[k] = v;
316
+ }
317
+ }
318
+ handleFieldConfirm(parsedEntries);
286
319
  }, onCancel: handleFieldCancel, isFocused: true }));
287
320
  }
288
321
  return (_jsx(ObjectEditor, { label: currentFieldLabel, entries: stringEntries, onConfirm: (entries) => {
@@ -308,6 +341,10 @@ export function ConfigEditor() {
308
341
  return (_jsx(StringEditor, { label: currentFieldLabel, value: String(currentFieldValue || ""), onConfirm: handleFieldConfirm, onCancel: handleFieldCancel, isFocused: true }));
309
342
  }
310
343
  };
311
- return (_jsxs(Box, { flexDirection: "column", children: [helpVisible && (_jsx(HelpPanel, { visible: helpVisible, onClose: toggleHelp })), _jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "ralph config" }), hasChanges && _jsx(Text, { color: "yellow", children: " (unsaved changes)" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "[S] Save" }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { dimColor: true, children: "[Q] Quit" }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { dimColor: true, children: "[?] Help" })] })] }), statusMessage && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: statusMessage.includes("Failed") || statusMessage.includes("Validation") ? "red" : "green", children: statusMessage }) })), focusPane === "field-editor" ? (_jsx(Box, { children: renderFieldEditor() })) : focusPane === "preset-selector" ? (_jsx(Box, { children: _jsx(PresetSelector, { sectionId: selectedSection, config: config, onSelectPreset: handleSelectPreset, onSkip: handleSkipPreset, onCancel: handleCancelPreset, isFocused: true }) })) : (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsx(SectionNav, { selectedSection: selectedSection, onSelectSection: handleSelectSection, isFocused: focusPane === "nav", maxHeight: navMaxHeight }) }), _jsx(Box, { flexGrow: 1, children: _jsx(EditorPanel, { config: config, selectedSection: selectedSection, selectedField: selectedField, onSelectField: handleSelectField, onBack: handleBack, isFocused: focusPane === "editor", validationErrors: validationErrors, maxHeight: editorMaxHeight }) }), _jsx(Preview, { config: config, selectedSection: selectedSection, visible: previewVisible })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [focusPane === "nav" && "j/k: navigate | Enter: select | l/→: editor | Tab: toggle preview", focusPane === "editor" && "j/k: navigate | Enter: edit | J: JSON | h/←: nav | Tab: preview | p: presets", focusPane === "field-editor" && "Follow editor hints", focusPane === "preset-selector" && "j/k: navigate | Enter: select | Esc: back"] }) })] }));
344
+ return (_jsxs(Box, { flexDirection: "column", children: [helpVisible && _jsx(HelpPanel, { visible: helpVisible, onClose: toggleHelp }), _jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "ralph config" }), hasChanges && _jsx(Text, { color: "yellow", children: " (unsaved changes)" })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "[S] Save" }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { dimColor: true, children: "[Q] Quit" }), _jsx(Text, { dimColor: true, children: " | " }), _jsx(Text, { dimColor: true, children: "[?] Help" })] })] }), statusMessage && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: statusMessage.includes("Failed") || statusMessage.includes("Validation")
345
+ ? "red"
346
+ : "green", children: statusMessage }) })), focusPane === "field-editor" ? (_jsx(Box, { children: renderFieldEditor() })) : focusPane === "preset-selector" ? (_jsx(Box, { children: _jsx(PresetSelector, { sectionId: selectedSection, config: config, onSelectPreset: handleSelectPreset, onSkip: handleSkipPreset, onCancel: handleCancelPreset, isFocused: true }) })) : (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsx(SectionNav, { selectedSection: selectedSection, onSelectSection: handleSelectSection, isFocused: focusPane === "nav", maxHeight: navMaxHeight }) }), _jsx(Box, { flexGrow: 1, children: _jsx(EditorPanel, { config: config, selectedSection: selectedSection, selectedField: selectedField, onSelectField: handleSelectField, onBack: handleBack, isFocused: focusPane === "editor", validationErrors: validationErrors, maxHeight: editorMaxHeight }) }), _jsx(Preview, { config: config, selectedSection: selectedSection, visible: previewVisible })] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [focusPane === "nav" &&
347
+ "j/k: navigate | Enter: select | l/→: editor | Tab: toggle preview", focusPane === "editor" &&
348
+ "j/k: navigate | Enter: edit | J: JSON | h/←: nav | Tab: preview | p: presets", focusPane === "field-editor" && "Follow editor hints", focusPane === "preset-selector" && "j/k: navigate | Enter: select | Esc: back"] }) })] }));
312
349
  }
313
350
  export default ConfigEditor;
@@ -177,7 +177,7 @@ export function ArrayEditor({ label, items, onConfirm, onCancel, isFocused = tru
177
177
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add" ? "Add New Item" : `Edit Item ${editingIndex + 1}` }) }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [">", " "] }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleTextSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
178
178
  }
179
179
  // Render list mode
180
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", editItems.length, " items)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" [", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), editItems.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No items" }) })) : (visibleItems.map((item, visibleIndex) => {
180
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", editItems.length, " items)"] }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" ", "[", highlightedIndex + 1, "/", totalOptions, "]"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? " ▲ more" : "" }) })), editItems.length === 0 && scrollOffset === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No items" }) })) : (visibleItems.map((item, visibleIndex) => {
181
181
  // Check if this is the "Add item" option
182
182
  if (typeof item === "object" && "isAddOption" in item) {
183
183
  const actualIndex = editItems.length;
@@ -213,7 +213,7 @@ export function EditorPanel({ config, selectedSection, selectedField, onSelectFi
213
213
  if (!currentSection || fields.length === 0) {
214
214
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: breadcrumb }) }), _jsx(Text, { dimColor: true, children: "No fields in this section" })] }));
215
215
  }
216
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: breadcrumb }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" (", highlightedIndex + 1, "/", totalFields, ")"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? "▲ more" : "" }) })), visibleFields.map((field) => {
216
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, flexGrow: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: breadcrumb }), hasOverflow && (_jsxs(Text, { dimColor: true, children: [" ", "(", highlightedIndex + 1, "/", totalFields, ")"] }))] }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollUp ? "cyan" : "gray", dimColor: !canScrollUp, children: canScrollUp ? "▲ more" : "" }) })), visibleFields.map((field) => {
217
217
  const actualIndex = fields.findIndex((f) => f.path === field.path);
218
218
  const isHighlighted = actualIndex === highlightedIndex;
219
219
  const value = getValueAtPath(config, field.path);
@@ -221,12 +221,16 @@ export function EditorPanel({ config, selectedSection, selectedField, onSelectFi
221
221
  const fieldHasError = hasFieldError(validationErrors, field.path);
222
222
  const fieldErrors = getFieldErrors(validationErrors, field.path);
223
223
  // Color based on field type, but red if there's an error
224
- const typeColor = fieldHasError ? "red"
225
- : field.type === "array" ? "yellow"
226
- : field.type === "object" ? "magenta"
227
- : field.type === "boolean" ? "blue"
224
+ const typeColor = fieldHasError
225
+ ? "red"
226
+ : field.type === "array"
227
+ ? "yellow"
228
+ : field.type === "object"
229
+ ? "magenta"
230
+ : field.type === "boolean"
231
+ ? "blue"
228
232
  : undefined;
229
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : fieldHasError ? "red" : undefined, children: isHighlighted ? "▸ " : fieldHasError ? "✗ " : " " }), _jsx(Text, { bold: isHighlighted, color: fieldHasError ? "red" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: field.label }), _jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { color: typeColor, dimColor: value === undefined || value === null, children: displayValue }), (field.type === "array" || field.type === "object") && (_jsx(Text, { dimColor: true, children: " \u2192" }))] }), fieldHasError && fieldErrors.length > 0 && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: "red", dimColor: true, children: fieldErrors[0].message }) }))] }, field.path));
233
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : fieldHasError ? "red" : undefined, children: isHighlighted ? "▸ " : fieldHasError ? "✗ " : " " }), _jsx(Text, { bold: isHighlighted, color: fieldHasError ? "red" : isHighlighted ? "cyan" : undefined, inverse: isHighlighted, children: field.label }), _jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { color: typeColor, dimColor: value === undefined || value === null, children: displayValue }), (field.type === "array" || field.type === "object") && _jsx(Text, { dimColor: true, children: " \u2192" })] }), fieldHasError && fieldErrors.length > 0 && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: "red", dimColor: true, children: fieldErrors[0].message }) }))] }, field.path));
230
234
  }), hasOverflow && (_jsx(Box, { children: _jsx(Text, { color: canScrollDown ? "cyan" : "gray", dimColor: !canScrollDown, children: canScrollDown ? "▼ more" : "" }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["j/k: navigate | Enter: edit | J: edit as JSON", hasOverflow && " | PgUp/Dn: scroll", " | Esc: back"] }) })] }));
231
235
  }
232
236
  export default EditorPanel;
@@ -9,5 +9,5 @@ export interface HelpPanelProps {
9
9
  * HelpPanel displays all keyboard shortcuts for the config editor.
10
10
  * Toggle with the ? key.
11
11
  */
12
- export declare function HelpPanel({ visible, onClose, }: HelpPanelProps): React.ReactElement | null;
12
+ export declare function HelpPanel({ visible, onClose }: HelpPanelProps): React.ReactElement | null;
13
13
  export default HelpPanel;
@@ -54,7 +54,7 @@ const SHORTCUT_GROUPS = [
54
54
  * HelpPanel displays all keyboard shortcuts for the config editor.
55
55
  * Toggle with the ? key.
56
56
  */
57
- export function HelpPanel({ visible, onClose, }) {
57
+ export function HelpPanel({ visible, onClose }) {
58
58
  // Handle keyboard input to close help
59
59
  useInput((input, key) => {
60
60
  if (input === "?" || key.escape) {
@@ -75,13 +75,16 @@ function validateJsonStructure(value, label) {
75
75
  if (serverConfig.args && !Array.isArray(serverConfig.args)) {
76
76
  warnings.push(`Server "${name}": "args" should be an array`);
77
77
  }
78
- if (serverConfig.env && (typeof serverConfig.env !== "object" || Array.isArray(serverConfig.env))) {
78
+ if (serverConfig.env &&
79
+ (typeof serverConfig.env !== "object" || Array.isArray(serverConfig.env))) {
79
80
  warnings.push(`Server "${name}": "env" should be an object`);
80
81
  }
81
82
  }
82
83
  }
83
84
  // Daemon Actions validation
84
- if (label.toLowerCase().includes("action") && typeof value === "object" && !Array.isArray(value)) {
85
+ if (label.toLowerCase().includes("action") &&
86
+ typeof value === "object" &&
87
+ !Array.isArray(value)) {
85
88
  const actions = value;
86
89
  for (const [name, config] of Object.entries(actions)) {
87
90
  if (typeof config !== "object" || config === null) {
@@ -227,7 +230,7 @@ function highlightJson(json, maxLines, maxLineWidth = 60) {
227
230
  elements.push(_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [lineNum, " "] }), tokens] }, i));
228
231
  }
229
232
  if (hasMore) {
230
- elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
233
+ elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
231
234
  }
232
235
  return elements;
233
236
  }
@@ -373,8 +376,8 @@ export function JsonSnippetEditor({ label, value, onConfirm, onCancel, isFocused
373
376
  // Account for border (2) and padding (2) when calculating preview width
374
377
  const previewWidth = Math.max(40, maxWidth - 4);
375
378
  const previewLines = highlightJson(editText, previewMaxLines, previewWidth);
376
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit as JSON: ", label] }), copied && _jsx(Text, { color: "green", children: " Copied!" })] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: previewLines }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: parseError ? (_jsx(Box, { children: _jsxs(Text, { color: "red", bold: true, children: ["Error: ", parseError.line && parseError.column
379
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit as JSON: ", label] }), copied && _jsx(Text, { color: "green", children: " Copied!" })] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: previewLines }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: parseError ? (_jsx(Box, { children: _jsxs(Text, { color: "red", bold: true, children: ["Error:", " ", parseError.line && parseError.column
377
380
  ? `Line ${parseError.line}:${parseError.column} - `
378
- : "", parseError.message] }) })) : warningCount > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", bold: true, children: [warningCount, " warning", warningCount > 1 ? "s" : "", ":"] }), warnings.slice(0, 3).map((w, i) => (_jsxs(Text, { color: "yellow", dimColor: true, children: [" - ", w] }, i))), warningCount > 3 && (_jsxs(Text, { dimColor: true, children: [" ... and ", warningCount - 3, " more"] }))] })) : (_jsx(Text, { color: "green", children: "Valid JSON" })) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "e/Enter: edit | s: save | c: copy | f: format" }), _jsx(Text, { dimColor: true, children: "j/k: scroll | PgUp/Dn: page | Esc: cancel" })] })] }));
381
+ : "", parseError.message] }) })) : warningCount > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", bold: true, children: [warningCount, " warning", warningCount > 1 ? "s" : "", ":"] }), warnings.slice(0, 3).map((w, i) => (_jsxs(Text, { color: "yellow", dimColor: true, children: [" ", "- ", w] }, i))), warningCount > 3 && _jsxs(Text, { dimColor: true, children: [" ... and ", warningCount - 3, " more"] })] })) : (_jsx(Text, { color: "green", children: "Valid JSON" })) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "e/Enter: edit | s: save | c: copy | f: format" }), _jsx(Text, { dimColor: true, children: "j/k: scroll | PgUp/Dn: page | Esc: cancel" })] })] }));
379
382
  }
380
383
  export default JsonSnippetEditor;
@@ -24,6 +24,70 @@ export const PROVIDER_HINTS = {
24
24
  { key: "token", description: "Gotify app token", required: true },
25
25
  { key: "priority", description: "Message priority (0-10)" },
26
26
  ],
27
+ // Chat provider hints
28
+ slack: [
29
+ {
30
+ key: "botToken",
31
+ description: "Slack Bot Token (xoxb-...) from OAuth & Permissions",
32
+ required: true,
33
+ },
34
+ {
35
+ key: "appToken",
36
+ description: "Slack App Token (xapp-...) from Basic Information > App-Level Tokens",
37
+ required: true,
38
+ },
39
+ {
40
+ key: "signingSecret",
41
+ description: "Slack Signing Secret from Basic Information > App Credentials",
42
+ required: true,
43
+ },
44
+ { key: "allowedChannelIds", description: "Only respond in these channel IDs (security)" },
45
+ { key: "enabled", description: "Enable/disable Slack integration" },
46
+ ],
47
+ telegram: [
48
+ { key: "botToken", description: "Telegram Bot API token from @BotFather", required: true },
49
+ { key: "allowedChatIds", description: "Only respond in these chat IDs (security)" },
50
+ { key: "enabled", description: "Enable/disable Telegram integration" },
51
+ ],
52
+ discord: [
53
+ {
54
+ key: "botToken",
55
+ description: "Discord Bot Token from Developer Portal > Bot > Token",
56
+ required: true,
57
+ },
58
+ { key: "allowedGuildIds", description: "Only respond in these server/guild IDs (security)" },
59
+ { key: "allowedChannelIds", description: "Only respond in these channel IDs (security)" },
60
+ { key: "enabled", description: "Enable/disable Discord integration" },
61
+ ],
62
+ // LLM provider hints
63
+ anthropic: [
64
+ { key: "type", description: "Provider type (anthropic)", required: true },
65
+ {
66
+ key: "model",
67
+ description: "Model name (e.g., claude-sonnet-4-20250514, claude-opus-4-20250514)",
68
+ required: true,
69
+ },
70
+ { key: "apiKey", description: "API key (defaults to ANTHROPIC_API_KEY env var)" },
71
+ { key: "baseUrl", description: "Custom API base URL (optional)" },
72
+ ],
73
+ openai: [
74
+ { key: "type", description: "Provider type (openai)", required: true },
75
+ { key: "model", description: "Model name (e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo)", required: true },
76
+ { key: "apiKey", description: "API key (defaults to OPENAI_API_KEY env var)" },
77
+ { key: "baseUrl", description: "Custom API base URL (for OpenAI-compatible services)" },
78
+ ],
79
+ ollama: [
80
+ { key: "type", description: "Provider type (ollama)", required: true },
81
+ { key: "model", description: "Model name (e.g., llama3, mistral, codellama)", required: true },
82
+ { key: "baseUrl", description: "Ollama server URL (default: http://localhost:11434)" },
83
+ ],
84
+ // Generic LLM provider hint for unknown providers
85
+ llmprovider: [
86
+ { key: "type", description: "Provider type (anthropic, openai, or ollama)", required: true },
87
+ { key: "model", description: "Model name", required: true },
88
+ { key: "apiKey", description: "API key (optional, uses env var if not set)" },
89
+ { key: "baseUrl", description: "Custom API base URL (optional)" },
90
+ ],
27
91
  };
28
92
  /**
29
93
  * KeyValueEditor component for editing key-value pairs with provider-specific hints.
@@ -45,7 +109,7 @@ export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused
45
109
  }, [providerName]);
46
110
  // Get hints that are not already in entries
47
111
  const availableHints = useMemo(() => {
48
- return providerHints.filter(hint => !(hint.key in editEntries));
112
+ return providerHints.filter((hint) => !(hint.key in editEntries));
49
113
  }, [providerHints, editEntries]);
50
114
  // Get sorted keys for consistent ordering
51
115
  const keys = Object.keys(editEntries).sort();
@@ -244,17 +308,17 @@ export function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused
244
308
  // Render value input mode (for add or edit)
245
309
  if (mode === "add-value" || mode === "edit-value") {
246
310
  // Find hint description for the current key
247
- const currentHint = providerHints.find(h => h.key === newKey);
311
+ const currentHint = providerHints.find((h) => h.key === newKey);
248
312
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: mode === "add-value" ? "Add New Entry - Enter Value" : `Edit Value for "${newKey}"` }) }), mode === "add-value" && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Key: " }), _jsx(Text, { color: "yellow", children: newKey }), currentHint && currentHint.required && _jsx(Text, { color: "red", children: " (required)" })] })), currentHint && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: currentHint.description }) })), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Value: " }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleValueSubmit, focus: isFocused })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: confirm | Esc: cancel" }) })] }));
249
313
  }
250
314
  // Check for missing required keys
251
- const missingRequired = providerHints.filter(h => h.required && !(h.key in editEntries));
315
+ const missingRequired = providerHints.filter((h) => h.required && !(h.key in editEntries));
252
316
  // Render list mode
253
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), providerName && _jsxs(Text, { dimColor: true, children: [" - ", providerName] })] }), missingRequired.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 Missing required: " }), _jsx(Text, { color: "yellow", children: missingRequired.map(h => h.key).join(", ") })] })), keys.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (keys.map((key, index) => {
317
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["Edit: ", label] }), _jsxs(Text, { dimColor: true, children: [" (", keys.length, " entries)"] }), providerName && _jsxs(Text, { dimColor: true, children: [" - ", providerName] })] }), missingRequired.length > 0 && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0 Missing required: " }), _jsx(Text, { color: "yellow", children: missingRequired.map((h) => h.key).join(", ") })] })), keys.length === 0 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, italic: true, children: "No entries" }) })) : (keys.map((key, index) => {
254
318
  const isHighlighted = index === highlightedIndex;
255
319
  const isExpanded = expandedKeys.has(key);
256
320
  const value = editEntries[key];
257
- const hint = providerHints.find(h => h.key === key);
321
+ const hint = providerHints.find((h) => h.key === key);
258
322
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? "cyan" : undefined, children: isHighlighted ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: isExpanded ? "▼ " : "▶ " }), _jsx(Text, { bold: isHighlighted, color: isHighlighted ? "cyan" : hint?.required ? "yellow" : "yellow", inverse: isHighlighted, children: key }), hint?.required && _jsx(Text, { color: "red", children: " *" }), !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ": " }), _jsx(Text, { dimColor: true, children: value.length > 30 ? value.substring(0, 30) + "..." : value })] }))] }), isExpanded && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: "green", children: value || "(empty)" }) }))] }, `entry-${key}`));
259
323
  })), hasHints && (_jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === keys.length ? "green" : undefined, children: highlightedIndex === keys.length ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === keys.length, color: highlightedIndex === keys.length ? "green" : "gray", inverse: highlightedIndex === keys.length, children: "+ Add from hints" })] })), _jsxs(Box, { children: [_jsx(Text, { color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : undefined, children: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "▸ " : " " }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { bold: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), color: highlightedIndex === (hasHints ? keys.length + 1 : keys.length) ? "green" : "gray", inverse: highlightedIndex === (hasHints ? keys.length + 1 : keys.length), children: "+ Add custom entry" })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "j/k: navigate | Tab/Space: expand | Enter/e: edit" }), _jsx(Text, { dimColor: true, children: "d: delete | s: save | Esc: cancel" })] })] }));
260
324
  }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import type { LLMProvidersConfig } from "../../utils/config.js";
3
+ export interface LLMProvidersEditorProps {
4
+ /** The label to display for this field */
5
+ label: string;
6
+ /** The current LLM providers config */
7
+ providers: LLMProvidersConfig;
8
+ /** Called when the user confirms the edit */
9
+ onConfirm: (newProviders: LLMProvidersConfig) => void;
10
+ /** Called when the user cancels the edit (Esc) */
11
+ onCancel: () => void;
12
+ /** Whether this editor has focus */
13
+ isFocused?: boolean;
14
+ /** Maximum height for the list (for scrolling) */
15
+ maxHeight?: number;
16
+ }
17
+ /**
18
+ * LLMProvidersEditor component for editing LLM provider configurations.
19
+ * Provides a user-friendly interface for adding, editing, and removing LLM providers.
20
+ */
21
+ export declare function LLMProvidersEditor({ label, providers, onConfirm, onCancel, isFocused, maxHeight, }: LLMProvidersEditorProps): React.ReactElement;
22
+ export default LLMProvidersEditor;