@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,435 @@
1
+ /**
2
+ * Text file editing via search/replace with fuzzy matching support.
3
+ *
4
+ * TECHNICAL DEBT / ARCHITECTURAL NOTE:
5
+ * This file contains text editing logic that should ideally live in TextFileHandler.editRange()
6
+ * to be consistent with how Excel editing works (ExcelFileHandler.editRange()).
7
+ *
8
+ * Current inconsistency:
9
+ * - Excel: edit_block → ExcelFileHandler.editRange() ✓ uses file handler
10
+ * - Text: edit_block → performSearchReplace() here → bypasses TextFileHandler
11
+ *
12
+ * Future refactor should:
13
+ * 1. Move performSearchReplace() + fuzzy logic into TextFileHandler.editRange()
14
+ * 2. Make this file a thin dispatch layer that routes to appropriate FileHandler
15
+ * 3. Unify the editRange() signature to handle both text search/replace and structured edits
16
+ */
17
+ import { getDefaultEditorMetadata, writeFile, readFileInternal, validatePath } from './filesystem.js';
18
+ import { runFuzzySearchInWorker, getSimilarityRatio } from './fuzzySearch.js';
19
+ import { capture } from '../utils/capture.js';
20
+ import { createErrorResponse } from '../error-handlers.js';
21
+ import { EditBlockArgsSchema } from "./schemas.js";
22
+ import path from 'path';
23
+ import { detectLineEnding, normalizeLineEndings } from '../utils/lineEndingHandler.js';
24
+ import { configManager } from '../config-manager.js';
25
+ import { fuzzySearchLogger } from '../utils/fuzzySearchLogger.js';
26
+ import { resolvePreviewFileType } from '../ui/file-preview/shared/preview-file-types.js';
27
+ import { resolveAbsolutePath } from '../handlers/filesystem-handlers.js';
28
+ /**
29
+ * Threshold for fuzzy matching - similarity must be at least this value to be considered
30
+ * (0-1 scale where 1 is perfect match and 0 is completely different)
31
+ */
32
+ const FUZZY_THRESHOLD = 0.7;
33
+ /**
34
+ * Extract character code data from diff
35
+ * @param expected The string that was searched for
36
+ * @param actual The string that was found
37
+ * @returns Character code statistics
38
+ */
39
+ function getCharacterCodeData(expected, actual) {
40
+ // Find common prefix and suffix
41
+ let prefixLength = 0;
42
+ const minLength = Math.min(expected.length, actual.length);
43
+ // Determine common prefix length
44
+ while (prefixLength < minLength &&
45
+ expected[prefixLength] === actual[prefixLength]) {
46
+ prefixLength++;
47
+ }
48
+ // Determine common suffix length
49
+ let suffixLength = 0;
50
+ while (suffixLength < minLength - prefixLength &&
51
+ expected[expected.length - 1 - suffixLength] === actual[actual.length - 1 - suffixLength]) {
52
+ suffixLength++;
53
+ }
54
+ // Extract the different parts
55
+ const expectedDiff = expected.substring(prefixLength, expected.length - suffixLength);
56
+ const actualDiff = actual.substring(prefixLength, actual.length - suffixLength);
57
+ // Count unique character codes in the diff
58
+ const characterCodes = new Map();
59
+ const fullDiff = expectedDiff + actualDiff;
60
+ for (let i = 0; i < fullDiff.length; i++) {
61
+ const charCode = fullDiff.charCodeAt(i);
62
+ characterCodes.set(charCode, (characterCodes.get(charCode) || 0) + 1);
63
+ }
64
+ // Create character codes string report
65
+ const charCodeReport = [];
66
+ characterCodes.forEach((count, code) => {
67
+ // Include character representation for better readability
68
+ const char = String.fromCharCode(code);
69
+ // Make special characters more readable
70
+ const charDisplay = code < 32 || code > 126 ? `\\x${code.toString(16).padStart(2, '0')}` : char;
71
+ charCodeReport.push(`${code}:${count}[${charDisplay}]`);
72
+ });
73
+ // Sort by character code for consistency
74
+ charCodeReport.sort((a, b) => {
75
+ const codeA = parseInt(a.split(':')[0]);
76
+ const codeB = parseInt(b.split(':')[0]);
77
+ return codeA - codeB;
78
+ });
79
+ return {
80
+ report: charCodeReport.join(','),
81
+ uniqueCount: characterCodes.size,
82
+ diffLength: fullDiff.length
83
+ };
84
+ }
85
+ export async function performSearchReplace(filePath, block, expectedReplacements = 1, origin) {
86
+ // Get file extension for telemetry using path module
87
+ const fileExtension = path.extname(filePath).toLowerCase();
88
+ // Capture file extension and string sizes in telemetry without capturing the file path
89
+ capture('server_edit_block', {
90
+ fileExtension: fileExtension,
91
+ oldStringLength: block.search.length,
92
+ oldStringLines: block.search.split('\n').length,
93
+ newStringLength: block.replace.length,
94
+ newStringLines: block.replace.split('\n').length,
95
+ expectedReplacements: expectedReplacements
96
+ });
97
+ // Check for empty search string to prevent infinite loops
98
+ if (block.search === "") {
99
+ // Capture file extension in telemetry without capturing the file path
100
+ capture('server_edit_block_empty_search', { fileExtension: fileExtension, expectedReplacements });
101
+ return {
102
+ content: [{
103
+ type: "text",
104
+ text: "Empty search strings are not allowed. Please provide a non-empty string to search for."
105
+ }],
106
+ };
107
+ }
108
+ // Read file directly to preserve line endings - critical for edit operations
109
+ const validPath = await validatePath(filePath);
110
+ const content = await readFileInternal(validPath, 0, Number.MAX_SAFE_INTEGER);
111
+ // Make sure content is a string
112
+ if (typeof content !== 'string') {
113
+ capture('server_edit_block_content_not_string', { fileExtension: fileExtension, expectedReplacements });
114
+ throw new Error('Wrong content for file ' + filePath);
115
+ }
116
+ // Get the line limit from configuration
117
+ const config = await configManager.getConfig();
118
+ const MAX_LINES = config.fileWriteLineLimit ?? 50; // Default to 50 if not set
119
+ // Detect file's line ending style
120
+ const fileLineEnding = detectLineEnding(content);
121
+ // Normalize search string to match file's line endings
122
+ const normalizedSearch = normalizeLineEndings(block.search, fileLineEnding);
123
+ // First try exact match
124
+ let tempContent = content;
125
+ let count = 0;
126
+ let pos = tempContent.indexOf(normalizedSearch);
127
+ while (pos !== -1) {
128
+ count++;
129
+ pos = tempContent.indexOf(normalizedSearch, pos + 1);
130
+ }
131
+ // If exact match found and count matches expected replacements, proceed with exact replacement
132
+ if (count > 0 && count === expectedReplacements) {
133
+ // Replace all occurrences
134
+ let newContent = content;
135
+ // If we're only replacing one occurrence, replace it directly
136
+ if (expectedReplacements === 1) {
137
+ const searchIndex = newContent.indexOf(normalizedSearch);
138
+ newContent =
139
+ newContent.substring(0, searchIndex) +
140
+ normalizeLineEndings(block.replace, fileLineEnding) +
141
+ newContent.substring(searchIndex + normalizedSearch.length);
142
+ }
143
+ else {
144
+ // Replace all occurrences using split and join for multiple replacements
145
+ newContent = newContent.split(normalizedSearch).join(normalizeLineEndings(block.replace, fileLineEnding));
146
+ }
147
+ // Check if search or replace text has too many lines
148
+ const searchLines = block.search.split('\n').length;
149
+ const replaceLines = block.replace.split('\n').length;
150
+ const maxLines = Math.max(searchLines, replaceLines);
151
+ let warningMessage = "";
152
+ if (maxLines > MAX_LINES) {
153
+ const problemText = searchLines > replaceLines ? 'search text' : 'replacement text';
154
+ warningMessage = `\n\nWARNING: The ${problemText} has ${maxLines} lines (maximum: ${MAX_LINES}).
155
+
156
+ RECOMMENDATION: For large search/replace operations, consider breaking them into smaller chunks with fewer lines.`;
157
+ }
158
+ await writeFile(filePath, newContent);
159
+ capture('server_edit_block_exact_success', { fileExtension: fileExtension, expectedReplacements, hasWarning: warningMessage !== "" });
160
+ const resolvedEditPath = resolveAbsolutePath(filePath);
161
+ // Show a partial preview centered on the edited area
162
+ const newLines = newContent.split('\n');
163
+ const totalLines = newLines.length;
164
+ const changePos = content.indexOf(normalizedSearch);
165
+ const changeStartLine = changePos >= 0 ? newContent.substring(0, changePos).split('\n').length - 1 : 0;
166
+ const changeLineCount = block.replace.split('\n').length;
167
+ const contextLines = 10;
168
+ const previewStart = Math.max(0, changeStartLine - contextLines);
169
+ const previewEnd = Math.min(totalLines, changeStartLine + changeLineCount + contextLines);
170
+ const previewContent = newLines.slice(previewStart, previewEnd).join('\n');
171
+ const previewLineCount = previewEnd - previewStart;
172
+ const remaining = totalLines - previewEnd;
173
+ const statusLine = `[Reading ${previewLineCount} lines from ${previewStart === 0 ? 'start' : `line ${previewStart}`} (total: ${totalLines} lines, ${remaining} remaining)]\n\n`;
174
+ return {
175
+ content: [{
176
+ type: "text",
177
+ text: `${statusLine}${previewContent}`
178
+ }],
179
+ // structuredContent only travels on widget-initiated calls: it is the
180
+ // widget's save-success signal (assertSuccessfulEditBlockResult) and
181
+ // metadata source. LLM-facing responses don't need it — nothing there
182
+ // consumes it, and the widget re-reads by path instead.
183
+ ...(origin === 'ui' ? {
184
+ structuredContent: {
185
+ fileName: path.basename(resolvedEditPath),
186
+ filePath: resolvedEditPath,
187
+ fileType: resolvePreviewFileType(resolvedEditPath),
188
+ ...await getDefaultEditorMetadata(resolvedEditPath),
189
+ },
190
+ } : {}),
191
+ };
192
+ }
193
+ // If exact match found but count doesn't match expected, inform the user
194
+ if (count > 0 && count !== expectedReplacements) {
195
+ capture('server_edit_block_unexpected_count', { fileExtension: fileExtension, expectedReplacements, expectedReplacementsCount: count });
196
+ return {
197
+ content: [{
198
+ type: "text",
199
+ text: `Expected ${expectedReplacements} occurrences but found ${count} in ${filePath}. ` +
200
+ `Double check and make sure you understand all occurencies and if you want to replace all ${count} occurrences, set expected_replacements to ${count}. ` +
201
+ `If there are many occurrancies and you want to change some of them and keep the rest. Do it one by one, by adding more lines around each occurrence.` +
202
+ `If you want to replace a specific occurrence, make your search string more unique by adding more lines around search string.`
203
+ }],
204
+ };
205
+ }
206
+ // If exact match not found, try fuzzy search
207
+ if (count === 0) {
208
+ // Track fuzzy search time
209
+ const startTime = performance.now();
210
+ // Perform fuzzy search in a worker thread so the main event loop stays
211
+ // responsive to pings and parallel tool calls during the scan
212
+ const fuzzyResult = await runFuzzySearchInWorker(content, block.search);
213
+ const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
214
+ // Calculate execution time in milliseconds
215
+ const executionTime = performance.now() - startTime;
216
+ // Generate diff and gather character code data
217
+ const diff = highlightDifferences(block.search, fuzzyResult.value);
218
+ // Count character codes in diff
219
+ const characterCodeData = getCharacterCodeData(block.search, fuzzyResult.value);
220
+ // Create comprehensive log entry
221
+ const logEntry = {
222
+ timestamp: new Date(),
223
+ searchText: block.search,
224
+ foundText: fuzzyResult.value,
225
+ similarity: similarity,
226
+ executionTime: executionTime,
227
+ exactMatchCount: count,
228
+ expectedReplacements: expectedReplacements,
229
+ fuzzyThreshold: FUZZY_THRESHOLD,
230
+ belowThreshold: similarity < FUZZY_THRESHOLD,
231
+ diff: diff,
232
+ searchLength: block.search.length,
233
+ foundLength: fuzzyResult.value.length,
234
+ fileExtension: fileExtension,
235
+ characterCodes: characterCodeData.report,
236
+ uniqueCharacterCount: characterCodeData.uniqueCount,
237
+ diffLength: characterCodeData.diffLength
238
+ };
239
+ // Log to file
240
+ await fuzzySearchLogger.log(logEntry);
241
+ // Combine all fuzzy search data for single capture
242
+ const fuzzySearchData = {
243
+ similarity: similarity,
244
+ execution_time_ms: executionTime,
245
+ search_length: block.search.length,
246
+ file_size: content.length,
247
+ threshold: FUZZY_THRESHOLD,
248
+ found_text_length: fuzzyResult.value.length,
249
+ character_codes: characterCodeData.report,
250
+ unique_character_count: characterCodeData.uniqueCount,
251
+ total_diff_length: characterCodeData.diffLength
252
+ };
253
+ // Check if the fuzzy match is "close enough"
254
+ if (similarity >= FUZZY_THRESHOLD) {
255
+ // Capture the fuzzy search event with all data
256
+ capture('server_fuzzy_search_performed', fuzzySearchData);
257
+ // If we allow fuzzy matches, we would make the replacement here
258
+ // For now, we'll return a detailed message about the fuzzy match
259
+ return {
260
+ content: [{
261
+ type: "text",
262
+ text: `Exact match not found, but found a similar text with ${Math.round(similarity * 100)}% similarity (found in ${executionTime.toFixed(2)}ms):\n\n` +
263
+ `Differences:\n${diff}\n\n` +
264
+ `To replace this text, use the exact text found in the file.\n\n` +
265
+ `Log entry saved for analysis. Use the following command to check the log:\n` +
266
+ `Check log: ${await fuzzySearchLogger.getLogPath()}`
267
+ }], // TODO
268
+ };
269
+ }
270
+ else {
271
+ // If the fuzzy match isn't close enough
272
+ // Still capture the fuzzy search event with all data
273
+ capture('server_fuzzy_search_performed', {
274
+ ...fuzzySearchData,
275
+ below_threshold: true
276
+ });
277
+ return {
278
+ content: [{
279
+ type: "text",
280
+ text: `Search content not found in ${filePath}. The closest match was "${fuzzyResult.value}" ` +
281
+ `with only ${Math.round(similarity * 100)}% similarity, which is below the ${Math.round(FUZZY_THRESHOLD * 100)}% threshold. ` +
282
+ `(Fuzzy search completed in ${executionTime.toFixed(2)}ms)\n\n` +
283
+ `Log entry saved for analysis. Use the following command to check the log:\n` +
284
+ `Check log: ${await fuzzySearchLogger.getLogPath()}`
285
+ }],
286
+ };
287
+ }
288
+ }
289
+ throw new Error("Unexpected error during search and replace operation.");
290
+ }
291
+ /**
292
+ * Generates a character-level diff using standard {-removed-}{+added+} format
293
+ * @param expected The string that was searched for
294
+ * @param actual The string that was found
295
+ * @returns A formatted string showing character-level differences
296
+ */
297
+ function highlightDifferences(expected, actual) {
298
+ // Implementation of a simplified character-level diff
299
+ // Find common prefix and suffix
300
+ let prefixLength = 0;
301
+ const minLength = Math.min(expected.length, actual.length);
302
+ // Determine common prefix length
303
+ while (prefixLength < minLength &&
304
+ expected[prefixLength] === actual[prefixLength]) {
305
+ prefixLength++;
306
+ }
307
+ // Determine common suffix length
308
+ let suffixLength = 0;
309
+ while (suffixLength < minLength - prefixLength &&
310
+ expected[expected.length - 1 - suffixLength] === actual[actual.length - 1 - suffixLength]) {
311
+ suffixLength++;
312
+ }
313
+ // Extract the common and different parts
314
+ const commonPrefix = expected.substring(0, prefixLength);
315
+ const commonSuffix = expected.substring(expected.length - suffixLength);
316
+ const expectedDiff = expected.substring(prefixLength, expected.length - suffixLength);
317
+ const actualDiff = actual.substring(prefixLength, actual.length - suffixLength);
318
+ // Format the output as a character-level diff
319
+ return `${commonPrefix}{-${expectedDiff}-}{+${actualDiff}+}${commonSuffix}`;
320
+ }
321
+ /**
322
+ * Handle edit_block command
323
+ *
324
+ * 1. Text files: String replacement (old_string/new_string)
325
+ * - Uses fuzzy matching for resilience
326
+ * - Handles expected_replacements parameter
327
+ *
328
+ * 2. Structured files (Excel): Range rewrite (range + content)
329
+ * - Bulk updates to cell ranges (e.g., "Sheet1!A1:C10")
330
+ * - Whole sheet replacement (e.g., "Sheet1")
331
+ * - More powerful and simpler than surgical location-based edits
332
+ * - Supports chunking for large datasets (e.g., 1000 rows at a time)
333
+
334
+ */
335
+ export async function handleEditBlock(args) {
336
+ const parsed = EditBlockArgsSchema.parse(args);
337
+ // Note: Check for truthy range to handle empty strings from AI clients that send all optional params
338
+ const hasRange = parsed.range !== undefined && parsed.range !== '';
339
+ const hasContent = parsed.content !== undefined && parsed.content !== '';
340
+ // Validate path and resolve handler once — used by both dispatch paths below
341
+ let validatedPath;
342
+ let handler;
343
+ try {
344
+ validatedPath = await validatePath(parsed.file_path);
345
+ const { getFileHandler } = await import('../utils/files/factory.js');
346
+ handler = await getFileHandler(validatedPath);
347
+ }
348
+ catch (error) {
349
+ const errorMessage = error instanceof Error ? error.message : String(error);
350
+ return createErrorResponse(errorMessage);
351
+ }
352
+ const hasEditRange = 'editRange' in handler && typeof handler.editRange === 'function';
353
+ // Path 1: Range rewrite (Excel, etc.) — range + content
354
+ if (hasRange && hasContent) {
355
+ // Parse content if it's a JSON string (AI often sends arrays as JSON strings)
356
+ let content = parsed.content;
357
+ if (typeof content === 'string') {
358
+ try {
359
+ content = JSON.parse(content);
360
+ }
361
+ catch {
362
+ // Leave as-is if not valid JSON - let handler decide
363
+ }
364
+ }
365
+ if (hasEditRange) {
366
+ try {
367
+ // parsed.range is guaranteed non-empty string by hasRange check above
368
+ await handler.editRange(validatedPath, parsed.range, content, parsed.options);
369
+ const resolvedRangePath = resolveAbsolutePath(parsed.file_path);
370
+ return {
371
+ content: [{
372
+ type: "text",
373
+ text: `Successfully updated range ${parsed.range} in ${parsed.file_path}`
374
+ }],
375
+ ...(parsed.origin === 'ui' ? {
376
+ structuredContent: {
377
+ fileName: path.basename(resolvedRangePath),
378
+ filePath: resolvedRangePath,
379
+ fileType: resolvePreviewFileType(resolvedRangePath),
380
+ ...await getDefaultEditorMetadata(resolvedRangePath),
381
+ },
382
+ } : {}),
383
+ };
384
+ }
385
+ catch (error) {
386
+ const errorMessage = error instanceof Error ? error.message : String(error);
387
+ return createErrorResponse(errorMessage);
388
+ }
389
+ }
390
+ return createErrorResponse(`Range-based editing not supported for ${parsed.file_path}. For text files, use old_string and new_string parameters instead. If your client requires range/content parameters, set them to empty strings ("").`);
391
+ }
392
+ // Path 2: Text replacement — old_string + new_string
393
+ if (parsed.old_string === undefined || parsed.new_string === undefined) {
394
+ return createErrorResponse(`Text replacement requires both old_string and new_string parameters`);
395
+ }
396
+ // If the handler implements editRange it owns text-replacement for its file type
397
+ // (e.g. DocxFileHandler does find/replace on pretty-printed XML rather than raw bytes).
398
+ // Plain text files fall through to performSearchReplace.
399
+ if (hasEditRange) {
400
+ try {
401
+ const result = await handler.editRange(validatedPath, '', {
402
+ old_string: parsed.old_string,
403
+ new_string: parsed.new_string,
404
+ expected_replacements: parsed.expected_replacements,
405
+ });
406
+ if (result.success) {
407
+ const resolvedEditRangePath = resolveAbsolutePath(parsed.file_path);
408
+ return {
409
+ content: [{
410
+ type: "text",
411
+ text: `Successfully applied ${result.editsApplied} edit(s) to ${parsed.file_path}`
412
+ }],
413
+ ...(parsed.origin === 'ui' ? {
414
+ structuredContent: {
415
+ fileName: path.basename(resolvedEditRangePath),
416
+ filePath: resolvedEditRangePath,
417
+ fileType: resolvePreviewFileType(resolvedEditRangePath),
418
+ ...await getDefaultEditorMetadata(resolvedEditRangePath),
419
+ },
420
+ } : {}),
421
+ };
422
+ }
423
+ const errorMsg = result.errors?.map(e => e.error).join('; ') || 'Unknown error';
424
+ return createErrorResponse(errorMsg);
425
+ }
426
+ catch (error) {
427
+ const errorMessage = error instanceof Error ? error.message : String(error);
428
+ return createErrorResponse(errorMessage);
429
+ }
430
+ }
431
+ return performSearchReplace(parsed.file_path, {
432
+ search: parsed.old_string,
433
+ replace: parsed.new_string
434
+ }, parsed.expected_replacements, parsed.origin);
435
+ }
@@ -0,0 +1,8 @@
1
+ import { ServerResult } from '../types.js';
2
+ interface FeedbackParams {
3
+ }
4
+ /**
5
+ * Open feedback form in browser with optional pre-filled data
6
+ */
7
+ export declare function giveFeedbackToDesktopCommander(params?: FeedbackParams): Promise<ServerResult>;
8
+ export {};
@@ -0,0 +1,132 @@
1
+ import { usageTracker } from '../utils/usageTracker.js';
2
+ import { capture } from '../utils/capture.js';
3
+ import { configManager } from '../config-manager.js';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
6
+ import * as os from 'os';
7
+ const execAsync = promisify(exec);
8
+ /**
9
+ * Open feedback form in browser with optional pre-filled data
10
+ */
11
+ export async function giveFeedbackToDesktopCommander(params = {}) {
12
+ try {
13
+ // Get usage stats for context
14
+ const stats = await usageTracker.getStats();
15
+ // Capture feedback tool usage event
16
+ await capture('feedback_tool_called', {
17
+ total_calls: stats.totalToolCalls,
18
+ successful_calls: stats.successfulCalls,
19
+ failed_calls: stats.failedCalls,
20
+ days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
21
+ total_sessions: stats.totalSessions,
22
+ platform: os.platform(),
23
+ });
24
+ // Build Tally.so URL with pre-filled parameters
25
+ const tallyUrl = await buildTallyUrl(params, stats);
26
+ // Open URL in default browser
27
+ const success = await openUrlInBrowser(tallyUrl);
28
+ if (success) {
29
+ // Capture successful browser opening
30
+ await capture('feedback_form_opened_successfully', {
31
+ total_calls: stats.totalToolCalls,
32
+ platform: os.platform()
33
+ });
34
+ // Mark that user has given feedback (or at least opened the form)
35
+ await usageTracker.markFeedbackGiven();
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: `🎉 **Feedback form opened in your browser!**\n\n` +
40
+ `Thank you for taking the time to share your experience with MCP Device. ` +
41
+ `Your feedback helps us build better features and improve the tool for everyone.\n\n` +
42
+ `The form has been pre-filled with the information you provided. ` +
43
+ `You can modify or add any additional details before submitting.\n\n` +
44
+ `**Form URL**: ${tallyUrl.length > 100 ? tallyUrl.substring(0, 100) + '...' : tallyUrl}`
45
+ }]
46
+ };
47
+ }
48
+ else {
49
+ // Capture browser opening failure
50
+ await capture('feedback_form_open_failed', {
51
+ total_calls: stats.totalToolCalls,
52
+ platform: os.platform(),
53
+ error_type: 'browser_open_failed'
54
+ });
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: `⚠️ **Couldn't open browser automatically**\n\n` +
59
+ `Please copy and paste this URL into your browser to access the feedback form:\n\n` +
60
+ `${tallyUrl}\n\n` +
61
+ `The form has been pre-filled with your information. Thank you for your feedback!`
62
+ }]
63
+ };
64
+ }
65
+ }
66
+ catch (error) {
67
+ // Capture error event
68
+ await capture('feedback_tool_error', {
69
+ error_message: error instanceof Error ? error.message : String(error),
70
+ error_type: error instanceof Error ? error.constructor.name : 'unknown'
71
+ });
72
+ return {
73
+ content: [{
74
+ type: "text",
75
+ text: `❌ **Error opening feedback form**: ${error instanceof Error ? error.message : String(error)}\n\n` +
76
+ `You can still access our feedback form directly at: https://tally.so/r/mKqoKg\n\n` +
77
+ `We appreciate your willingness to provide feedback!`
78
+ }],
79
+ isError: true
80
+ };
81
+ }
82
+ }
83
+ /**
84
+ * Build Tally.so URL with pre-filled parameters
85
+ */
86
+ async function buildTallyUrl(params, stats) {
87
+ const baseUrl = 'https://tally.so/r/mKqoKg';
88
+ const urlParams = new URLSearchParams();
89
+ // Only auto-filled hidden fields remain
90
+ urlParams.set('tool_call_count', stats.totalToolCalls.toString());
91
+ // Calculate days using
92
+ const daysUsing = Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24));
93
+ urlParams.set('days_using', daysUsing.toString());
94
+ // Add platform info
95
+ urlParams.set('platform', os.platform());
96
+ // Add client_id from analytics config
97
+ try {
98
+ const clientId = await configManager.getValue('clientId') || 'unknown';
99
+ urlParams.set('client_id', clientId);
100
+ }
101
+ catch (error) {
102
+ // Fallback if config read fails
103
+ urlParams.set('client_id', 'unknown');
104
+ }
105
+ return `${baseUrl}?${urlParams.toString()}`;
106
+ }
107
+ /**
108
+ * Open URL in default browser (cross-platform)
109
+ */
110
+ async function openUrlInBrowser(url) {
111
+ try {
112
+ const platform = os.platform();
113
+ let command;
114
+ switch (platform) {
115
+ case 'darwin': // macOS
116
+ command = `open "${url}"`;
117
+ break;
118
+ case 'win32': // Windows
119
+ command = `start "" "${url}"`;
120
+ break;
121
+ default: // Linux and others
122
+ command = `xdg-open "${url}"`;
123
+ break;
124
+ }
125
+ await execAsync(command);
126
+ return true;
127
+ }
128
+ catch (error) {
129
+ console.error('Failed to open browser:', error);
130
+ return false;
131
+ }
132
+ }
@@ -0,0 +1,79 @@
1
+ import type { ReadOptions, FileResult, PdfPageItem } from '../utils/files/base.js';
2
+ import { PdfOperations, PdfMetadata } from './pdf/index.js';
3
+ export declare const READ_OPERATION_TIMEOUT_MS: number;
4
+ /**
5
+ * Validates a path to ensure it can be accessed or created.
6
+ * For existing paths, returns the real path (resolving symlinks).
7
+ * For non-existent paths, validates parent directories to ensure they exist.
8
+ *
9
+ * @param requestedPath The path to validate
10
+ * @returns Promise<string> The validated path
11
+ * @throws Error if the path or its parent directories don't exist or if the path is not allowed
12
+ */
13
+ export declare function validatePath(requestedPath: string): Promise<string>;
14
+ export type { FileResult } from '../utils/files/base.js';
15
+ type PdfPayload = {
16
+ metadata: PdfMetadata;
17
+ pages: PdfPageItem[];
18
+ };
19
+ type FileResultPayloads = PdfPayload;
20
+ /**
21
+ * Read file content from a URL
22
+ * @param url URL to fetch content from
23
+ * @returns File content or file result with metadata
24
+ */
25
+ export declare function readFileFromUrl(url: string): Promise<FileResult>;
26
+ /**
27
+ * Read file content from the local filesystem
28
+ * @param filePath Path to the file
29
+ * @param options Read options (offset, length, sheet, range)
30
+ * @returns File content or file result with metadata
31
+ */
32
+ export declare function readFileFromDisk(filePath: string, options?: ReadOptions): Promise<FileResult>;
33
+ /**
34
+ * Read a file from either the local filesystem or a URL
35
+ * @param filePath Path to the file or URL
36
+ * @param options Read options (isUrl, offset, length, sheet, range)
37
+ * @returns File content or file result with metadata
38
+ */
39
+ export declare function readFile(filePath: string, options?: ReadOptions): Promise<FileResult>;
40
+ /**
41
+ * Read file content without status messages for internal operations
42
+ * This function preserves exact file content including original line endings,
43
+ * which is essential for edit operations that need to maintain file formatting.
44
+ * @param filePath Path to the file
45
+ * @param offset Starting line number to read from (default: 0)
46
+ * @param length Maximum number of lines to read (default: from config or 1000)
47
+ * @returns File content without status headers, with preserved line endings
48
+ */
49
+ export declare function readFileInternal(filePath: string, offset?: number, length?: number): Promise<string>;
50
+ export declare function writeFile(filePath: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
51
+ export interface MultiFileResult {
52
+ path: string;
53
+ content?: string;
54
+ mimeType?: string;
55
+ isImage?: boolean;
56
+ error?: string;
57
+ isPdf?: boolean;
58
+ payload?: FileResultPayloads;
59
+ }
60
+ export declare function readMultipleFiles(paths: string[]): Promise<MultiFileResult[]>;
61
+ export declare function createDirectory(dirPath: string): Promise<void>;
62
+ export declare function listDirectory(dirPath: string, depth?: number): Promise<string[]>;
63
+ export declare function moveFile(sourcePath: string, destinationPath: string): Promise<void>;
64
+ export declare function searchFiles(rootPath: string, pattern: string): Promise<string[]>;
65
+ export declare function getFileInfo(filePath: string): Promise<Record<string, any>>;
66
+ /**
67
+ * Write content to a PDF file.
68
+ * Can create a new PDF from Markdown string, or modify an existing PDF using operations.
69
+ *
70
+ * @param filePath Path to the output PDF file
71
+ * @param content Markdown string (for creation) or array of operations (for modification)
72
+ * @param options Options for PDF generation or modification. For modification, can include `sourcePdf`.
73
+ */
74
+ export declare function writePdf(filePath: string, content: string | PdfOperations[], outputPath?: string, options?: any): Promise<void>;
75
+ type DefaultEditorMetadata = {
76
+ defaultEditorName?: string;
77
+ defaultEditorPath?: string;
78
+ };
79
+ export declare function getDefaultEditorMetadata(filePath: string): Promise<DefaultEditorMetadata>;