rl-rockcli 0.0.8 → 0.0.10

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 (162) hide show
  1. package/commands/attach/basic-repl.js +212 -0
  2. package/commands/attach/cleanup-history.js +189 -0
  3. package/commands/attach/cleanup-manager.js +163 -0
  4. package/commands/attach/copy-ui/copyRepl.js +195 -0
  5. package/commands/attach/copy-ui/index.js +7 -0
  6. package/commands/attach/copy-ui/render/outputBlock.js +25 -0
  7. package/commands/attach/copy-ui/viewport/viewport.js +23 -0
  8. package/commands/attach/copy-ui/viewport/wheel.js +14 -0
  9. package/commands/attach/history-manager.js +507 -0
  10. package/commands/attach/history-session.js +48 -0
  11. package/commands/attach/ink-repl/InkREPL.js +1507 -0
  12. package/commands/attach/ink-repl/builtinCommands.js +1253 -0
  13. package/commands/attach/ink-repl/components/ConnectingScreen.js +76 -0
  14. package/commands/attach/ink-repl/components/Console.js +191 -0
  15. package/commands/attach/ink-repl/components/DetailView.js +148 -0
  16. package/commands/attach/ink-repl/components/DropdownMenu.js +86 -0
  17. package/commands/attach/ink-repl/components/InputArea.js +125 -0
  18. package/commands/attach/ink-repl/components/InputLine.js +18 -0
  19. package/commands/attach/ink-repl/components/OutputArea.js +22 -0
  20. package/commands/attach/ink-repl/components/OutputItem.js +96 -0
  21. package/commands/attach/ink-repl/components/ShellLayout.js +61 -0
  22. package/commands/attach/ink-repl/components/Spinner.js +79 -0
  23. package/commands/attach/ink-repl/components/StatusBar.js +106 -0
  24. package/commands/attach/ink-repl/components/WelcomeBanner.js +48 -0
  25. package/commands/attach/ink-repl/contexts/LayoutContext.js +12 -0
  26. package/commands/attach/ink-repl/contexts/ThemeContext.js +43 -0
  27. package/commands/attach/ink-repl/hooks/useFunctionKeys.js +70 -0
  28. package/commands/attach/ink-repl/hooks/useMouse.js +162 -0
  29. package/commands/attach/ink-repl/hooks/useResources.js +132 -0
  30. package/commands/attach/ink-repl/hooks/useSpinner.js +49 -0
  31. package/commands/attach/ink-repl/index.js +112 -0
  32. package/commands/attach/ink-repl/package.json +3 -0
  33. package/commands/attach/ink-repl/replState.js +947 -0
  34. package/commands/attach/ink-repl/shortcuts/defaultKeybindings.js +138 -0
  35. package/commands/attach/ink-repl/shortcuts/index.js +332 -0
  36. package/commands/attach/ink-repl/themes/defaultDark.js +18 -0
  37. package/commands/attach/ink-repl/themes/defaultLight.js +18 -0
  38. package/commands/attach/ink-repl/themes/index.js +4 -0
  39. package/commands/attach/ink-repl/themes/themeManager.js +45 -0
  40. package/commands/attach/ink-repl/themes/themeTokens.js +15 -0
  41. package/commands/attach/ink-repl/utils/atCompletion.js +346 -0
  42. package/commands/attach/ink-repl/utils/clipboard.js +50 -0
  43. package/commands/attach/ink-repl/utils/consoleLogger.js +81 -0
  44. package/commands/attach/ink-repl/utils/exitCodeHandler.js +49 -0
  45. package/commands/attach/ink-repl/utils/exitCodeTips.js +56 -0
  46. package/commands/attach/ink-repl/utils/formatTime.js +12 -0
  47. package/commands/attach/ink-repl/utils/outputSelection.js +120 -0
  48. package/commands/attach/ink-repl/utils/outputViewport.js +77 -0
  49. package/commands/attach/ink-repl/utils/paginatedFileLoading.js +76 -0
  50. package/commands/attach/ink-repl/utils/paramHint.js +60 -0
  51. package/commands/attach/ink-repl/utils/parseError.js +174 -0
  52. package/commands/attach/ink-repl/utils/pathCompletion.js +167 -0
  53. package/commands/attach/ink-repl/utils/remotePathSafety.js +56 -0
  54. package/commands/attach/ink-repl/utils/replSelection.js +205 -0
  55. package/commands/attach/ink-repl/utils/responseFormatter.js +127 -0
  56. package/commands/attach/ink-repl/utils/textWrap.js +117 -0
  57. package/commands/attach/ink-repl/utils/truncate.js +115 -0
  58. package/commands/attach/opentui-repl/App.tsx +891 -0
  59. package/commands/attach/opentui-repl/builtinCommands.ts +80 -0
  60. package/commands/attach/opentui-repl/components/ConfirmDialog.tsx +116 -0
  61. package/commands/attach/opentui-repl/components/ConnectingScreen.tsx +131 -0
  62. package/commands/attach/opentui-repl/components/Console.tsx +73 -0
  63. package/commands/attach/opentui-repl/components/DetailView.tsx +45 -0
  64. package/commands/attach/opentui-repl/components/DropdownMenu.tsx +130 -0
  65. package/commands/attach/opentui-repl/components/ExecutionStatus.tsx +66 -0
  66. package/commands/attach/opentui-repl/components/Header.tsx +24 -0
  67. package/commands/attach/opentui-repl/components/OutputArea.tsx +25 -0
  68. package/commands/attach/opentui-repl/components/OutputBlock.tsx +108 -0
  69. package/commands/attach/opentui-repl/components/PromptInput.tsx +109 -0
  70. package/commands/attach/opentui-repl/components/StatusBar.tsx +63 -0
  71. package/commands/attach/opentui-repl/components/Toast.tsx +65 -0
  72. package/commands/attach/opentui-repl/components/WelcomeBanner.tsx +41 -0
  73. package/commands/attach/opentui-repl/contexts/ReplContext.tsx +137 -0
  74. package/commands/attach/opentui-repl/contexts/SessionContext.tsx +32 -0
  75. package/commands/attach/opentui-repl/contexts/ThemeContext.tsx +70 -0
  76. package/commands/attach/opentui-repl/contexts/ToastContext.tsx +69 -0
  77. package/commands/attach/opentui-repl/contexts/toast-logic.js +71 -0
  78. package/commands/attach/opentui-repl/hooks/useResources.ts +102 -0
  79. package/commands/attach/opentui-repl/hooks/useSpinner.ts +46 -0
  80. package/commands/attach/opentui-repl/index.js +99 -0
  81. package/commands/attach/opentui-repl/keybindings.ts +39 -0
  82. package/commands/attach/opentui-repl/package.json +3 -0
  83. package/commands/attach/opentui-repl/render.tsx +72 -0
  84. package/commands/attach/opentui-repl/tsconfig.json +12 -0
  85. package/commands/attach/repl.js +791 -0
  86. package/commands/attach/sandbox-id-resolver.js +56 -0
  87. package/commands/attach/session-manager.js +307 -0
  88. package/commands/attach/ui-mode.js +146 -0
  89. package/commands/log/core/constants.js +237 -0
  90. package/commands/log/core/display.js +370 -0
  91. package/commands/log/core/search.js +330 -0
  92. package/commands/log/core/tail.js +216 -0
  93. package/commands/log/core/utils.js +424 -0
  94. package/commands/log.js +298 -0
  95. package/commands/sandbox/core/log-bridge.js +119 -0
  96. package/commands/sandbox/core/replay/analyzer.js +311 -0
  97. package/commands/sandbox/core/replay/batch-orchestrator.js +536 -0
  98. package/commands/sandbox/core/replay/batch-task.js +369 -0
  99. package/commands/sandbox/core/replay/concurrent-display.js +70 -0
  100. package/commands/sandbox/core/replay/concurrent-orchestrator.js +170 -0
  101. package/commands/sandbox/core/replay/data-source.js +86 -0
  102. package/commands/sandbox/core/replay/display.js +231 -0
  103. package/commands/sandbox/core/replay/executor.js +634 -0
  104. package/commands/sandbox/core/replay/history-fetcher.js +124 -0
  105. package/commands/sandbox/core/replay/index.js +338 -0
  106. package/commands/sandbox/core/replay/loghouse-data-source.js +177 -0
  107. package/commands/sandbox/core/replay/pid-mapping.js +26 -0
  108. package/commands/sandbox/core/replay/request.js +109 -0
  109. package/commands/sandbox/core/replay/worker.js +166 -0
  110. package/commands/sandbox/core/session.js +346 -0
  111. package/commands/sandbox/log-bridge.js +2 -0
  112. package/commands/sandbox/ray.js +2 -0
  113. package/commands/sandbox/replay/analyzer.js +311 -0
  114. package/commands/sandbox/replay/batch-orchestrator.js +536 -0
  115. package/commands/sandbox/replay/batch-task.js +369 -0
  116. package/commands/sandbox/replay/concurrent-display.js +70 -0
  117. package/commands/sandbox/replay/concurrent-orchestrator.js +170 -0
  118. package/commands/sandbox/replay/display.js +231 -0
  119. package/commands/sandbox/replay/executor.js +634 -0
  120. package/commands/sandbox/replay/history-fetcher.js +118 -0
  121. package/commands/sandbox/replay/index.js +338 -0
  122. package/commands/sandbox/replay/pid-mapping.js +26 -0
  123. package/commands/sandbox/replay/request.js +109 -0
  124. package/commands/sandbox/replay/worker.js +166 -0
  125. package/commands/sandbox/replay.js +2 -0
  126. package/commands/sandbox/session.js +2 -0
  127. package/commands/sandbox-original.js +1393 -0
  128. package/commands/sandbox.js +499 -0
  129. package/help/help.json +1071 -0
  130. package/help/middleware.js +71 -0
  131. package/help/renderer.js +800 -0
  132. package/index.js +5 -15
  133. package/lib/plugin-context.js +40 -0
  134. package/package.json +2 -2
  135. package/sdks/sandbox/core/client.js +845 -0
  136. package/sdks/sandbox/core/config.js +70 -0
  137. package/sdks/sandbox/core/types.js +74 -0
  138. package/sdks/sandbox/httpLogger.js +251 -0
  139. package/sdks/sandbox/index.js +9 -0
  140. package/utils/asciiArt.js +138 -0
  141. package/utils/bun-compat.js +59 -0
  142. package/utils/ciPipelines.js +138 -0
  143. package/utils/cli.js +17 -0
  144. package/utils/command-router.js +79 -0
  145. package/utils/configManager.js +503 -0
  146. package/utils/dependency-resolver.js +135 -0
  147. package/utils/eagleeye_traceid.js +151 -0
  148. package/utils/envDetector.js +78 -0
  149. package/utils/execution_logger.js +415 -0
  150. package/utils/featureManager.js +68 -0
  151. package/utils/firstTimeTip.js +44 -0
  152. package/utils/hook-manager.js +125 -0
  153. package/utils/http-logger.js +264 -0
  154. package/utils/i18n.js +139 -0
  155. package/utils/image-progress.js +159 -0
  156. package/utils/logger.js +154 -0
  157. package/utils/plugin-loader.js +124 -0
  158. package/utils/plugin-manager.js +348 -0
  159. package/utils/ray_cli_wrapper.js +746 -0
  160. package/utils/sandbox-client.js +419 -0
  161. package/utils/terminal.js +32 -0
  162. package/utils/tips.js +106 -0
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Output selection utilities
3
+ * Handles text extraction from output items, filtering out borders and padding
4
+ */
5
+
6
+ /**
7
+ * Build a line map for outputs, tracking which lines belong to which output
8
+ * and filtering out border characters
9
+ * @param {Array} outputs - Array of output items
10
+ * @param {string} shellPrompt - Current shell prompt
11
+ * @returns {Object} { lines: string[], lineMap: Array<{outputId, lineType, originalLine}> }
12
+ */
13
+ export function buildOutputLineMap(outputs, shellPrompt) {
14
+ const lines = [];
15
+ const lineMap = [];
16
+
17
+ for (const output of outputs) {
18
+ const { id, command, output: text, exitCode, isWelcome } = output;
19
+
20
+ if (isWelcome) {
21
+ // Welcome message - single line, no border filtering needed
22
+ lines.push(text);
23
+ lineMap.push({ outputId: id, lineType: 'content', originalLine: text });
24
+ continue;
25
+ }
26
+
27
+ // Command line: prompt + command
28
+ const commandLine = `${shellPrompt}${command}`;
29
+ lines.push(commandLine);
30
+ lineMap.push({ outputId: id, lineType: 'command', originalLine: commandLine });
31
+
32
+ // Output text - split by newlines
33
+ if (text) {
34
+ const outputLines = text.split('\n');
35
+ for (const line of outputLines) {
36
+ lines.push(line);
37
+ lineMap.push({ outputId: id, lineType: 'output', originalLine: line });
38
+ }
39
+ }
40
+ }
41
+
42
+ return { lines, lineMap };
43
+ }
44
+
45
+ /**
46
+ * Filter border characters from selected text
47
+ * Removes box drawing characters commonly used for borders
48
+ * @param {string} text - Selected text
49
+ * @returns {string} Filtered text
50
+ */
51
+ export function filterBorderCharacters(text) {
52
+ // Box drawing characters used by Ink borders
53
+ const borderChars = /[─│┌┐└┘├┤┬┴┼╭╮╰╯═║╔╗╚╝╠╣╦╩╬]/g;
54
+
55
+ return text
56
+ .split('\n')
57
+ .map(line => {
58
+ // Remove border characters
59
+ let cleaned = line.replace(borderChars, '');
60
+ // Trim excessive whitespace but preserve meaningful indentation
61
+ return cleaned.trimEnd();
62
+ })
63
+ .filter(line => line.length > 0) // Remove empty lines that were just borders
64
+ .join('\n');
65
+ }
66
+
67
+ /**
68
+ * Extract text from a screen region, considering the rendered layout
69
+ * This function is aware of the OutputItem layout with borders
70
+ * @param {number} startX - Start X coordinate (1-based)
71
+ * @param {number} startY - Start Y coordinate (1-based)
72
+ * @param {number} endX - End X coordinate (1-based)
73
+ * @param {number} endY - End Y coordinate (1-based)
74
+ * @param {Array} outputs - Array of output items
75
+ * @param {string} shellPrompt - Current shell prompt
76
+ * @returns {string} Selected text with borders filtered
77
+ */
78
+ export function extractOutputText(startX, startY, endX, endY, outputs, shellPrompt) {
79
+ const { lines } = buildOutputLineMap(outputs, shellPrompt);
80
+
81
+ if (!lines || lines.length === 0) return '';
82
+
83
+ // Normalize coordinates (ensure start <= end)
84
+ if (startY > endY || (startY === endY && startX > endX)) {
85
+ [startX, endX] = [endX, startX];
86
+ [startY, endY] = [endY, startY];
87
+ }
88
+
89
+ // Convert 1-based to 0-based
90
+ const startRow = Math.max(0, startY - 1);
91
+ const endRow = Math.min(lines.length - 1, endY - 1);
92
+ const startCol = Math.max(0, startX - 1);
93
+ const endCol = Math.max(0, endX - 1);
94
+
95
+ if (startRow > endRow) return '';
96
+ if (startRow === endRow && startCol > endCol) return '';
97
+
98
+ const selectedLines = [];
99
+
100
+ for (let row = startRow; row <= endRow; row++) {
101
+ const line = lines[row] || '';
102
+
103
+ if (row === startRow && row === endRow) {
104
+ // Single line selection
105
+ selectedLines.push(line.substring(startCol, endCol));
106
+ } else if (row === startRow) {
107
+ // First line of multi-line selection
108
+ selectedLines.push(line.substring(startCol));
109
+ } else if (row === endRow) {
110
+ // Last line of multi-line selection
111
+ selectedLines.push(line.substring(0, endCol));
112
+ } else {
113
+ // Middle lines - select entire line
114
+ selectedLines.push(line);
115
+ }
116
+ }
117
+
118
+ const rawText = selectedLines.join('\n');
119
+ return filterBorderCharacters(rawText);
120
+ }
@@ -0,0 +1,77 @@
1
+ import { truncateOutputToLines } from './truncate.js';
2
+
3
+ /**
4
+ * Estimate the rendered height (rows) of an OutputItem card in the OutputArea.
5
+ *
6
+ * This mirrors the structure in `components/OutputItem.js`:
7
+ * - Welcome item: 1 row (best-effort; welcome is rare after first render)
8
+ * - Normal item: top border + command line + (N output lines) + (optional hint) + bottom border
9
+ *
10
+ * Note: OutputArea applies `rowGap: 1` between items, which is handled by the caller.
11
+ *
12
+ * @param {Object} item
13
+ * @param {number} contentWidth Outer card width
14
+ * @param {number} maxLines Max visual output lines to show
15
+ * @returns {number}
16
+ */
17
+ export function estimateOutputItemHeight(item, contentWidth, maxLines = 50) {
18
+ if (!item) return 0;
19
+ if (item.isWelcome) {
20
+ return 1;
21
+ }
22
+
23
+ const boxInnerWidth = Math.max(10, contentWidth - 4);
24
+ const hasOutput = typeof item.output === 'string' ? item.output.length > 0 : !!item.output;
25
+ const { lines, truncated, hiddenLines } = hasOutput
26
+ ? truncateOutputToLines(String(item.output), boxInnerWidth, maxLines)
27
+ : { lines: [], truncated: false, hiddenLines: 0 };
28
+
29
+ const outputLineCount = lines.length;
30
+ const hintLineCount = truncated && hiddenLines > 0 ? 1 : 0;
31
+
32
+ // borders(2) + command line(1) + output lines + hint line (optional)
33
+ return 3 + outputLineCount + hintLineCount;
34
+ }
35
+
36
+ /**
37
+ * Pick the tail of outputs that best fits within a target height.
38
+ * Always returns at least 1 item if outputs is non-empty.
39
+ *
40
+ * @param {Array<Object>} outputs
41
+ * @param {Object} options
42
+ * @param {number} options.contentWidth Outer card width
43
+ * @param {number} options.maxLines Max visual output lines to show per card
44
+ * @param {number} options.maxHeight Max rows available for cards (excluding fixed UI)
45
+ * @param {number} [options.rowGap=1] Gap rows between cards
46
+ * @returns {Array<Object>}
47
+ */
48
+ export function pickVisibleOutputsByHeight(
49
+ outputs,
50
+ { contentWidth, maxLines, maxHeight, rowGap = 1 } = {},
51
+ ) {
52
+ const list = Array.isArray(outputs) ? outputs : [];
53
+ if (list.length === 0) return [];
54
+
55
+ const heightLimit = Math.max(1, Number(maxHeight || 1));
56
+ let used = 0;
57
+ let start = list.length;
58
+
59
+ for (let i = list.length - 1; i >= 0; i--) {
60
+ const itemHeight = estimateOutputItemHeight(list[i], contentWidth, maxLines);
61
+ const needed = used === 0 ? itemHeight : itemHeight + rowGap;
62
+
63
+ // Always include at least one (the most recent) item.
64
+ if (used > 0 && used + needed > heightLimit) {
65
+ break;
66
+ }
67
+
68
+ used += needed;
69
+ start = i;
70
+
71
+ if (used >= heightLimit) {
72
+ break;
73
+ }
74
+ }
75
+
76
+ return list.slice(start);
77
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Utilities for paginated file loading
3
+ */
4
+
5
+ /**
6
+ * Extract file path from cat command
7
+ * @param {string} command - Shell command
8
+ * @returns {string|null} - File path or null
9
+ */
10
+ export function extractCatFilePath(command) {
11
+ const trimmed = command.trim();
12
+ const match = trimmed.match(/^cat\s+(.+)$/);
13
+ return match ? match[1].trim() : null;
14
+ }
15
+
16
+ /**
17
+ * Check if command should enable pagination
18
+ * @param {string} command - Shell command
19
+ * @returns {boolean}
20
+ */
21
+ export function shouldEnablePagination(command) {
22
+ const trimmed = command.trim();
23
+ // Must be pure cat command (no pipes, no -n option)
24
+ return /^cat\s+[^\|]+$/.test(trimmed) && !/-n\s+/.test(trimmed);
25
+ }
26
+
27
+ /**
28
+ * Calculate line range for previous page (prepend)
29
+ * @param {number} currentStartLine
30
+ * @param {number} pageSize
31
+ * @returns {{newStartLine: number, newEndLine: number}}
32
+ */
33
+ export function calculatePreviousPageRange(currentStartLine, pageSize) {
34
+ const newStartLine = Math.max(1, currentStartLine - pageSize);
35
+ const newEndLine = currentStartLine - 1;
36
+ return { newStartLine, newEndLine };
37
+ }
38
+
39
+ /**
40
+ * Calculate line range for next page (append)
41
+ * @param {number} currentEndLine
42
+ * @param {number} totalLines
43
+ * @param {number} pageSize
44
+ * @returns {{newStartLine: number, newEndLine: number}}
45
+ */
46
+ export function calculateNextPageRange(currentEndLine, totalLines, pageSize) {
47
+ const newStartLine = currentEndLine + 1;
48
+ const newEndLine = Math.min(totalLines, currentEndLine + pageSize);
49
+ return { newStartLine, newEndLine };
50
+ }
51
+
52
+ /**
53
+ * Check if should trigger previous page load (near top)
54
+ * @param {number} scrollOffset - Current scroll position
55
+ * @param {number} threshold - Lines from top to trigger
56
+ * @param {number} loadedStartLine - Currently loaded start line
57
+ * @returns {boolean}
58
+ */
59
+ export function shouldLoadPreviousPage(scrollOffset, threshold, loadedStartLine) {
60
+ return scrollOffset < threshold && loadedStartLine > 1;
61
+ }
62
+
63
+ /**
64
+ * Check if should trigger next page load (near bottom)
65
+ * @param {number} scrollOffset - Current scroll position
66
+ * @param {number} totalVisibleLines - Total lines in content
67
+ * @param {number} viewportHeight - Visible height in lines
68
+ * @param {number} threshold - Lines from bottom to trigger
69
+ * @param {number} loadedEndLine - Currently loaded end line
70
+ * @param {number} totalLines - Total lines in file
71
+ * @returns {boolean}
72
+ */
73
+ export function shouldLoadNextPage(scrollOffset, totalVisibleLines, viewportHeight, threshold, loadedEndLine, totalLines) {
74
+ const distanceFromBottom = totalVisibleLines - scrollOffset - viewportHeight;
75
+ return distanceFromBottom < threshold && loadedEndLine < totalLines;
76
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Parameter hint calculation for inline placeholder display
3
+ *
4
+ * Shows placeholder text (e.g., "@<local> @<remote>") when user is typing
5
+ * /upload or /download commands to help them remember the parameter format.
6
+ */
7
+
8
+ import { COMMAND_PARAM_TEMPLATES } from '../builtinCommands.js';
9
+
10
+ /**
11
+ * Calculate placeholder text to display
12
+ *
13
+ * Key logic:
14
+ * 1. Cursor must be at end of buffer (FR-006)
15
+ * 2. Command must be /upload or /download with space after
16
+ * 3. Show remaining @ parameters based on how many have been started
17
+ *
18
+ * @param {string} buffer - Full input buffer
19
+ * @param {number} cursorPosition - Current cursor position
20
+ * @returns {string|null} Placeholder text or null if not applicable
21
+ */
22
+ export function getPlaceholderText(buffer, cursorPosition) {
23
+ // FR-006: Cursor must be at end of buffer
24
+ if (cursorPosition !== buffer.length) {
25
+ return null;
26
+ }
27
+
28
+ // Parse command - must have space after command name
29
+ const match = buffer.match(/^\/(upload|download)\s+/);
30
+ if (!match) {
31
+ // No space after command = don't show placeholder (might be typing other command)
32
+ return null;
33
+ }
34
+
35
+ const command = match[1];
36
+ const template = COMMAND_PARAM_TEMPLATES[command];
37
+ if (!template) {
38
+ return null;
39
+ }
40
+
41
+ // Get input after command (including the space)
42
+ const afterCommand = buffer.slice(match[0].length);
43
+
44
+ // Count @ symbols that have been started
45
+ let startedParams = 0;
46
+ for (let i = 0; i < afterCommand.length; i++) {
47
+ if (afterCommand[i] === '@') {
48
+ startedParams++;
49
+ }
50
+ }
51
+
52
+ // If all params started, no placeholder needed
53
+ if (startedParams >= template.params.length) {
54
+ return null;
55
+ }
56
+
57
+ // Show remaining params
58
+ const remainingParams = template.params.slice(startedParams);
59
+ return remainingParams.map(p => `${p.prefix}${p.placeholder}`).join(' ');
60
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 底层错误消息友好化映射表
3
+ * 将底层技术错误转换为用户友好的提示信息
4
+ */
5
+ const ERROR_FRIENDLY_MAP = {
6
+ // Bun/运行时兼容性问题
7
+ 'First argument must be an Error object': '网络连接异常,请检查网络后重试',
8
+
9
+ // Node 网络错误
10
+ 'ECONNREFUSED': '无法连接到服务器',
11
+ 'ETIMEDOUT': '请求超时,请重试',
12
+ 'ENOTFOUND': '无法解析服务器地址',
13
+ 'ECONNRESET': '网络连接已断开',
14
+ 'EPIPE': '连接已关闭',
15
+ 'EHOSTUNREACH': '无法访问服务器',
16
+ 'ENETUNREACH': '网络不可达',
17
+
18
+ // HTTP 错误
19
+ 'socket hang up': '网络连接已断开',
20
+ 'network error': '网络异常',
21
+ 'Network Error': '网络异常',
22
+ 'request aborted': '请求被中断',
23
+ 'canceled': '请求已取消',
24
+
25
+ // SSL/TLS 错误
26
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE': 'SSL证书验证失败',
27
+ 'CERT_HAS_EXPIRED': 'SSL证书已过期',
28
+ 'DEPTH_ZERO_SELF_SIGNED_CERT': 'SSL证书不受信任',
29
+ };
30
+
31
+ /**
32
+ * 错误消息友好化转换
33
+ * 将底层技术错误转换为用户友好的提示
34
+ *
35
+ * @param {string} message - 原始错误消息
36
+ * @returns {string} 友好化后的错误消息
37
+ */
38
+ export function friendlyErrorMessage(message) {
39
+ if (!message || typeof message !== 'string') {
40
+ return '未知错误';
41
+ }
42
+
43
+ // 1. 精确匹配
44
+ if (ERROR_FRIENDLY_MAP[message]) {
45
+ return ERROR_FRIENDLY_MAP[message];
46
+ }
47
+
48
+ // 2. 部分匹配(处理包含错误码的消息)
49
+ for (const [key, friendly] of Object.entries(ERROR_FRIENDLY_MAP)) {
50
+ if (message.includes(key)) {
51
+ return friendly;
52
+ }
53
+ }
54
+
55
+ // 3. HTTP 状态码错误处理
56
+ const statusMatch = message.match(/status code (\d+)/i);
57
+ if (statusMatch) {
58
+ const code = parseInt(statusMatch[1], 10);
59
+ if (code >= 500) {
60
+ return '服务暂时不可用,请稍后重试';
61
+ }
62
+ if (code === 401 || code === 403) {
63
+ return '认证失败,请检查登录状态';
64
+ }
65
+ if (code === 404) {
66
+ return '请求的资源不存在';
67
+ }
68
+ if (code === 429) {
69
+ return '请求过于频繁,请稍后重试';
70
+ }
71
+ return `请求失败 (${code})`;
72
+ }
73
+
74
+ // 4. JSON 解析错误
75
+ if (message.includes('Unexpected token') || message.includes('JSON')) {
76
+ return '服务器响应格式异常';
77
+ }
78
+
79
+ // 5. 超时相关
80
+ if (message.toLowerCase().includes('timeout') || message.includes('超时')) {
81
+ return '请求超时,请重试';
82
+ }
83
+
84
+ // 6. 网络相关关键词
85
+ const networkKeywords = ['network', 'socket', 'connection', 'connect', '网络'];
86
+ const lowerMessage = message.toLowerCase();
87
+ if (networkKeywords.some(kw => lowerMessage.includes(kw))) {
88
+ return '网络连接异常,请检查网络后重试';
89
+ }
90
+
91
+ // 7. 返回原始消息(如果已经是用户友好的)
92
+ return message;
93
+ }
94
+
95
+ /**
96
+ * Parse execution error to extract meaningful output
97
+ *
98
+ * Handles errors like:
99
+ * Error: Failed to run in session: {"status":"Failed","error":"Command 'aaa' failed with exit code 127. Here is the output:\n'bash: aaa: command not found'","result":null}
100
+ *
101
+ * Exit codes:
102
+ * - 0: Success
103
+ * - 1-255: Command exit codes
104
+ * - -1: Timeout/Network/Session errors (returned by SDK, not parsed from error message)
105
+ *
106
+ * @param {Error} error - The error object
107
+ * @returns {{ output: string, exitCode: number }}
108
+ */
109
+ export function parseExecutionError(error) {
110
+ const message = error?.message || '';
111
+
112
+ if (!message) {
113
+ return { output: 'Unknown error', exitCode: 1 };
114
+ }
115
+
116
+ // Try to extract JSON from the error message first
117
+ const jsonMatch = message.match(/\{[\s\S]*\}/);
118
+
119
+ if (jsonMatch) {
120
+ try {
121
+ const parsed = JSON.parse(jsonMatch[0]);
122
+
123
+ if (parsed.error) {
124
+ // Extract exit code from error message
125
+ const exitCodeMatch = parsed.error.match(/exit code (\d+)/);
126
+ const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
127
+
128
+ // Extract the actual shell output from the error
129
+ // Pattern 1: "Here is the output:\n'actual message'"
130
+ let outputMatch = parsed.error.match(/Here is the output:\s*\n'(.+?)'/s);
131
+
132
+ if (outputMatch) {
133
+ // Extract just the command output, remove extra formatting
134
+ let output = outputMatch[1];
135
+
136
+ // If output contains "---- Stderr ----" sections, simplify it
137
+ const stderrMatch = output.match(/---- Stderr ----\s*([\s\S]*?)(?:---- Stdout ----)?/);
138
+ if (stderrMatch && stderrMatch[1].trim()) {
139
+ // Only show stderr content for syntax errors
140
+ output = stderrMatch[1].trim();
141
+ }
142
+
143
+ return {
144
+ output,
145
+ exitCode,
146
+ };
147
+ }
148
+
149
+ // Pattern 2: Extract just the bash error message
150
+ const bashErrorMatch = parsed.error.match(/bash: .+/);
151
+ if (bashErrorMatch) {
152
+ return {
153
+ output: bashErrorMatch[0],
154
+ exitCode,
155
+ };
156
+ }
157
+
158
+ // Fallback: use the error field directly
159
+ return {
160
+ output: parsed.error,
161
+ exitCode,
162
+ };
163
+ }
164
+ } catch (e) {
165
+ // JSON parsing failed, fall through to default handling
166
+ }
167
+ }
168
+
169
+ // 对底层错误消息进行友好化转换
170
+ return {
171
+ output: friendlyErrorMessage(message),
172
+ exitCode: 1,
173
+ };
174
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Path Completion Utilities
3
+ * Provides path auto-completion for shell commands in the REPL
4
+ */
5
+
6
+ import { hasDangerousShellChars, shellEscapePosix } from './remotePathSafety.js';
7
+
8
+ /**
9
+ * Commands that typically take path arguments
10
+ */
11
+ export const PATH_COMMANDS = [
12
+ 'cd', 'cat', 'ls', 'less', 'more', 'head', 'tail',
13
+ 'vi', 'vim', 'nano', 'emacs',
14
+ 'rm', 'cp', 'mv', 'mkdir', 'rmdir', 'touch',
15
+ 'chmod', 'chown', 'chgrp',
16
+ 'source', '.',
17
+ 'python', 'python3', 'node', 'bash', 'sh',
18
+ 'file', 'stat', 'du', 'find',
19
+ ];
20
+
21
+ /**
22
+ * Check if a command typically takes path arguments
23
+ * @param {string} cmd - Command name
24
+ * @returns {boolean}
25
+ */
26
+ export function isPathCommand(cmd) {
27
+ if (!cmd) return false;
28
+ return PATH_COMMANDS.includes(cmd);
29
+ }
30
+
31
+ /**
32
+ * Parse input to extract path completion context
33
+ * @param {string} input - Current input buffer
34
+ * @returns {Object} Parsed path info
35
+ */
36
+ export function parsePathInput(input) {
37
+ if (!input || !input.trim()) {
38
+ return { needsCompletion: false };
39
+ }
40
+
41
+ // Check if input ends with whitespace (user wants to complete from current dir)
42
+ const endsWithSpace = /\s$/.test(input);
43
+ const parts = input.split(/\s+/).filter(Boolean);
44
+ const command = parts[0];
45
+
46
+ // Skip path completion for built-in commands (starting with /)
47
+ // These commands use @ for path completion instead
48
+ if (command && command.startsWith('/')) {
49
+ return { needsCompletion: false };
50
+ }
51
+
52
+ // Handle trailing space case: "cd " should trigger current directory completion
53
+ if (endsWithSpace && isPathCommand(command)) {
54
+ return {
55
+ command,
56
+ prefix: input,
57
+ dirPath: './',
58
+ filePrefix: '',
59
+ needsCompletion: true,
60
+ };
61
+ }
62
+
63
+ const lastPart = parts[parts.length - 1] || '';
64
+
65
+ // No argument to complete
66
+ if (parts.length < 2 || !lastPart) {
67
+ return { needsCompletion: false };
68
+ }
69
+
70
+ // Check if completion is needed
71
+ const looksLikePath = lastPart.includes('/') || lastPart.startsWith('.') || lastPart.startsWith('~');
72
+ const isPathCmd = isPathCommand(command);
73
+
74
+ if (!looksLikePath && !isPathCmd) {
75
+ return { needsCompletion: false };
76
+ }
77
+
78
+ // Parse the path
79
+ let dirPath, filePrefix;
80
+
81
+ if (lastPart.endsWith('/')) {
82
+ dirPath = lastPart;
83
+ filePrefix = '';
84
+ } else if (lastPart.includes('/')) {
85
+ const lastSlash = lastPart.lastIndexOf('/');
86
+ dirPath = lastPart.substring(0, lastSlash + 1);
87
+ filePrefix = lastPart.substring(lastSlash + 1);
88
+ } else {
89
+ // No slash - complete in current directory
90
+ dirPath = './';
91
+ filePrefix = lastPart;
92
+ }
93
+
94
+ // Build prefix (everything before the path argument)
95
+ const prefix = parts.slice(0, -1).join(' ') + ' ';
96
+
97
+ return {
98
+ command,
99
+ prefix,
100
+ dirPath,
101
+ filePrefix,
102
+ needsCompletion: true,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Build completion menu items from file list
108
+ * @param {Object} parsed - Parsed path info from parsePathInput
109
+ * @param {string[]} files - List of files in directory
110
+ * @returns {Array<{label: string, value: string, description: string}>}
111
+ */
112
+ export function buildCompletions(parsed, files) {
113
+ const { prefix, dirPath, filePrefix } = parsed;
114
+
115
+ // Filter files by prefix
116
+ const matches = files.filter(f => f.startsWith(filePrefix));
117
+
118
+ // Build completion items
119
+ // For current directory (./) don't include the ./ in the result
120
+ const pathPrefix = dirPath === './' ? '' : dirPath;
121
+
122
+ return matches.map(f => ({
123
+ label: f,
124
+ value: `${prefix}${pathPrefix}${f}`,
125
+ description: '',
126
+ }));
127
+ }
128
+
129
+ /**
130
+ * Get path completions from sandbox (async)
131
+ * @param {Object} sessionManager - Session manager for sandbox communication
132
+ * @param {string} input - Current input buffer
133
+ * @returns {Promise<Array>} Completion items
134
+ */
135
+ export async function getPathCompletions(sessionManager, input) {
136
+ if (hasDangerousShellChars(input)) {
137
+ return [];
138
+ }
139
+
140
+ const parsed = parsePathInput(input);
141
+
142
+ if (!parsed.needsCompletion) {
143
+ return [];
144
+ }
145
+
146
+ try {
147
+ const combined = `${parsed.dirPath || ''}${parsed.filePrefix || ''}`;
148
+ if (hasDangerousShellChars(combined)) {
149
+ return [];
150
+ }
151
+
152
+ // Query sandbox for directory contents
153
+ const result = await sessionManager.execute(`ls -1Ap -- ${shellEscapePosix(parsed.dirPath)} 2>/dev/null`);
154
+
155
+ if (!result.output) {
156
+ return [];
157
+ }
158
+
159
+ // Parse file list
160
+ const files = result.output.split(/[\r\n]+/).filter(f => f.trim()).slice(0, 200);
161
+
162
+ return buildCompletions(parsed, files);
163
+ } catch (e) {
164
+ // Silently fail - don't interrupt user input
165
+ return [];
166
+ }
167
+ }