ralph-cli-sandboxed 0.3.0 → 0.4.0

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 (55) hide show
  1. package/dist/commands/action.d.ts +7 -0
  2. package/dist/commands/action.js +276 -0
  3. package/dist/commands/chat.js +95 -7
  4. package/dist/commands/config.js +6 -18
  5. package/dist/commands/fix-config.d.ts +4 -0
  6. package/dist/commands/fix-config.js +388 -0
  7. package/dist/commands/help.js +17 -0
  8. package/dist/commands/init.js +89 -2
  9. package/dist/commands/listen.js +50 -9
  10. package/dist/commands/prd.js +2 -2
  11. package/dist/config/languages.json +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/providers/telegram.d.ts +6 -2
  14. package/dist/providers/telegram.js +68 -2
  15. package/dist/templates/macos-scripts.d.ts +42 -0
  16. package/dist/templates/macos-scripts.js +448 -0
  17. package/dist/tui/ConfigEditor.d.ts +7 -0
  18. package/dist/tui/ConfigEditor.js +313 -0
  19. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  20. package/dist/tui/components/ArrayEditor.js +193 -0
  21. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  22. package/dist/tui/components/BooleanToggle.js +43 -0
  23. package/dist/tui/components/EditorPanel.d.ts +50 -0
  24. package/dist/tui/components/EditorPanel.js +232 -0
  25. package/dist/tui/components/HelpPanel.d.ts +13 -0
  26. package/dist/tui/components/HelpPanel.js +69 -0
  27. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  28. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  29. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  30. package/dist/tui/components/KeyValueEditor.js +261 -0
  31. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  32. package/dist/tui/components/ObjectEditor.js +227 -0
  33. package/dist/tui/components/PresetSelector.d.ts +23 -0
  34. package/dist/tui/components/PresetSelector.js +58 -0
  35. package/dist/tui/components/Preview.d.ts +18 -0
  36. package/dist/tui/components/Preview.js +190 -0
  37. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  38. package/dist/tui/components/ScrollableContainer.js +77 -0
  39. package/dist/tui/components/SectionNav.d.ts +31 -0
  40. package/dist/tui/components/SectionNav.js +130 -0
  41. package/dist/tui/components/StringEditor.d.ts +21 -0
  42. package/dist/tui/components/StringEditor.js +29 -0
  43. package/dist/tui/hooks/useConfig.d.ts +16 -0
  44. package/dist/tui/hooks/useConfig.js +89 -0
  45. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  46. package/dist/tui/hooks/useTerminalSize.js +48 -0
  47. package/dist/tui/utils/presets.d.ts +52 -0
  48. package/dist/tui/utils/presets.js +191 -0
  49. package/dist/tui/utils/validation.d.ts +49 -0
  50. package/dist/tui/utils/validation.js +198 -0
  51. package/dist/utils/chat-client.d.ts +31 -1
  52. package/dist/utils/chat-client.js +27 -1
  53. package/dist/utils/config.d.ts +7 -2
  54. package/docs/MACOS-DEVELOPMENT.md +435 -0
  55. package/package.json +1 -1
