@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,627 @@
1
+ import { terminalManager, MAX_BUFFERED_OUTPUT_CHARS } from '../terminal-manager.js';
2
+ import { commandManager } from '../command-manager.js';
3
+ import { StartProcessArgsSchema, ReadProcessOutputArgsSchema, InteractWithProcessArgsSchema, ForceTerminateArgsSchema } from './schemas.js';
4
+ import { validatePath } from './filesystem.js';
5
+ import { capture } from "../utils/capture.js";
6
+ import { analyzeProcessState, cleanProcessOutput, formatProcessStateMessage } from '../utils/process-detection.js';
7
+ import * as os from 'os';
8
+ import { configManager } from '../config-manager.js';
9
+ import { spawn } from 'child_process';
10
+ import fs from 'fs/promises';
11
+ import path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ // Get the directory where the MCP is installed (for ES module imports)
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const mcpRoot = path.resolve(__dirname, '..', '..');
17
+ // Track virtual Node sessions (PIDs that are actually Node fallback sessions)
18
+ const virtualNodeSessions = new Map();
19
+ let virtualPidCounter = -1000; // Use negative PIDs for virtual sessions
20
+ /**
21
+ * Execute Node.js code via temp file (fallback when Python unavailable)
22
+ * Creates temp .mjs file in MCP directory for ES module import access
23
+ */
24
+ async function executeNodeCode(code, timeout_ms = 30000) {
25
+ const tempFile = path.join(mcpRoot, `.mcp-exec-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
26
+ try {
27
+ await fs.writeFile(tempFile, code, 'utf8');
28
+ const result = await new Promise((resolve) => {
29
+ const proc = spawn(process.execPath, [tempFile], {
30
+ cwd: mcpRoot,
31
+ timeout: timeout_ms,
32
+ windowsHide: true // Prevent visible console windows on Windows
33
+ });
34
+ let stdout = '';
35
+ let stderr = '';
36
+ proc.stdout.on('data', (data) => {
37
+ stdout += data.toString();
38
+ });
39
+ proc.stderr.on('data', (data) => {
40
+ stderr += data.toString();
41
+ });
42
+ proc.on('close', (exitCode) => {
43
+ resolve({ stdout, stderr, exitCode: exitCode ?? 1 });
44
+ });
45
+ proc.on('error', (err) => {
46
+ resolve({ stdout, stderr: stderr + '\n' + err.message, exitCode: 1 });
47
+ });
48
+ });
49
+ // Clean up temp file
50
+ await fs.unlink(tempFile).catch(() => { });
51
+ if (result.exitCode !== 0) {
52
+ return {
53
+ content: [{
54
+ type: "text",
55
+ text: `Execution failed (exit code ${result.exitCode}):\n${result.stderr}\n${result.stdout}`
56
+ }],
57
+ isError: true
58
+ };
59
+ }
60
+ return {
61
+ content: [{
62
+ type: "text",
63
+ text: result.stdout || '(no output)'
64
+ }]
65
+ };
66
+ }
67
+ catch (error) {
68
+ // Clean up temp file on error
69
+ await fs.unlink(tempFile).catch(() => { });
70
+ return {
71
+ content: [{
72
+ type: "text",
73
+ text: `Failed to execute Node.js code: ${error instanceof Error ? error.message : String(error)}`
74
+ }],
75
+ isError: true
76
+ };
77
+ }
78
+ }
79
+ /**
80
+ * Start a new process (renamed from execute_command)
81
+ * Includes early detection of process waiting for input
82
+ */
83
+ export async function startProcess(args) {
84
+ const parsed = StartProcessArgsSchema.safeParse(args);
85
+ if (!parsed.success) {
86
+ capture('server_start_process_failed');
87
+ return {
88
+ content: [{ type: "text", text: `Error: Invalid arguments for start_process: ${parsed.error}` }],
89
+ isError: true,
90
+ };
91
+ }
92
+ try {
93
+ const commands = commandManager.extractCommands(parsed.data.command).join(', ');
94
+ capture('server_start_process', {
95
+ command: commandManager.getBaseCommand(parsed.data.command),
96
+ commands: commands
97
+ });
98
+ }
99
+ catch (error) {
100
+ capture('server_start_process', {
101
+ command: commandManager.getBaseCommand(parsed.data.command)
102
+ });
103
+ }
104
+ const isAllowed = await commandManager.validateCommand(parsed.data.command);
105
+ if (!isAllowed) {
106
+ return {
107
+ content: [{ type: "text", text: `Error: Command not allowed: ${parsed.data.command}` }],
108
+ isError: true,
109
+ };
110
+ }
111
+ const commandToRun = parsed.data.command;
112
+ const workingDirectory = parsed.data.working_directory
113
+ ? await validatePath(parsed.data.working_directory)
114
+ : undefined;
115
+ // Handle node:local - runs Node.js code directly on MCP server
116
+ if (commandToRun.trim() === 'node:local') {
117
+ const virtualPid = virtualPidCounter--;
118
+ virtualNodeSessions.set(virtualPid, { timeout_ms: parsed.data.timeout_ms || 30000 });
119
+ return {
120
+ content: [{
121
+ type: "text",
122
+ text: `Node.js session started with PID ${virtualPid} (MCP server execution)
123
+
124
+ IMPORTANT: Each interact_with_process call runs as a FRESH script.
125
+ State is NOT preserved between calls. Include ALL code in ONE call:
126
+ - imports, file reading, processing, and output together.
127
+
128
+ Available libraries:
129
+ - ExcelJS for Excel files: import ExcelJS from 'exceljs'
130
+ - All Node.js built-ins: fs, path, http, crypto, etc.
131
+
132
+ 🔄 Ready for code - send complete self-contained script via interact_with_process.`
133
+ }],
134
+ };
135
+ }
136
+ let shellUsed = parsed.data.shell;
137
+ if (!shellUsed) {
138
+ const config = await configManager.getConfig();
139
+ if (config.defaultShell) {
140
+ shellUsed = config.defaultShell;
141
+ }
142
+ else {
143
+ const isWindows = os.platform() === 'win32';
144
+ if (isWindows && process.env.COMSPEC) {
145
+ shellUsed = process.env.COMSPEC;
146
+ }
147
+ else if (!isWindows && process.env.SHELL) {
148
+ shellUsed = process.env.SHELL;
149
+ }
150
+ else {
151
+ shellUsed = isWindows ? 'cmd.exe' : '/bin/sh';
152
+ }
153
+ }
154
+ }
155
+ const result = await terminalManager.executeCommand(commandToRun, parsed.data.timeout_ms, shellUsed, parsed.data.verbose_timing || false, workingDirectory);
156
+ if (result.pid === -1) {
157
+ return {
158
+ content: [{ type: "text", text: result.output }],
159
+ isError: true,
160
+ };
161
+ }
162
+ // Analyze the process state to detect if it's waiting for input
163
+ const processState = analyzeProcessState(result.output, result.pid);
164
+ let statusMessage = '';
165
+ if (processState.isWaitingForInput) {
166
+ statusMessage = `\n🔄 ${formatProcessStateMessage(processState, result.pid)}`;
167
+ }
168
+ else if (processState.isFinished) {
169
+ statusMessage = `\n✅ ${formatProcessStateMessage(processState, result.pid)}`;
170
+ }
171
+ else if (result.isBlocked) {
172
+ statusMessage = '\n⏳ Process is running. Use read_process_output to get more output.';
173
+ }
174
+ // Add timing information if requested
175
+ let timingMessage = '';
176
+ if (result.timingInfo) {
177
+ timingMessage = formatTimingInfo(result.timingInfo);
178
+ }
179
+ return {
180
+ content: [{
181
+ type: "text",
182
+ text: `Process started with PID ${result.pid} (shell: ${shellUsed})\nInitial output:\n${result.output}${statusMessage}${timingMessage}`
183
+ }],
184
+ };
185
+ }
186
+ function formatTimingInfo(timing) {
187
+ let msg = '\n\n📊 Timing Information:\n';
188
+ msg += ` Exit Reason: ${timing.exitReason}\n`;
189
+ msg += ` Total Duration: ${timing.totalDurationMs}ms\n`;
190
+ if (timing.timeToFirstOutputMs !== undefined) {
191
+ msg += ` Time to First Output: ${timing.timeToFirstOutputMs}ms\n`;
192
+ }
193
+ if (timing.firstOutputTime && timing.lastOutputTime) {
194
+ msg += ` Output Window: ${timing.lastOutputTime - timing.firstOutputTime}ms\n`;
195
+ }
196
+ if (timing.outputEvents && timing.outputEvents.length > 0) {
197
+ msg += `\n Output Events (${timing.outputEvents.length} total):\n`;
198
+ timing.outputEvents.forEach((event, idx) => {
199
+ msg += ` [${idx + 1}] +${event.deltaMs}ms | ${event.source} | ${event.length}b`;
200
+ if (event.matchedPattern) {
201
+ msg += ` | 🎯 ${event.matchedPattern}`;
202
+ }
203
+ msg += `\n "${event.snippet}"\n`;
204
+ });
205
+ }
206
+ return msg;
207
+ }
208
+ /**
209
+ * Read output from a running process with file-like pagination
210
+ * Supports offset/length parameters for controlled reading
211
+ */
212
+ export async function readProcessOutput(args) {
213
+ const parsed = ReadProcessOutputArgsSchema.safeParse(args);
214
+ if (!parsed.success) {
215
+ return {
216
+ content: [{ type: "text", text: `Error: Invalid arguments for read_process_output: ${parsed.error}` }],
217
+ isError: true,
218
+ };
219
+ }
220
+ // Get default line limit from config
221
+ const config = await configManager.getConfig();
222
+ const defaultLength = config.fileReadLineLimit ?? 1000;
223
+ const { pid, timeout_ms = 5000, offset = 0, // 0 = from last read, positive = absolute, negative = tail
224
+ length = defaultLength, // Default from config, same as file reading
225
+ verbose_timing = false } = parsed.data;
226
+ // Timing telemetry
227
+ const startTime = Date.now();
228
+ // For active sessions with no new output yet, optionally wait for output
229
+ const session = terminalManager.getSession(pid);
230
+ if (session && offset === 0) {
231
+ // Wait for new output to arrive (only for "new output" reads, not absolute/tail)
232
+ const waitForOutput = () => {
233
+ return new Promise((resolve) => {
234
+ // Check if there's already new output
235
+ const currentLines = terminalManager.getOutputLineCount(pid) || 0;
236
+ if (currentLines > session.lastReadIndex) {
237
+ resolve();
238
+ return;
239
+ }
240
+ let resolved = false;
241
+ let interval = null;
242
+ let timeout = null;
243
+ const cleanup = () => {
244
+ if (interval)
245
+ clearInterval(interval);
246
+ if (timeout)
247
+ clearTimeout(timeout);
248
+ };
249
+ const resolveOnce = () => {
250
+ if (resolved)
251
+ return;
252
+ resolved = true;
253
+ cleanup();
254
+ resolve();
255
+ };
256
+ // Poll for new output
257
+ interval = setInterval(() => {
258
+ const newLineCount = terminalManager.getOutputLineCount(pid) || 0;
259
+ if (newLineCount > session.lastReadIndex) {
260
+ resolveOnce();
261
+ }
262
+ }, 50);
263
+ // Timeout
264
+ timeout = setTimeout(() => {
265
+ resolveOnce();
266
+ }, timeout_ms);
267
+ });
268
+ };
269
+ await waitForOutput();
270
+ }
271
+ // Read output with pagination
272
+ const result = terminalManager.readOutputPaginated(pid, offset, length);
273
+ if (!result) {
274
+ return {
275
+ content: [{ type: "text", text: `No session found for PID ${pid}` }],
276
+ isError: true,
277
+ };
278
+ }
279
+ // Join lines back into string
280
+ const output = result.lines.join('\n');
281
+ // Generate status message similar to file reading
282
+ let statusMessage = '';
283
+ if (offset < 0) {
284
+ // Tail read - match file reading format for consistency
285
+ statusMessage = `[Reading last ${result.readCount} lines (total: ${result.totalLines} lines)]`;
286
+ }
287
+ else if (offset === 0) {
288
+ // "New output" read
289
+ if (result.remaining > 0) {
290
+ statusMessage = `[Reading ${result.readCount} new lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
291
+ }
292
+ else {
293
+ statusMessage = `[Reading ${result.readCount} new lines (total: ${result.totalLines} lines)]`;
294
+ }
295
+ }
296
+ else {
297
+ // Absolute position read
298
+ statusMessage = `[Reading ${result.readCount} lines from line ${result.readFrom} (total: ${result.totalLines} lines, ${result.remaining} remaining)]`;
299
+ }
300
+ // Surface buffer-cap eviction so the model knows the retained output is not
301
+ // the full output and that line numbers shifted (matches the truncation
302
+ // markers used by other tools).
303
+ if (result.evictedLines && result.evictedLines > 0) {
304
+ const capMB = Math.round(MAX_BUFFERED_OUTPUT_CHARS / 1024 / 1024);
305
+ statusMessage += `\n[WARNING: output exceeded the ${capMB}MB buffer cap; the ${result.evictedLines} earliest lines were evicted and cannot be read. Line numbers and totals refer to the retained buffer only]`;
306
+ }
307
+ // Add process state info
308
+ let processStateMessage = '';
309
+ if (result.isComplete) {
310
+ const runtimeStr = result.runtimeMs !== undefined
311
+ ? ` (runtime: ${(result.runtimeMs / 1000).toFixed(2)}s)`
312
+ : '';
313
+ processStateMessage = `\n✅ Process completed with exit code ${result.exitCode}${runtimeStr}`;
314
+ }
315
+ else if (session) {
316
+ // Analyze state for running processes
317
+ const fullOutput = session.outputLines.join('\n');
318
+ const processState = analyzeProcessState(fullOutput, pid);
319
+ if (processState.isWaitingForInput) {
320
+ processStateMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
321
+ }
322
+ }
323
+ // Add timing information if requested
324
+ let timingMessage = '';
325
+ if (verbose_timing) {
326
+ const endTime = Date.now();
327
+ timingMessage = `\n\n📊 Timing: ${endTime - startTime}ms`;
328
+ }
329
+ const responseText = output || '(No output in requested range)';
330
+ return {
331
+ content: [{
332
+ type: "text",
333
+ text: `${statusMessage}\n\n${responseText}${processStateMessage}${timingMessage}`
334
+ }],
335
+ };
336
+ }
337
+ /**
338
+ * Interact with a running process (renamed from send_input)
339
+ * Automatically detects when process is ready and returns output
340
+ */
341
+ export async function interactWithProcess(args) {
342
+ const parsed = InteractWithProcessArgsSchema.safeParse(args);
343
+ if (!parsed.success) {
344
+ capture('server_interact_with_process_failed', {
345
+ error: 'Invalid arguments'
346
+ });
347
+ return {
348
+ content: [{ type: "text", text: `Error: Invalid arguments for interact_with_process: ${parsed.error}` }],
349
+ isError: true,
350
+ };
351
+ }
352
+ const { pid, input, timeout_ms = 8000, wait_for_prompt = true, verbose_timing = false } = parsed.data;
353
+ // Get config for output line limit
354
+ const config = await configManager.getConfig();
355
+ const maxOutputLines = config.fileReadLineLimit ?? 1000;
356
+ // Check if this is a virtual Node session (node:local)
357
+ if (virtualNodeSessions.has(pid)) {
358
+ const session = virtualNodeSessions.get(pid);
359
+ capture('server_interact_with_process_node_fallback', {
360
+ pid: pid,
361
+ inputLength: input.length
362
+ });
363
+ // Execute code via temp file approach
364
+ // Respect per-call timeout if provided, otherwise use session default
365
+ const effectiveTimeout = timeout_ms ?? session.timeout_ms;
366
+ return executeNodeCode(input, effectiveTimeout);
367
+ }
368
+ // Timing telemetry
369
+ const startTime = Date.now();
370
+ let firstOutputTime;
371
+ let lastOutputTime;
372
+ const outputEvents = [];
373
+ let exitReason = 'timeout';
374
+ try {
375
+ capture('server_interact_with_process', {
376
+ pid: pid,
377
+ inputLength: input.length
378
+ });
379
+ // Capture output snapshot BEFORE sending input
380
+ // This handles REPLs where output is appended to the prompt line
381
+ const outputSnapshot = terminalManager.captureOutputSnapshot(pid);
382
+ const success = terminalManager.sendInputToProcess(pid, input);
383
+ if (!success) {
384
+ return {
385
+ content: [{ type: "text", text: `Error: Failed to send input to process ${pid}. The process may have exited or doesn't accept input.` }],
386
+ isError: true,
387
+ };
388
+ }
389
+ // If not waiting for response, return immediately
390
+ if (!wait_for_prompt) {
391
+ exitReason = 'no_wait';
392
+ let timingMessage = '';
393
+ if (verbose_timing) {
394
+ const endTime = Date.now();
395
+ const timingInfo = {
396
+ startTime,
397
+ endTime,
398
+ totalDurationMs: endTime - startTime,
399
+ exitReason,
400
+ firstOutputTime,
401
+ lastOutputTime,
402
+ timeToFirstOutputMs: undefined,
403
+ outputEvents: undefined
404
+ };
405
+ timingMessage = formatTimingInfo(timingInfo);
406
+ }
407
+ return {
408
+ content: [{
409
+ type: "text",
410
+ text: `✅ Input sent to process ${pid}. Use read_process_output to get the response.${timingMessage}`
411
+ }],
412
+ };
413
+ }
414
+ // Smart waiting with immediate and periodic detection
415
+ let output = "";
416
+ let processState;
417
+ let earlyExit = false;
418
+ // Quick prompt patterns for immediate detection
419
+ const quickPromptPatterns = />>>\s*$|>\s*$|\$\s*$|#\s*$/;
420
+ const waitForResponse = () => {
421
+ return new Promise((resolve) => {
422
+ let resolved = false;
423
+ let attempts = 0;
424
+ const pollIntervalMs = 50; // Poll every 50ms for faster response
425
+ const maxAttempts = Math.ceil(timeout_ms / pollIntervalMs);
426
+ let interval = null;
427
+ let lastOutputLength = 0; // Track output length to detect new output
428
+ let resolveOnce = () => {
429
+ if (resolved)
430
+ return;
431
+ resolved = true;
432
+ if (interval)
433
+ clearInterval(interval);
434
+ resolve();
435
+ };
436
+ // Fast-polling check - check every 50ms for quick responses
437
+ interval = setInterval(() => {
438
+ if (resolved)
439
+ return;
440
+ // Use snapshot-based reading to handle REPL prompt line appending
441
+ const newOutput = outputSnapshot
442
+ ? terminalManager.getOutputSinceSnapshot(pid, outputSnapshot)
443
+ : terminalManager.getNewOutput(pid);
444
+ if (newOutput && newOutput.length > lastOutputLength) {
445
+ const now = Date.now();
446
+ if (!firstOutputTime)
447
+ firstOutputTime = now;
448
+ lastOutputTime = now;
449
+ if (verbose_timing) {
450
+ outputEvents.push({
451
+ timestamp: now,
452
+ deltaMs: now - startTime,
453
+ source: 'periodic_poll',
454
+ length: newOutput.length - lastOutputLength,
455
+ snippet: newOutput.slice(lastOutputLength, lastOutputLength + 50).replace(/\n/g, '\\n')
456
+ });
457
+ }
458
+ output = newOutput; // Replace with full output since snapshot
459
+ lastOutputLength = newOutput.length;
460
+ // Analyze current state
461
+ processState = analyzeProcessState(output, pid);
462
+ // Exit early if we detect the process is waiting for input
463
+ if (processState.isWaitingForInput) {
464
+ earlyExit = true;
465
+ exitReason = 'early_exit_periodic_check';
466
+ if (verbose_timing && outputEvents.length > 0) {
467
+ outputEvents[outputEvents.length - 1].matchedPattern = 'periodic_check';
468
+ }
469
+ resolveOnce();
470
+ return;
471
+ }
472
+ // Also exit if process finished
473
+ if (processState.isFinished) {
474
+ exitReason = 'process_finished';
475
+ resolveOnce();
476
+ return;
477
+ }
478
+ }
479
+ attempts++;
480
+ if (attempts >= maxAttempts) {
481
+ exitReason = 'timeout';
482
+ resolveOnce();
483
+ }
484
+ }, pollIntervalMs);
485
+ });
486
+ };
487
+ await waitForResponse();
488
+ // Clean and format output
489
+ let cleanOutput = cleanProcessOutput(output, input);
490
+ const timeoutReached = !earlyExit && !processState?.isFinished && !processState?.isWaitingForInput;
491
+ // Apply output line limit to prevent context overflow
492
+ let truncationMessage = '';
493
+ const outputLines = cleanOutput.split('\n');
494
+ if (outputLines.length > maxOutputLines) {
495
+ const truncatedLines = outputLines.slice(0, maxOutputLines);
496
+ cleanOutput = truncatedLines.join('\n');
497
+ const remainingLines = outputLines.length - maxOutputLines;
498
+ truncationMessage = `\n\n⚠️ Output truncated: showing ${maxOutputLines} of ${outputLines.length} lines (${remainingLines} hidden). Use read_process_output with offset/length for full output.`;
499
+ }
500
+ // Determine final state
501
+ if (!processState) {
502
+ processState = analyzeProcessState(output, pid);
503
+ }
504
+ let statusMessage = '';
505
+ if (processState.isWaitingForInput) {
506
+ statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
507
+ }
508
+ else if (processState.isFinished) {
509
+ statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`;
510
+ }
511
+ else if (timeoutReached) {
512
+ statusMessage = '\n⏱️ Response may be incomplete (timeout reached)';
513
+ }
514
+ // Add timing information if requested
515
+ let timingMessage = '';
516
+ if (verbose_timing) {
517
+ const endTime = Date.now();
518
+ const timingInfo = {
519
+ startTime,
520
+ endTime,
521
+ totalDurationMs: endTime - startTime,
522
+ exitReason,
523
+ firstOutputTime,
524
+ lastOutputTime,
525
+ timeToFirstOutputMs: firstOutputTime ? firstOutputTime - startTime : undefined,
526
+ outputEvents: outputEvents.length > 0 ? outputEvents : undefined
527
+ };
528
+ timingMessage = formatTimingInfo(timingInfo);
529
+ }
530
+ if (cleanOutput.trim().length === 0 && !timeoutReached) {
531
+ return {
532
+ content: [{
533
+ type: "text",
534
+ text: `✅ Input executed in process ${pid}.\n📭 (No output produced)${statusMessage}${timingMessage}`
535
+ }],
536
+ };
537
+ }
538
+ // Format response with better structure and consistent emojis
539
+ let responseText = `✅ Input executed in process ${pid}`;
540
+ if (cleanOutput && cleanOutput.trim().length > 0) {
541
+ responseText += `:\n\n📤 Output:\n${cleanOutput}`;
542
+ }
543
+ else {
544
+ responseText += `.\n📭 (No output produced)`;
545
+ }
546
+ if (statusMessage) {
547
+ responseText += `\n\n${statusMessage}`;
548
+ }
549
+ if (truncationMessage) {
550
+ responseText += truncationMessage;
551
+ }
552
+ if (timingMessage) {
553
+ responseText += timingMessage;
554
+ }
555
+ return {
556
+ content: [{
557
+ type: "text",
558
+ text: responseText
559
+ }],
560
+ };
561
+ }
562
+ catch (error) {
563
+ const errorMessage = error instanceof Error ? error.message : String(error);
564
+ capture('server_interact_with_process_error', {
565
+ error: errorMessage
566
+ });
567
+ return {
568
+ content: [{ type: "text", text: `Error interacting with process: ${errorMessage}` }],
569
+ isError: true,
570
+ };
571
+ }
572
+ }
573
+ /**
574
+ * Force terminate a process
575
+ */
576
+ export async function forceTerminate(args) {
577
+ const parsed = ForceTerminateArgsSchema.safeParse(args);
578
+ if (!parsed.success) {
579
+ return {
580
+ content: [{ type: "text", text: `Error: Invalid arguments for force_terminate: ${parsed.error}` }],
581
+ isError: true,
582
+ };
583
+ }
584
+ const pid = parsed.data.pid;
585
+ // Handle virtual Node.js sessions (node:local)
586
+ if (virtualNodeSessions.has(pid)) {
587
+ virtualNodeSessions.delete(pid);
588
+ return {
589
+ content: [{
590
+ type: "text",
591
+ text: `Cleared virtual Node.js session ${pid}`
592
+ }],
593
+ };
594
+ }
595
+ const success = terminalManager.forceTerminate(pid);
596
+ return {
597
+ content: [{
598
+ type: "text",
599
+ text: success
600
+ ? `Successfully initiated termination of session ${pid}`
601
+ : `No active session found for PID ${pid}`
602
+ }],
603
+ };
604
+ }
605
+ /**
606
+ * List active sessions
607
+ */
608
+ export async function listSessions() {
609
+ const sessions = terminalManager.listActiveSessions();
610
+ // Include virtual Node.js sessions
611
+ const virtualSessions = Array.from(virtualNodeSessions.entries()).map(([pid, session]) => ({
612
+ pid,
613
+ type: 'node:local',
614
+ timeout_ms: session.timeout_ms
615
+ }));
616
+ const realSessionsText = sessions.map(s => `PID: ${s.pid}, Blocked: ${s.isBlocked}, Runtime: ${Math.round(s.runtime / 1000)}s`);
617
+ const virtualSessionsText = virtualSessions.map(s => `PID: ${s.pid} (node:local), Timeout: ${s.timeout_ms}ms`);
618
+ const allSessions = [...realSessionsText, ...virtualSessionsText];
619
+ return {
620
+ content: [{
621
+ type: "text",
622
+ text: allSessions.length === 0
623
+ ? 'No active sessions'
624
+ : allSessions.join('\n')
625
+ }],
626
+ };
627
+ }
@@ -0,0 +1,3 @@
1
+ export declare function getMimeType(filePath: string): string;
2
+ export declare function isPdfFile(mimeType: string): boolean;
3
+ export declare function isImageFile(mimeType: string): boolean;
@@ -0,0 +1,28 @@
1
+ // Simple MIME type detection based on file extension
2
+ export function getMimeType(filePath) {
3
+ const extension = filePath.toLowerCase().split('.').pop() || '';
4
+ if (extension === "pdf") {
5
+ return "application/pdf";
6
+ }
7
+ // Image types - only the formats we can display
8
+ const imageTypes = {
9
+ 'png': 'image/png',
10
+ 'jpg': 'image/jpeg',
11
+ 'jpeg': 'image/jpeg',
12
+ 'gif': 'image/gif',
13
+ 'webp': 'image/webp'
14
+ };
15
+ // Check if the file is an image
16
+ if (extension in imageTypes) {
17
+ return imageTypes[extension];
18
+ }
19
+ // Default to text/plain for all other files
20
+ return 'text/plain';
21
+ }
22
+ export function isPdfFile(mimeType) {
23
+ const [baseType] = mimeType.toLowerCase().split(';');
24
+ return baseType.trim() === 'application/pdf';
25
+ }
26
+ export function isImageFile(mimeType) {
27
+ return mimeType.startsWith('image/');
28
+ }
@@ -0,0 +1,34 @@
1
+ export interface ImageInfo {
2
+ /** Object ID within PDF */
3
+ objId: number;
4
+ width: number;
5
+ height: number;
6
+ /** Raw image data as base64 */
7
+ data: string;
8
+ /** MIME type of the image */
9
+ mimeType: string;
10
+ /** Original size in bytes before compression */
11
+ originalSize?: number;
12
+ /** Compressed size in bytes */
13
+ compressedSize?: number;
14
+ }
15
+ export interface PageImages {
16
+ pageNumber: number;
17
+ images: ImageInfo[];
18
+ }
19
+ export interface ImageCompressionOptions {
20
+ /** Output format: 'jpeg' | 'webp' */
21
+ format?: 'jpeg' | 'webp';
22
+ /** Quality for lossy formats (0-100, default 85) */
23
+ quality?: number;
24
+ /** Maximum dimension to resize to (maintains aspect ratio) */
25
+ maxDimension?: number;
26
+ }
27
+ /**
28
+ * Optimized image extraction from PDF using unpdf's built-in extractImages method
29
+ * @param pdfBuffer PDF file as Uint8Array
30
+ * @param pageNumbers Optional array of specific page numbers to process
31
+ * @param compressionOptions Image compression settings
32
+ * @returns Record of page numbers to extracted images
33
+ */
34
+ export declare function extractImagesFromPdf(pdfBuffer: Uint8Array, pageNumbers?: number[], compressionOptions?: ImageCompressionOptions): Promise<Record<number, ImageInfo[]>>;