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
package/dist/tui/ConfigEditor.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
285
|
-
|
|
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 &&
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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") &&
|
|
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
|
|
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 &&
|
|
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") &&
|
|
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: ["
|
|
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 &&
|
|
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;
|