@@ -0,0 +1,380 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect, useMemo } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ /**
6
+ * Parse JSON with detailed error information including line and column.
7
+ */
8
+ function parseJsonWithLineInfo(jsonStr) {
9
+ try {
10
+ const value = JSON.parse(jsonStr);
11
+ return { value };
12
+ }
13
+ catch (err) {
14
+ const message = err instanceof Error ? err.message : "Invalid JSON";
15
+ // Try to extract line/column from error message
16
+ // Format: "... at position N" or "... at line X column Y"
17
+ const posMatch = message.match(/at position (\d+)/);
18
+ const lineColMatch = message.match(/at line (\d+) column (\d+)/);
19
+ if (lineColMatch) {
20
+ return {
21
+ error: {
22
+ message: message.replace(/at line \d+ column \d+/, "").trim(),
23
+ line: parseInt(lineColMatch[1], 10),
24
+ column: parseInt(lineColMatch[2], 10),
25
+ },
26
+ };
27
+ }
28
+ if (posMatch) {
29
+ const position = parseInt(posMatch[1], 10);
30
+ // Calculate line and column from position
31
+ const lines = jsonStr.substring(0, position).split("\n");
32
+ const line = lines.length;
33
+ const column = lines[lines.length - 1].length + 1;
34
+ return {
35
+ error: {
36
+ message: message.replace(/at position \d+/, "").trim(),
37
+ line,
38
+ column,
39
+ },
40
+ };
41
+ }
42
+ return { error: { message } };
43
+ }
44
+ }
45
+ /**
46
+ * Format JSON with indentation for display.
47
+ */
48
+ function formatJson(value) {
49
+ if (value === undefined) {
50
+ return "null";
51
+ }
52
+ return JSON.stringify(value, null, 2);
53
+ }
54
+ /**
55
+ * Validate JSON structure against known config schemas.
56
+ * Returns warnings for common issues.
57
+ */
58
+ function validateJsonStructure(value, label) {
59
+ const warnings = [];
60
+ if (value === null || value === undefined) {
61
+ return warnings;
62
+ }
63
+ // MCP Servers validation
64
+ if (label.toLowerCase().includes("mcp") && typeof value === "object" && !Array.isArray(value)) {
65
+ const servers = value;
66
+ for (const [name, config] of Object.entries(servers)) {
67
+ if (typeof config !== "object" || config === null) {
68
+ warnings.push(`Server "${name}": expected object with command field`);
69
+ continue;
70
+ }
71
+ const serverConfig = config;
72
+ if (!serverConfig.command || typeof serverConfig.command !== "string") {
73
+ warnings.push(`Server "${name}": missing or invalid "command" field`);
74
+ }
75
+ if (serverConfig.args && !Array.isArray(serverConfig.args)) {
76
+ warnings.push(`Server "${name}": "args" should be an array`);
77
+ }
78
+ if (serverConfig.env && (typeof serverConfig.env !== "object" || Array.isArray(serverConfig.env))) {
79
+ warnings.push(`Server "${name}": "env" should be an object`);
80
+ }
81
+ }
82
+ }
83
+ // Daemon Actions validation
84
+ if (label.toLowerCase().includes("action") && typeof value === "object" && !Array.isArray(value)) {
85
+ const actions = value;
86
+ for (const [name, config] of Object.entries(actions)) {
87
+ if (typeof config !== "object" || config === null) {
88
+ warnings.push(`Action "${name}": expected object with command field`);
89
+ continue;
90
+ }
91
+ const actionConfig = config;
92
+ if (!actionConfig.command || typeof actionConfig.command !== "string") {
93
+ warnings.push(`Action "${name}": missing or invalid "command" field`);
94
+ }
95
+ }
96
+ }
97
+ // Skills validation
98
+ if (label.toLowerCase().includes("skill") && Array.isArray(value)) {
99
+ for (let i = 0; i < value.length; i++) {
100
+ const skill = value[i];
101
+ if (typeof skill !== "object" || skill === null) {
102
+ warnings.push(`Skill [${i}]: expected object`);
103
+ continue;
104
+ }
105
+ const skillConfig = skill;
106
+ if (!skillConfig.name || typeof skillConfig.name !== "string") {
107
+ warnings.push(`Skill [${i}]: missing or invalid "name" field`);
108
+ }
109
+ if (!skillConfig.description || typeof skillConfig.description !== "string") {
110
+ warnings.push(`Skill [${i}]: missing or invalid "description" field`);
111
+ }
112
+ if (!skillConfig.instructions || typeof skillConfig.instructions !== "string") {
113
+ warnings.push(`Skill [${i}]: missing or invalid "instructions" field`);
114
+ }
115
+ }
116
+ }
117
+ // Daemon Events validation
118
+ if (label.toLowerCase().includes("event") && typeof value === "object" && !Array.isArray(value)) {
119
+ const validEventTypes = ["task_complete", "ralph_complete", "iteration_complete", "error"];
120
+ const events = value;
121
+ for (const [eventType, handlers] of Object.entries(events)) {
122
+ if (!validEventTypes.includes(eventType)) {
123
+ warnings.push(`Unknown event type: "${eventType}". Valid: ${validEventTypes.join(", ")}`);
124
+ }
125
+ if (!Array.isArray(handlers)) {
126
+ warnings.push(`Event "${eventType}": handlers should be an array`);
127
+ continue;
128
+ }
129
+ for (let i = 0; i < handlers.length; i++) {
130
+ const handler = handlers[i];
131
+ if (typeof handler !== "object" || handler === null) {
132
+ warnings.push(`Event "${eventType}"[${i}]: expected object`);
133
+ continue;
134
+ }
135
+ const eventConfig = handler;
136
+ if (!eventConfig.action || typeof eventConfig.action !== "string") {
137
+ warnings.push(`Event "${eventType}"[${i}]: missing or invalid "action" field`);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return warnings;
143
+ }
144
+ /**
145
+ * Truncate a JSON string value if it's too long.
146
+ * Keeps the quotes and adds "..." before the closing quote.
147
+ */
148
+ function truncateJsonString(str, maxLen) {
149
+ // str includes the surrounding quotes, so we need to account for that
150
+ if (str.length <= maxLen) {
151
+ return str;
152
+ }
153
+ // Truncate the content (excluding quotes) and add ellipsis
154
+ // Leave room for the ellipsis (3 chars) and closing quote
155
+ const truncated = str.slice(0, maxLen - 4) + '..."';
156
+ return truncated;
157
+ }
158
+ /**
159
+ * Simple syntax highlighting for JSON preview.
160
+ */
161
+ function highlightJson(json, maxLines, maxLineWidth = 60) {
162
+ const lines = json.split("\n");
163
+ const displayLines = lines.slice(0, maxLines);
164
+ const hasMore = lines.length > maxLines;
165
+ const elements = [];
166
+ // Calculate max string length based on available width (account for line number, indentation)
167
+ const maxStringLen = Math.max(20, Math.min(50, maxLineWidth - 15));
168
+ for (let i = 0; i < displayLines.length; i++) {
169
+ const line = displayLines[i];
170
+ const lineNum = String(i + 1).padStart(3, " ");
171
+ // Simple tokenization for highlighting
172
+ const tokens = [];
173
+ let remaining = line;
174
+ let tokenKey = 0;
175
+ let lineLength = 0;
176
+ const effectiveMaxWidth = maxLineWidth - 6; // Account for line number prefix "123 "
177
+ while (remaining.length > 0 && lineLength < effectiveMaxWidth) {
178
+ // Match string (key or value)
179
+ const stringMatch = remaining.match(/^("(?:[^"\\]|\\.)*")/);
180
+ if (stringMatch) {
181
+ let str = stringMatch[1];
182
+ // Check if this is a key (followed by :)
183
+ const afterStr = remaining.slice(str.length).trim();
184
+ const isKey = afterStr.startsWith(":");
185
+ // Truncate long string values (not keys)
186
+ if (!isKey && str.length > maxStringLen) {
187
+ str = truncateJsonString(str, maxStringLen);
188
+ }
189
+ tokens.push(_jsx(Text, { color: isKey ? "cyan" : "green", children: str }, tokenKey++));
190
+ lineLength += str.length;
191
+ remaining = remaining.slice(stringMatch[1].length);
192
+ continue;
193
+ }
194
+ // Match number
195
+ const numMatch = remaining.match(/^(-?\d+\.?\d*(?:[eE][+-]?\d+)?)/);
196
+ if (numMatch) {
197
+ tokens.push(_jsx(Text, { color: "yellow", children: numMatch[1] }, tokenKey++));
198
+ lineLength += numMatch[1].length;
199
+ remaining = remaining.slice(numMatch[1].length);
200
+ continue;
201
+ }
202
+ // Match boolean or null
203
+ const boolNullMatch = remaining.match(/^(true|false|null)/);
204
+ if (boolNullMatch) {
205
+ tokens.push(_jsx(Text, { color: "magenta", children: boolNullMatch[1] }, tokenKey++));
206
+ lineLength += boolNullMatch[1].length;
207
+ remaining = remaining.slice(boolNullMatch[1].length);
208
+ continue;
209
+ }
210
+ // Match whitespace or punctuation
211
+ const otherMatch = remaining.match(/^([\s{}[\]:,]+)/);
212
+ if (otherMatch) {
213
+ tokens.push(_jsx(Text, { dimColor: true, children: otherMatch[1] }, tokenKey++));
214
+ lineLength += otherMatch[1].length;
215
+ remaining = remaining.slice(otherMatch[1].length);
216
+ continue;
217
+ }
218
+ // Fallback: single character
219
+ tokens.push(_jsx(Text, { children: remaining[0] }, tokenKey++));
220
+ lineLength += 1;
221
+ remaining = remaining.slice(1);
222
+ }
223
+ // If line was truncated due to length
224
+ if (remaining.length > 0) {
225
+ tokens.push(_jsx(Text, { dimColor: true, children: "..." }, tokenKey++));
226
+ }
227
+ elements.push(_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [lineNum, " "] }), tokens] }, i));
228
+ }
229
+ if (hasMore) {
230
+ elements.push(_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ... (", lines.length - maxLines, " more lines)"] }) }, "more"));
231
+ }
232
+ return elements;
233
+ }
234
+ /**
235
+ * JsonSnippetEditor component for editing complex nested JSON sections.
236
+ * Supports copy/paste and live JSON validation with syntax highlighting.
237
+ * Used for MCP servers, actions, skills, and other complex configs.
238
+ */
239
+ export function JsonSnippetEditor({ label, value, onConfirm, onCancel, isFocused = true, maxHeight = 15, maxWidth = 80, }) {
240
+ // Format the value as JSON for editing
241
+ const initialJson = useMemo(() => formatJson(value), [value]);
242
+ const [mode, setMode] = useState("view");
243
+ const [editText, setEditText] = useState(initialJson);
244
+ const [parseError, setParseError] = useState(null);
245
+ const [warnings, setWarnings] = useState([]);
246
+ const [scrollOffset, setScrollOffset] = useState(0);
247
+ const [copied, setCopied] = useState(false);
248
+ // Validate JSON as user types
249
+ useEffect(() => {
250
+ const result = parseJsonWithLineInfo(editText);
251
+ if (result.error) {
252
+ setParseError(result.error);
253
+ setWarnings([]);
254
+ }
255
+ else {
256
+ setParseError(null);
257
+ const structureWarnings = validateJsonStructure(result.value, label);
258
+ setWarnings(structureWarnings);
259
+ }
260
+ }, [editText, label]);
261
+ // Reset edit text when value changes externally
262
+ useEffect(() => {
263
+ const newJson = formatJson(value);
264
+ if (mode === "view") {
265
+ setEditText(newJson);
266
+ }
267
+ }, [value, mode]);
268
+ // Calculate content lines for scrolling
269
+ const contentLines = useMemo(() => {
270
+ return editText.split("\n").length;
271
+ }, [editText]);
272
+ // Max visible lines for preview (accounting for header, errors, footer)
273
+ const previewMaxLines = Math.max(3, maxHeight - 6);
274
+ // Handle saving
275
+ const handleSave = useCallback(() => {
276
+ const result = parseJsonWithLineInfo(editText);
277
+ if (result.error) {
278
+ // Cannot save with parse errors
279
+ return;
280
+ }
281
+ onConfirm(result.value);
282
+ }, [editText, onConfirm]);
283
+ // Handle entering edit mode
284
+ const handleEdit = useCallback(() => {
285
+ setMode("edit");
286
+ }, []);
287
+ // Handle canceling edit
288
+ const handleCancelEdit = useCallback(() => {
289
+ setEditText(initialJson);
290
+ setMode("view");
291
+ setParseError(null);
292
+ setWarnings([]);
293
+ }, [initialJson]);
294
+ // Handle copying to clipboard
295
+ const handleCopy = useCallback(() => {
296
+ // Use process.stdout.write with OSC 52 escape sequence for clipboard
297
+ // This works in most modern terminals
298
+ const base64 = Buffer.from(editText).toString("base64");
299
+ process.stdout.write(`\x1b]52;c;${base64}\x07`);
300
+ setCopied(true);
301
+ setTimeout(() => setCopied(false), 1500);
302
+ }, [editText]);
303
+ // Handle formatting JSON
304
+ const handleFormat = useCallback(() => {
305
+ const result = parseJsonWithLineInfo(editText);
306
+ if (!result.error && result.value !== undefined) {
307
+ setEditText(formatJson(result.value));
308
+ }
309
+ }, [editText]);
310
+ // Scroll handlers
311
+ const handleScrollUp = useCallback(() => {
312
+ setScrollOffset((prev) => Math.max(0, prev - 1));
313
+ }, []);
314
+ const handleScrollDown = useCallback(() => {
315
+ setScrollOffset((prev) => Math.min(contentLines - previewMaxLines, prev + 1));
316
+ }, [contentLines, previewMaxLines]);
317
+ const handlePageUp = useCallback(() => {
318
+ setScrollOffset((prev) => Math.max(0, prev - previewMaxLines));
319
+ }, [previewMaxLines]);
320
+ const handlePageDown = useCallback(() => {
321
+ setScrollOffset((prev) => Math.min(contentLines - previewMaxLines, prev + previewMaxLines));
322
+ }, [contentLines, previewMaxLines]);
323
+ // Handle keyboard input for view mode
324
+ useInput((input, key) => {
325
+ if (!isFocused || mode !== "view")
326
+ return;
327
+ if (key.return || input === "e") {
328
+ handleEdit();
329
+ }
330
+ else if (input === "s" || input === "S") {
331
+ handleSave();
332
+ }
333
+ else if (key.escape) {
334
+ onCancel();
335
+ }
336
+ else if (input === "c" || input === "C") {
337
+ handleCopy();
338
+ }
339
+ else if (input === "f" || input === "F") {
340
+ handleFormat();
341
+ }
342
+ else if (input === "j" || key.downArrow) {
343
+ handleScrollDown();
344
+ }
345
+ else if (input === "k" || key.upArrow) {
346
+ handleScrollUp();
347
+ }
348
+ else if (key.pageUp) {
349
+ handlePageUp();
350
+ }
351
+ else if (key.pageDown) {
352
+ handlePageDown();
353
+ }
354
+ }, { isActive: isFocused && mode === "view" });
355
+ // Handle keyboard input for edit mode
356
+ useInput((_input, key) => {
357
+ if (!isFocused || mode !== "edit")
358
+ return;
359
+ if (key.escape) {
360
+ handleCancelEdit();
361
+ }
362
+ }, { isActive: isFocused && mode === "edit" });
363
+ // Count errors and warnings for status bar
364
+ const errorCount = parseError ? 1 : 0;
365
+ const warningCount = warnings.length;
366
+ // Render edit mode with text input
367
+ if (mode === "edit") {
368
+ 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 JSON: ", label] }) }), _jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [">", " "] }), _jsx(TextInput, { value: editText, onChange: setEditText, onSubmit: handleSave, focus: isFocused })] }), parseError && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "red", bold: true, children: "Syntax Error:" }), _jsxs(Text, { color: "red", children: [parseError.line && parseError.column
369
+ ? `Line ${parseError.line}, Column ${parseError.column}: `
370
+ : "", parseError.message] })] })), _jsx(Box, { marginTop: 1, children: errorCount > 0 ? (_jsxs(Text, { color: "red", children: [errorCount, " error", errorCount > 1 ? "s" : ""] })) : warningCount > 0 ? (_jsxs(Text, { color: "yellow", children: [warningCount, " warning", warningCount > 1 ? "s" : ""] })) : (_jsx(Text, { color: "green", children: "Valid JSON" })) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save (if valid) | Esc: cancel" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Tip: Paste multi-line JSON, then press Enter" }) })] }));
371
+ }
372
+ // Render view mode with syntax-highlighted preview
373
+ // Account for border (2) and padding (2) when calculating preview width
374
+ const previewWidth = Math.max(40, maxWidth - 4);
375
+ 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
377
+ ? `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" })] })] }));
379
+ }
380
+ export default JsonSnippetEditor;
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ /**
3
+ * Provider-specific hint configuration.
4
+ * Shows common keys and their descriptions based on the notification provider.
5
+ */
6
+ export interface ProviderHint {
7
+ key: string;
8
+ description: string;
9
+ required?: boolean;
10
+ }
11
+ /**
12
+ * Built-in hints for known notification providers.
13
+ */
14
+ export declare const PROVIDER_HINTS: Record<string, ProviderHint[]>;
15
+ export interface KeyValueEditorProps {
16
+ /** The label to display for this object field */
17
+ label: string;
18
+ /** The current object entries as key-value pairs */
19
+ entries: Record<string, string>;
20
+ /** Called when the user confirms the edit */
21
+ onConfirm: (newEntries: Record<string, string>) => void;
22
+ /** Called when the user cancels the edit (Esc) */
23
+ onCancel: () => void;
24
+ /** Whether this editor has focus */
25
+ isFocused?: boolean;
26
+ /** Provider name for showing hints (e.g., "ntfy", "pushover", "gotify") */
27
+ providerName?: string;
28
+ }
29
+ /**
30
+ * KeyValueEditor component for editing key-value pairs with provider-specific hints.
31
+ * Enhanced version of ObjectEditor with support for common key suggestions.
32
+ */
33
+ export declare function KeyValueEditor({ label, entries, onConfirm, onCancel, isFocused, providerName, }: KeyValueEditorProps): React.ReactElement;
34
+ export default KeyValueEditor;