@hcu-lab.me/mcp-device 1.0.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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/bootstrap.d.ts +1 -0
  4. package/dist/bootstrap.js +22 -0
  5. package/dist/command-manager.d.ts +8 -0
  6. package/dist/command-manager.js +241 -0
  7. package/dist/config-field-definitions.d.ts +41 -0
  8. package/dist/config-field-definitions.js +37 -0
  9. package/dist/config-manager.d.ts +102 -0
  10. package/dist/config-manager.js +291 -0
  11. package/dist/config.d.ts +6 -0
  12. package/dist/config.js +10 -0
  13. package/dist/custom-stdio.d.ts +51 -0
  14. package/dist/custom-stdio.js +358 -0
  15. package/dist/data/official-app-ca.pem +12 -0
  16. package/dist/data/onboarding-prompts.json +70 -0
  17. package/dist/device/device-state.d.ts +13 -0
  18. package/dist/device/device-state.js +100 -0
  19. package/dist/device/device-status.d.ts +81 -0
  20. package/dist/device/device-status.js +156 -0
  21. package/dist/device/device.d.ts +16 -0
  22. package/dist/device/device.js +190 -0
  23. package/dist/device/execution-engine.d.ts +144 -0
  24. package/dist/device/execution-engine.js +195 -0
  25. package/dist/device/gateway-channel.d.ts +48 -0
  26. package/dist/device/gateway-channel.js +384 -0
  27. package/dist/device/gateway-config.d.ts +40 -0
  28. package/dist/device/gateway-config.js +207 -0
  29. package/dist/device/gateway-identity.d.ts +35 -0
  30. package/dist/device/gateway-identity.js +178 -0
  31. package/dist/device/gateway-pairing.d.ts +25 -0
  32. package/dist/device/gateway-pairing.js +105 -0
  33. package/dist/device/gateway-secure-transport.d.ts +25 -0
  34. package/dist/device/gateway-secure-transport.js +117 -0
  35. package/dist/device/gateway-tool-adapter.d.ts +17 -0
  36. package/dist/device/gateway-tool-adapter.js +284 -0
  37. package/dist/device/gateway-url-policy.d.ts +3 -0
  38. package/dist/device/gateway-url-policy.js +43 -0
  39. package/dist/device/linux-service.d.ts +70 -0
  40. package/dist/device/linux-service.js +268 -0
  41. package/dist/device/official-trust.d.ts +7 -0
  42. package/dist/device/official-trust.js +49 -0
  43. package/dist/device/project-inspection.d.ts +1 -0
  44. package/dist/device/project-inspection.js +180 -0
  45. package/dist/device/runtime-owner.d.ts +41 -0
  46. package/dist/device/runtime-owner.js +177 -0
  47. package/dist/device/windows-dpapi.d.ts +11 -0
  48. package/dist/device/windows-dpapi.js +67 -0
  49. package/dist/device/windows-service.d.ts +56 -0
  50. package/dist/device/windows-service.js +238 -0
  51. package/dist/error-handlers.d.ts +7 -0
  52. package/dist/error-handlers.js +15 -0
  53. package/dist/handlers/edit-search-handlers.d.ts +6 -0
  54. package/dist/handlers/edit-search-handlers.js +6 -0
  55. package/dist/handlers/filesystem-handlers.d.ts +38 -0
  56. package/dist/handlers/filesystem-handlers.js +440 -0
  57. package/dist/handlers/history-handlers.d.ts +12 -0
  58. package/dist/handlers/history-handlers.js +67 -0
  59. package/dist/handlers/index.d.ts +6 -0
  60. package/dist/handlers/index.js +7 -0
  61. package/dist/handlers/process-handlers.d.ts +9 -0
  62. package/dist/handlers/process-handlers.js +15 -0
  63. package/dist/handlers/search-handlers.d.ts +17 -0
  64. package/dist/handlers/search-handlers.js +224 -0
  65. package/dist/handlers/terminal-handlers.d.ts +21 -0
  66. package/dist/handlers/terminal-handlers.js +35 -0
  67. package/dist/index.d.ts +2 -0
  68. package/dist/index.js +138 -0
  69. package/dist/mcp-device.d.ts +2 -0
  70. package/dist/mcp-device.js +7 -0
  71. package/dist/npm-scripts/remote.d.ts +3 -0
  72. package/dist/npm-scripts/remote.js +314 -0
  73. package/dist/search-manager.d.ts +147 -0
  74. package/dist/search-manager.js +828 -0
  75. package/dist/server.d.ts +46 -0
  76. package/dist/server.js +1563 -0
  77. package/dist/terminal-manager.d.ts +104 -0
  78. package/dist/terminal-manager.js +635 -0
  79. package/dist/tools/config.d.ts +96 -0
  80. package/dist/tools/config.js +260 -0
  81. package/dist/tools/edit.d.ts +38 -0
  82. package/dist/tools/edit.js +435 -0
  83. package/dist/tools/feedback.d.ts +8 -0
  84. package/dist/tools/feedback.js +132 -0
  85. package/dist/tools/filesystem.d.ts +79 -0
  86. package/dist/tools/filesystem.js +947 -0
  87. package/dist/tools/fuzzySearch.d.ts +12 -0
  88. package/dist/tools/fuzzySearch.js +74 -0
  89. package/dist/tools/fuzzySearchCore.d.ts +52 -0
  90. package/dist/tools/fuzzySearchCore.js +125 -0
  91. package/dist/tools/improved-process-tools.d.ts +24 -0
  92. package/dist/tools/improved-process-tools.js +627 -0
  93. package/dist/tools/mime-types.d.ts +3 -0
  94. package/dist/tools/mime-types.js +28 -0
  95. package/dist/tools/pdf/extract-images.d.ts +34 -0
  96. package/dist/tools/pdf/extract-images.js +132 -0
  97. package/dist/tools/pdf/index.d.ts +6 -0
  98. package/dist/tools/pdf/index.js +3 -0
  99. package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
  100. package/dist/tools/pdf/lib/pdf2md.js +76 -0
  101. package/dist/tools/pdf/manipulations.d.ts +13 -0
  102. package/dist/tools/pdf/manipulations.js +99 -0
  103. package/dist/tools/pdf/markdown.d.ts +26 -0
  104. package/dist/tools/pdf/markdown.js +275 -0
  105. package/dist/tools/pdf/utils.d.ts +12 -0
  106. package/dist/tools/pdf/utils.js +34 -0
  107. package/dist/tools/process.d.ts +3 -0
  108. package/dist/tools/process.js +56 -0
  109. package/dist/tools/prompts.d.ts +28 -0
  110. package/dist/tools/prompts.js +259 -0
  111. package/dist/tools/schemas.d.ts +454 -0
  112. package/dist/tools/schemas.js +242 -0
  113. package/dist/tools/usage.d.ts +5 -0
  114. package/dist/tools/usage.js +24 -0
  115. package/dist/types.d.ts +83 -0
  116. package/dist/types.js +1 -0
  117. package/dist/ui/config-editor/config-editor-runtime.js +150 -0
  118. package/dist/ui/config-editor/index.html +13 -0
  119. package/dist/ui/config-editor/src/app.d.ts +43 -0
  120. package/dist/ui/config-editor/src/app.js +831 -0
  121. package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
  122. package/dist/ui/config-editor/src/array-modal.js +185 -0
  123. package/dist/ui/config-editor/src/main.d.ts +1 -0
  124. package/dist/ui/config-editor/src/main.js +2 -0
  125. package/dist/ui/config-editor/styles.css +587 -0
  126. package/dist/ui/contracts.d.ts +14 -0
  127. package/dist/ui/contracts.js +21 -0
  128. package/dist/ui/file-preview/index.html +16 -0
  129. package/dist/ui/file-preview/preview-runtime.js +469 -0
  130. package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
  131. package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
  132. package/dist/ui/file-preview/src/app.d.ts +4 -0
  133. package/dist/ui/file-preview/src/app.js +680 -0
  134. package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
  135. package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
  136. package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
  137. package/dist/ui/file-preview/src/components/highlighting.js +54 -0
  138. package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
  139. package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
  140. package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
  141. package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
  142. package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
  143. package/dist/ui/file-preview/src/directory-controller.js +240 -0
  144. package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
  145. package/dist/ui/file-preview/src/document-layout.js +109 -0
  146. package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
  147. package/dist/ui/file-preview/src/document-outline.js +97 -0
  148. package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
  149. package/dist/ui/file-preview/src/document-workspace.js +33 -0
  150. package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
  151. package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
  152. package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
  153. package/dist/ui/file-preview/src/host/external-actions.js +55 -0
  154. package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
  155. package/dist/ui/file-preview/src/host/selection-context.js +106 -0
  156. package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
  157. package/dist/ui/file-preview/src/image-preview.js +21 -0
  158. package/dist/ui/file-preview/src/main.d.ts +1 -0
  159. package/dist/ui/file-preview/src/main.js +5 -0
  160. package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
  161. package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
  162. package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
  163. package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
  164. package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
  165. package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
  166. package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
  167. package/dist/ui/file-preview/src/markdown/linking.js +228 -0
  168. package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
  169. package/dist/ui/file-preview/src/markdown/outline.js +16 -0
  170. package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
  171. package/dist/ui/file-preview/src/markdown/parser.js +38 -0
  172. package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
  173. package/dist/ui/file-preview/src/markdown/preview.js +20 -0
  174. package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
  175. package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
  176. package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
  177. package/dist/ui/file-preview/src/markdown/utils.js +15 -0
  178. package/dist/ui/file-preview/src/model.d.ts +35 -0
  179. package/dist/ui/file-preview/src/model.js +1 -0
  180. package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
  181. package/dist/ui/file-preview/src/panel-actions.js +182 -0
  182. package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
  183. package/dist/ui/file-preview/src/path-utils.js +64 -0
  184. package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
  185. package/dist/ui/file-preview/src/payload-utils.js +119 -0
  186. package/dist/ui/file-preview/src/types.d.ts +1 -0
  187. package/dist/ui/file-preview/src/types.js +1 -0
  188. package/dist/ui/file-preview/styles.css +1631 -0
  189. package/dist/ui/resources.d.ts +28 -0
  190. package/dist/ui/resources.js +86 -0
  191. package/dist/ui/shared/compact-row.d.ts +11 -0
  192. package/dist/ui/shared/compact-row.js +18 -0
  193. package/dist/ui/shared/escape-html.d.ts +4 -0
  194. package/dist/ui/shared/escape-html.js +11 -0
  195. package/dist/ui/shared/host-context.d.ts +15 -0
  196. package/dist/ui/shared/host-context.js +51 -0
  197. package/dist/ui/shared/tool-bridge.d.ts +30 -0
  198. package/dist/ui/shared/tool-bridge.js +137 -0
  199. package/dist/ui/shared/tool-shell.d.ts +25 -0
  200. package/dist/ui/shared/tool-shell.js +107 -0
  201. package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
  202. package/dist/ui/shared/ui-event-tracker.js +27 -0
  203. package/dist/ui/shared/widget-state.d.ts +33 -0
  204. package/dist/ui/shared/widget-state.js +158 -0
  205. package/dist/utils/ab-test.d.ts +12 -0
  206. package/dist/utils/ab-test.js +104 -0
  207. package/dist/utils/capture.d.ts +35 -0
  208. package/dist/utils/capture.js +481 -0
  209. package/dist/utils/dockerPrompt.d.ts +17 -0
  210. package/dist/utils/dockerPrompt.js +53 -0
  211. package/dist/utils/feature-flags.d.ts +59 -0
  212. package/dist/utils/feature-flags.js +215 -0
  213. package/dist/utils/files/base.d.ts +173 -0
  214. package/dist/utils/files/base.js +5 -0
  215. package/dist/utils/files/binary.d.ts +21 -0
  216. package/dist/utils/files/binary.js +65 -0
  217. package/dist/utils/files/docx.d.ts +47 -0
  218. package/dist/utils/files/docx.js +679 -0
  219. package/dist/utils/files/excel.d.ts +24 -0
  220. package/dist/utils/files/excel.js +437 -0
  221. package/dist/utils/files/factory.d.ts +41 -0
  222. package/dist/utils/files/factory.js +113 -0
  223. package/dist/utils/files/image.d.ts +21 -0
  224. package/dist/utils/files/image.js +78 -0
  225. package/dist/utils/files/index.d.ts +10 -0
  226. package/dist/utils/files/index.js +13 -0
  227. package/dist/utils/files/pdf.d.ts +32 -0
  228. package/dist/utils/files/pdf.js +142 -0
  229. package/dist/utils/files/text.d.ts +63 -0
  230. package/dist/utils/files/text.js +370 -0
  231. package/dist/utils/fuzzySearchLogger.d.ts +30 -0
  232. package/dist/utils/fuzzySearchLogger.js +126 -0
  233. package/dist/utils/lineEndingHandler.d.ts +21 -0
  234. package/dist/utils/lineEndingHandler.js +77 -0
  235. package/dist/utils/logger.d.ts +32 -0
  236. package/dist/utils/logger.js +72 -0
  237. package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
  238. package/dist/utils/mcp-ui-ab-test.js +62 -0
  239. package/dist/utils/open-browser.d.ts +9 -0
  240. package/dist/utils/open-browser.js +42 -0
  241. package/dist/utils/process-detection.d.ts +23 -0
  242. package/dist/utils/process-detection.js +150 -0
  243. package/dist/utils/ripgrep-resolver.d.ts +9 -0
  244. package/dist/utils/ripgrep-resolver.js +74 -0
  245. package/dist/utils/system-info.d.ts +71 -0
  246. package/dist/utils/system-info.js +712 -0
  247. package/dist/utils/toolHistory.d.ts +86 -0
  248. package/dist/utils/toolHistory.js +257 -0
  249. package/dist/utils/trackTools.d.ts +6 -0
  250. package/dist/utils/trackTools.js +54 -0
  251. package/dist/utils/unsupportedParams.d.ts +24 -0
  252. package/dist/utils/unsupportedParams.js +66 -0
  253. package/dist/utils/usageTracker.d.ts +133 -0
  254. package/dist/utils/usageTracker.js +477 -0
  255. package/dist/utils/welcome-onboarding.d.ts +11 -0
  256. package/dist/utils/welcome-onboarding.js +99 -0
  257. package/dist/utils/withTimeout.d.ts +28 -0
  258. package/dist/utils/withTimeout.js +85 -0
  259. package/dist/version.d.ts +1 -0
  260. package/dist/version.js +1 -0
  261. package/package.json +131 -0
@@ -0,0 +1,635 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import { DEFAULT_COMMAND_TIMEOUT } from './config.js';
4
+ import { configManager } from './config-manager.js';
5
+ import { capture } from "./utils/capture.js";
6
+ import { analyzeProcessState } from './utils/process-detection.js';
7
+ /**
8
+ * Standard Windows PATHEXT value, used to repair a corrupted PATHEXT before
9
+ * spawning child shells.
10
+ *
11
+ * On some Windows Claude Desktop / DXT launches the server process inherits a
12
+ * broken PATHEXT (observed as ".CPL" only). Because we build the child env from
13
+ * { ...process.env }, that broken value would propagate into every spawned
14
+ * shell, stripping ".EXE" and breaking resolution of git / node / python / rg /
15
+ * etc. (and even full-path .exe invocations under PowerShell). See issue #481.
16
+ */
17
+ const STANDARD_PATHEXT = '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC';
18
+ /**
19
+ * Return a healthy PATHEXT for spawned Windows shells.
20
+ * - Unset -> use the standard list.
21
+ * - Missing ".EXE" -> corrupted; merge the standard list with whatever was
22
+ * present (preserves any extra extensions, order-stable).
23
+ * - Otherwise -> leave the inherited value untouched.
24
+ */
25
+ function getRepairedPathExt() {
26
+ const current = process.env.PATHEXT;
27
+ if (!current)
28
+ return STANDARD_PATHEXT;
29
+ const exts = current.split(';').map(e => e.trim().toUpperCase()).filter(Boolean);
30
+ if (!exts.includes('.EXE')) {
31
+ return [...new Set([...STANDARD_PATHEXT.split(';'), ...exts])].join(';');
32
+ }
33
+ return current;
34
+ }
35
+ /**
36
+ * Output buffering caps. Without a cap, a process emitting enough output makes
37
+ * string concatenation throw "RangeError: Invalid string length" at V8's max
38
+ * string size (~536M chars) inside a stdout 'data' handler — an uncaught
39
+ * exception that kills the whole server (index.ts exits on uncaughtException).
40
+ * The cap also bounds the join() cost in snapshot reads and the periodic
41
+ * process-state scan, both of which are O(total output).
42
+ */
43
+ export const MAX_BUFFERED_OUTPUT_CHARS = 50 * 1024 * 1024; // per session; oldest lines evicted first
44
+ const MAX_LINE_CHARS = 1024 * 1024; // force-split longer lines so eviction can work
45
+ const MAX_WAIT_OUTPUT_CHARS = 2 * 1024 * 1024; // start_process wait buffer (prompt/state detection)
46
+ /**
47
+ * Get the appropriate spawn configuration for a given shell
48
+ * This handles login shell flags for different shell types
49
+ */
50
+ function getShellSpawnArgs(shellPath, command) {
51
+ const shellName = path.basename(shellPath).toLowerCase();
52
+ // Unix shells with login flag support
53
+ if (shellName.includes('bash') || shellName.includes('zsh')) {
54
+ return {
55
+ executable: shellPath,
56
+ args: ['-l', '-c', command],
57
+ useShellOption: false
58
+ };
59
+ }
60
+ // PowerShell Core (cross-platform, supports -Login)
61
+ if (shellName === 'pwsh' || shellName === 'pwsh.exe') {
62
+ return {
63
+ executable: shellPath,
64
+ args: ['-Login', '-Command', command],
65
+ useShellOption: false
66
+ };
67
+ }
68
+ // Windows PowerShell 5.1 (no login flag support)
69
+ if (shellName === 'powershell' || shellName === 'powershell.exe') {
70
+ return {
71
+ executable: shellPath,
72
+ args: ['-NoProfile', '-NonInteractive', '-Command', command],
73
+ useShellOption: false
74
+ };
75
+ }
76
+ // CMD
77
+ if (shellName === 'cmd' || shellName === 'cmd.exe') {
78
+ return {
79
+ executable: shellPath,
80
+ args: ['/c', command],
81
+ windowsVerbatim: true,
82
+ useShellOption: false
83
+ };
84
+ }
85
+ // Fish shell (uses -l for login, -c for command)
86
+ if (shellName.includes('fish')) {
87
+ return {
88
+ executable: shellPath,
89
+ args: ['-l', '-c', command],
90
+ useShellOption: false
91
+ };
92
+ }
93
+ // Unknown/other shells - use shell option for safety
94
+ // This provides a fallback for shells we don't explicitly handle
95
+ return {
96
+ executable: command,
97
+ args: [],
98
+ useShellOption: shellPath
99
+ };
100
+ }
101
+ export class TerminalManager {
102
+ constructor() {
103
+ this.sessions = new Map();
104
+ this.completedSessions = new Map();
105
+ }
106
+ /**
107
+ * Send input to a running process
108
+ * @param pid Process ID
109
+ * @param input Text to send to the process
110
+ * @returns Whether input was successfully sent
111
+ */
112
+ sendInputToProcess(pid, input) {
113
+ const session = this.sessions.get(pid);
114
+ if (!session) {
115
+ return false;
116
+ }
117
+ try {
118
+ if (session.process.stdin && !session.process.stdin.destroyed) {
119
+ // Ensure input ends with a newline for most REPLs
120
+ const inputWithNewline = input.endsWith('\n') ? input : input + '\n';
121
+ session.process.stdin.write(inputWithNewline);
122
+ return true;
123
+ }
124
+ return false;
125
+ }
126
+ catch (error) {
127
+ console.error(`Error sending input to process ${pid}:`, error);
128
+ return false;
129
+ }
130
+ }
131
+ async executeCommand(command, timeoutMs = DEFAULT_COMMAND_TIMEOUT, shell, collectTiming = false, workingDirectory) {
132
+ // Get the shell from config if not specified
133
+ let shellToUse = shell;
134
+ if (!shellToUse) {
135
+ try {
136
+ const config = await configManager.getConfig();
137
+ shellToUse = config.defaultShell || true;
138
+ }
139
+ catch (error) {
140
+ // If there's an error getting the config, fall back to default
141
+ shellToUse = true;
142
+ }
143
+ }
144
+ // For REPL interactions, we need to ensure stdin, stdout, and stderr are properly configured
145
+ // Note: No special stdio options needed here, Node.js handles pipes by default
146
+ // Enhance SSH commands automatically
147
+ let enhancedCommand = command;
148
+ if (command.trim().startsWith('ssh ') && !command.includes(' -t')) {
149
+ enhancedCommand = command.replace(/^ssh /, 'ssh -t ');
150
+ console.log(`Enhanced SSH command: ${enhancedCommand}`);
151
+ }
152
+ // Get the appropriate spawn configuration for the shell
153
+ let spawnConfig;
154
+ let spawnOptions;
155
+ if (typeof shellToUse === 'string') {
156
+ // Use shell-specific configuration with login flags where appropriate
157
+ spawnConfig = getShellSpawnArgs(shellToUse, enhancedCommand);
158
+ spawnOptions = {
159
+ env: {
160
+ ...process.env,
161
+ TERM: 'xterm-256color' // Better terminal compatibility
162
+ },
163
+ windowsHide: true, // Prevent visible console windows on Windows
164
+ ...(workingDirectory ? { cwd: workingDirectory } : {})
165
+ };
166
+ // Add shell option if needed (for unknown shells)
167
+ if (spawnConfig.useShellOption) {
168
+ spawnOptions.shell = spawnConfig.useShellOption;
169
+ }
170
+ }
171
+ else {
172
+ // Boolean or undefined shell - use default shell option behavior
173
+ spawnConfig = {
174
+ executable: enhancedCommand,
175
+ args: [],
176
+ useShellOption: shellToUse
177
+ };
178
+ spawnOptions = {
179
+ shell: shellToUse,
180
+ env: {
181
+ ...process.env,
182
+ TERM: 'xterm-256color'
183
+ },
184
+ windowsHide: true, // Prevent visible console windows on Windows
185
+ ...(workingDirectory ? { cwd: workingDirectory } : {})
186
+ };
187
+ }
188
+ // Repair PATHEXT on Windows before spawning. On some Windows DXT launches
189
+ // the server process inherits a corrupted PATHEXT (e.g. ".CPL"), which we
190
+ // would otherwise propagate via { ...process.env } and break command
191
+ // resolution (git, node, python, rg, ...) in the spawned shell. See #481.
192
+ if (process.platform === 'win32' && spawnOptions.env) {
193
+ spawnOptions.env.PATHEXT = getRepairedPathExt();
194
+ }
195
+ // On Windows, when we invoke cmd.exe directly and pass the user's command as a
196
+ // single argument, Node/libuv applies MSVCRT-style quoting that escapes embedded
197
+ // double quotes as \" . cmd.exe does not understand that escaping, so any command
198
+ // containing quotes (e.g. a quoted path with spaces like "C:\Program Files\app.exe")
199
+ // is corrupted before the shell ever parses it. Passing arguments verbatim lets
200
+ // cmd handle its own quoting. Scoped to shells that set windowsVerbatim (cmd only)
201
+ // because PowerShell/pwsh have different quote rules and must NOT use verbatim.
202
+ if (process.platform === 'win32' && spawnConfig.windowsVerbatim) {
203
+ spawnOptions.windowsVerbatimArguments = true;
204
+ }
205
+ // Spawn the process with appropriate arguments
206
+ const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
207
+ let output = '';
208
+ // Ensure childProcess.pid is defined before proceeding
209
+ if (!childProcess.pid) {
210
+ // Return a consistent error object instead of throwing
211
+ return {
212
+ pid: -1, // Use -1 to indicate an error state
213
+ output: 'Error: Failed to get process ID. The command could not be executed.',
214
+ isBlocked: false
215
+ };
216
+ }
217
+ const session = {
218
+ pid: childProcess.pid,
219
+ process: childProcess,
220
+ outputLines: [], // Line-based buffer
221
+ lastReadIndex: 0, // Track where "new" output starts
222
+ isBlocked: false,
223
+ startTime: new Date(),
224
+ bufferedChars: 0,
225
+ evictedLines: 0,
226
+ evictedChars: 0
227
+ };
228
+ this.sessions.set(childProcess.pid, session);
229
+ // Timing telemetry
230
+ const startTime = Date.now();
231
+ let firstOutputTime;
232
+ let lastOutputTime;
233
+ const outputEvents = [];
234
+ let exitReason = 'timeout';
235
+ return new Promise((resolve) => {
236
+ let resolved = false;
237
+ let periodicCheck = null;
238
+ // Quick prompt patterns for immediate detection
239
+ const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
240
+ const resolveOnce = (result) => {
241
+ if (resolved)
242
+ return;
243
+ resolved = true;
244
+ if (periodicCheck)
245
+ clearInterval(periodicCheck);
246
+ // Add timing info if requested
247
+ if (collectTiming) {
248
+ const endTime = Date.now();
249
+ result.timingInfo = {
250
+ startTime,
251
+ endTime,
252
+ totalDurationMs: endTime - startTime,
253
+ exitReason,
254
+ firstOutputTime,
255
+ lastOutputTime,
256
+ timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
257
+ outputEvents: outputEvents.length > 0 ? outputEvents : undefined
258
+ };
259
+ }
260
+ resolve(result);
261
+ };
262
+ childProcess.stdout.on('data', (data) => {
263
+ const text = data.toString();
264
+ const now = Date.now();
265
+ if (!firstOutputTime)
266
+ firstOutputTime = now;
267
+ lastOutputTime = now;
268
+ // `output` only feeds the wait-phase result and prompt/state detection,
269
+ // so stop growing it once resolved and keep only a bounded tail.
270
+ if (!resolved) {
271
+ output += text;
272
+ if (output.length > MAX_WAIT_OUTPUT_CHARS) {
273
+ output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
274
+ }
275
+ }
276
+ // Append to line-based buffer
277
+ this.appendToLineBuffer(session, text);
278
+ // Record output event if collecting timing
279
+ if (collectTiming) {
280
+ outputEvents.push({
281
+ timestamp: now,
282
+ deltaMs: now - startTime,
283
+ source: 'stdout',
284
+ length: text.length,
285
+ snippet: text.slice(0, 50).replace(/\n/g, '\\n')
286
+ });
287
+ }
288
+ // Immediate check for obvious prompts
289
+ if (quickPromptPatterns.test(text)) {
290
+ session.isBlocked = true;
291
+ exitReason = 'early_exit_quick_pattern';
292
+ if (collectTiming && outputEvents.length > 0) {
293
+ outputEvents[outputEvents.length - 1].matchedPattern = 'quick_pattern';
294
+ }
295
+ resolveOnce({
296
+ pid: childProcess.pid,
297
+ output,
298
+ isBlocked: true
299
+ });
300
+ }
301
+ });
302
+ childProcess.stderr.on('data', (data) => {
303
+ const text = data.toString();
304
+ const now = Date.now();
305
+ if (!firstOutputTime)
306
+ firstOutputTime = now;
307
+ lastOutputTime = now;
308
+ if (!resolved) {
309
+ output += text;
310
+ if (output.length > MAX_WAIT_OUTPUT_CHARS) {
311
+ output = output.slice(-Math.floor(MAX_WAIT_OUTPUT_CHARS / 2));
312
+ }
313
+ }
314
+ // Append to line-based buffer
315
+ this.appendToLineBuffer(session, text);
316
+ // Record output event if collecting timing
317
+ if (collectTiming) {
318
+ outputEvents.push({
319
+ timestamp: now,
320
+ deltaMs: now - startTime,
321
+ source: 'stderr',
322
+ length: text.length,
323
+ snippet: text.slice(0, 50).replace(/\n/g, '\\n')
324
+ });
325
+ }
326
+ });
327
+ // Periodic comprehensive check every 100ms
328
+ periodicCheck = setInterval(() => {
329
+ if (output.trim()) {
330
+ const processState = analyzeProcessState(output, childProcess.pid);
331
+ if (processState.isWaitingForInput) {
332
+ session.isBlocked = true;
333
+ exitReason = 'early_exit_periodic_check';
334
+ resolveOnce({
335
+ pid: childProcess.pid,
336
+ output,
337
+ isBlocked: true
338
+ });
339
+ }
340
+ }
341
+ }, 100);
342
+ // Timeout fallback
343
+ setTimeout(() => {
344
+ session.isBlocked = true;
345
+ exitReason = 'timeout';
346
+ resolveOnce({
347
+ pid: childProcess.pid,
348
+ output,
349
+ isBlocked: true
350
+ });
351
+ }, timeoutMs);
352
+ childProcess.on('exit', (code) => {
353
+ if (childProcess.pid) {
354
+ // Store completed session before removing active session
355
+ this.completedSessions.set(childProcess.pid, {
356
+ pid: childProcess.pid,
357
+ outputLines: [...session.outputLines], // Copy line buffer
358
+ exitCode: code,
359
+ startTime: session.startTime,
360
+ endTime: new Date(),
361
+ evictedLines: session.evictedLines,
362
+ evictedChars: session.evictedChars
363
+ });
364
+ // Keep only last 100 completed sessions
365
+ if (this.completedSessions.size > 100) {
366
+ const oldestKey = Array.from(this.completedSessions.keys())[0];
367
+ this.completedSessions.delete(oldestKey);
368
+ }
369
+ this.sessions.delete(childProcess.pid);
370
+ }
371
+ exitReason = 'process_exit';
372
+ resolveOnce({
373
+ pid: childProcess.pid,
374
+ output,
375
+ isBlocked: false
376
+ });
377
+ });
378
+ });
379
+ }
380
+ /**
381
+ * Append text to a session's line buffer
382
+ * Handles partial lines and newline splitting
383
+ */
384
+ appendToLineBuffer(session, text) {
385
+ if (!text)
386
+ return;
387
+ // Split text into lines, keeping track of whether text ends with newline
388
+ const lines = text.split('\n');
389
+ for (let i = 0; i < lines.length; i++) {
390
+ const line = lines[i];
391
+ const isLastFragment = i === lines.length - 1;
392
+ const endsWithNewline = text.endsWith('\n');
393
+ if (session.outputLines.length === 0) {
394
+ // First line ever
395
+ session.outputLines.push(line);
396
+ }
397
+ else if (i === 0) {
398
+ // First fragment - append to last line (might be partial)
399
+ session.outputLines[session.outputLines.length - 1] += line;
400
+ }
401
+ else {
402
+ // Subsequent lines - add as new lines
403
+ session.outputLines.push(line);
404
+ }
405
+ }
406
+ // Appended text contributes exactly its length to the joined buffer
407
+ // (its newlines become the join separators).
408
+ session.bufferedChars += text.length;
409
+ // A process printing without newlines grows a single line forever, which
410
+ // eviction can't bound — force-split so no line exceeds MAX_LINE_CHARS.
411
+ // Each inserted break adds one separator to the joined length.
412
+ let lastIndex = session.outputLines.length - 1;
413
+ while (session.outputLines[lastIndex].length > MAX_LINE_CHARS) {
414
+ const overlong = session.outputLines[lastIndex];
415
+ session.outputLines[lastIndex] = overlong.slice(0, MAX_LINE_CHARS);
416
+ session.outputLines.push(overlong.slice(MAX_LINE_CHARS));
417
+ session.bufferedChars += 1;
418
+ lastIndex++;
419
+ }
420
+ // Enforce the per-session cap by evicting the oldest lines. Keeps the
421
+ // buffer far below V8's max string length so concatenation and join()
422
+ // can never throw "Invalid string length" and kill the server.
423
+ while (session.bufferedChars > MAX_BUFFERED_OUTPUT_CHARS && session.outputLines.length > 1) {
424
+ const dropped = session.outputLines.shift();
425
+ const droppedJoinedChars = dropped.length + 1; // +1 for its join separator
426
+ session.bufferedChars -= droppedJoinedChars;
427
+ session.evictedChars += droppedJoinedChars;
428
+ session.evictedLines++;
429
+ if (session.lastReadIndex > 0)
430
+ session.lastReadIndex--;
431
+ }
432
+ }
433
+ /**
434
+ * Read process output with pagination (like file reading)
435
+ * @param pid Process ID
436
+ * @param offset Line offset: 0=from lastReadIndex, positive=absolute, negative=tail
437
+ * @param length Max lines to return
438
+ * @param updateReadIndex Whether to update lastReadIndex (default: true for offset=0)
439
+ */
440
+ readOutputPaginated(pid, offset = 0, length = 1000) {
441
+ // First check active sessions
442
+ const session = this.sessions.get(pid);
443
+ if (session) {
444
+ const result = this.readFromLineBuffer(session.outputLines, offset, length, session.lastReadIndex, (newIndex) => { session.lastReadIndex = newIndex; }, false, undefined);
445
+ result.evictedLines = session.evictedLines;
446
+ return result;
447
+ }
448
+ // Then check completed sessions
449
+ const completedSession = this.completedSessions.get(pid);
450
+ if (completedSession) {
451
+ const runtimeMs = completedSession.endTime.getTime() - completedSession.startTime.getTime();
452
+ const result = this.readFromLineBuffer(completedSession.outputLines, offset, length, 0, // Completed sessions don't track read position
453
+ () => { }, // No-op for completed sessions
454
+ true, completedSession.exitCode, runtimeMs);
455
+ result.evictedLines = completedSession.evictedLines;
456
+ return result;
457
+ }
458
+ return null;
459
+ }
460
+ /**
461
+ * Internal helper to read from a line buffer with offset/length
462
+ */
463
+ readFromLineBuffer(lines, offset, length, lastReadIndex, updateLastRead, isComplete, exitCode, runtimeMs) {
464
+ const totalLines = lines.length;
465
+ let startIndex;
466
+ let linesToRead;
467
+ if (offset < 0) {
468
+ // Negative offset = start position from end, then read 'length' lines forward
469
+ // e.g., offset=-50, length=10 means: start 50 lines from end, read 10 lines
470
+ const fromEnd = Math.abs(offset);
471
+ startIndex = Math.max(0, totalLines - fromEnd);
472
+ linesToRead = lines.slice(startIndex, startIndex + length);
473
+ // Don't update lastReadIndex for tail reads
474
+ }
475
+ else if (offset === 0) {
476
+ // offset=0 means "from where I last read" (like getNewOutput)
477
+ startIndex = lastReadIndex;
478
+ linesToRead = lines.slice(startIndex, startIndex + length);
479
+ // Update lastReadIndex for "new output" behavior
480
+ updateLastRead(Math.min(startIndex + linesToRead.length, totalLines));
481
+ }
482
+ else {
483
+ // Positive offset = absolute position
484
+ startIndex = offset;
485
+ linesToRead = lines.slice(startIndex, startIndex + length);
486
+ // Don't update lastReadIndex for absolute position reads
487
+ }
488
+ const readCount = linesToRead.length;
489
+ const endIndex = startIndex + readCount;
490
+ const remaining = Math.max(0, totalLines - endIndex);
491
+ return {
492
+ lines: linesToRead,
493
+ totalLines,
494
+ readFrom: startIndex,
495
+ readCount,
496
+ remaining,
497
+ isComplete,
498
+ exitCode,
499
+ runtimeMs
500
+ };
501
+ }
502
+ /**
503
+ * Get total line count for a process
504
+ */
505
+ getOutputLineCount(pid) {
506
+ const session = this.sessions.get(pid);
507
+ if (session) {
508
+ return session.outputLines.length;
509
+ }
510
+ const completedSession = this.completedSessions.get(pid);
511
+ if (completedSession) {
512
+ return completedSession.outputLines.length;
513
+ }
514
+ return null;
515
+ }
516
+ /**
517
+ * Legacy method for backward compatibility
518
+ * Returns all new output since last read
519
+ * @param maxLines Maximum lines to return (default: 1000 for context protection)
520
+ * @deprecated Use readOutputPaginated instead
521
+ */
522
+ getNewOutput(pid, maxLines = 1000) {
523
+ const result = this.readOutputPaginated(pid, 0, maxLines);
524
+ if (!result)
525
+ return null;
526
+ const output = result.lines.join('\n').trim();
527
+ // For completed sessions, append completion info with runtime
528
+ if (result.isComplete) {
529
+ const runtimeStr = result.runtimeMs !== undefined
530
+ ? `\nRuntime: ${(result.runtimeMs / 1000).toFixed(2)}s`
531
+ : '';
532
+ if (output) {
533
+ return `${output}\n\nProcess completed with exit code ${result.exitCode}${runtimeStr}`;
534
+ }
535
+ else {
536
+ return `Process completed with exit code ${result.exitCode}${runtimeStr}\n(No output produced)`;
537
+ }
538
+ }
539
+ // Add truncation warning if there's more output
540
+ if (result.remaining > 0) {
541
+ return `${output}\n\n[Output truncated: ${result.remaining} more lines available. Use read_process_output with offset/length for full output.]`;
542
+ }
543
+ return output || null;
544
+ }
545
+ /**
546
+ * Capture a snapshot of current output state for interaction tracking.
547
+ * Used by interactWithProcess to know what output existed before sending input.
548
+ */
549
+ captureOutputSnapshot(pid) {
550
+ const session = this.sessions.get(pid);
551
+ if (session) {
552
+ const fullOutput = session.outputLines.join('\n');
553
+ return {
554
+ // Absolute since process start (includes evicted output), so the
555
+ // offset stays valid even if the cap evicts lines between
556
+ // snapshot and read.
557
+ totalChars: session.evictedChars + fullOutput.length,
558
+ lineCount: session.evictedLines + session.outputLines.length
559
+ };
560
+ }
561
+ return null;
562
+ }
563
+ /**
564
+ * Get output that appeared since a snapshot was taken.
565
+ * This handles the case where output is appended to the last line (REPL prompts).
566
+ * Also checks completed sessions in case process finished between snapshot and poll.
567
+ */
568
+ getOutputSinceSnapshot(pid, snapshot) {
569
+ // Check active session first
570
+ const session = this.sessions.get(pid);
571
+ if (session) {
572
+ return TerminalManager.outputSinceSnapshot(session.outputLines, session.evictedChars, snapshot.totalChars);
573
+ }
574
+ // Fallback to completed sessions - process may have finished between snapshot and poll
575
+ const completedSession = this.completedSessions.get(pid);
576
+ if (completedSession) {
577
+ return TerminalManager.outputSinceSnapshot(completedSession.outputLines, completedSession.evictedChars, snapshot.totalChars);
578
+ }
579
+ return null;
580
+ }
581
+ /**
582
+ * New output since a snapshot, in absolute (since process start) offsets.
583
+ * If eviction dropped part of the unseen output, returns what the buffer
584
+ * still holds — the oldest unseen chars are lost to the cap.
585
+ */
586
+ static outputSinceSnapshot(outputLines, evictedChars, snapshotTotalChars) {
587
+ const fullOutput = outputLines.join('\n');
588
+ const newChars = evictedChars + fullOutput.length - snapshotTotalChars;
589
+ if (newChars <= 0) {
590
+ return ''; // No new output
591
+ }
592
+ return fullOutput.substring(Math.max(0, fullOutput.length - newChars));
593
+ }
594
+ /**
595
+ * Get a session by PID
596
+ * @param pid Process ID
597
+ * @returns The session or undefined if not found
598
+ */
599
+ getSession(pid) {
600
+ return this.sessions.get(pid);
601
+ }
602
+ forceTerminate(pid) {
603
+ const session = this.sessions.get(pid);
604
+ if (!session) {
605
+ return false;
606
+ }
607
+ try {
608
+ session.process.kill('SIGINT');
609
+ setTimeout(() => {
610
+ if (this.sessions.has(pid)) {
611
+ session.process.kill('SIGKILL');
612
+ }
613
+ }, 1000);
614
+ return true;
615
+ }
616
+ catch (error) {
617
+ // Convert error to string, handling both Error objects and other types
618
+ const errorMessage = error instanceof Error ? error.message : String(error);
619
+ capture('server_request_error', { error: errorMessage, message: `Failed to terminate process ${pid}:` });
620
+ return false;
621
+ }
622
+ }
623
+ listActiveSessions() {
624
+ const now = new Date();
625
+ return Array.from(this.sessions.values()).map(session => ({
626
+ pid: session.pid,
627
+ isBlocked: session.isBlocked,
628
+ runtime: now.getTime() - session.startTime.getTime()
629
+ }));
630
+ }
631
+ listCompletedSessions() {
632
+ return Array.from(this.completedSessions.values());
633
+ }
634
+ }
635
+ export const terminalManager = new TerminalManager();