@akiojin/unity-mcp-server 2.40.2 → 2.40.3

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 (90) hide show
  1. package/README.md +21 -0
  2. package/bin/unity-mcp-server +1 -1
  3. package/package.json +1 -1
  4. package/src/core/codeIndex.js +64 -15
  5. package/src/core/server.js +3 -34
  6. package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +27 -24
  7. package/src/handlers/analysis/FindByComponentToolHandler.js +4 -1
  8. package/src/handlers/analysis/GetAnimatorStateToolHandler.js +5 -5
  9. package/src/handlers/analysis/GetComponentValuesToolHandler.js +4 -1
  10. package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +27 -24
  11. package/src/handlers/analysis/GetInputActionsStateToolHandler.js +5 -5
  12. package/src/handlers/analysis/GetObjectReferencesToolHandler.js +4 -1
  13. package/src/handlers/asset/AssetDatabaseManageToolHandler.js +24 -6
  14. package/src/handlers/asset/AssetDependencyAnalyzeToolHandler.js +21 -11
  15. package/src/handlers/asset/AssetImportSettingsManageToolHandler.js +7 -7
  16. package/src/handlers/asset/AssetMaterialCreateToolHandler.js +78 -81
  17. package/src/handlers/asset/AssetMaterialModifyToolHandler.js +57 -61
  18. package/src/handlers/asset/AssetPrefabCreateToolHandler.js +61 -64
  19. package/src/handlers/asset/AssetPrefabExitModeToolHandler.js +9 -13
  20. package/src/handlers/asset/AssetPrefabInstantiateToolHandler.js +110 -116
  21. package/src/handlers/asset/AssetPrefabModifyToolHandler.js +58 -58
  22. package/src/handlers/asset/AssetPrefabOpenToolHandler.js +7 -5
  23. package/src/handlers/asset/AssetPrefabSaveToolHandler.js +13 -6
  24. package/src/handlers/compilation/CompilationGetStateToolHandler.js +4 -3
  25. package/src/handlers/component/ComponentAddToolHandler.js +2 -2
  26. package/src/handlers/component/ComponentGetTypesToolHandler.js +17 -21
  27. package/src/handlers/component/ComponentListToolHandler.js +5 -3
  28. package/src/handlers/component/ComponentModifyToolHandler.js +3 -3
  29. package/src/handlers/component/ComponentRemoveToolHandler.js +2 -2
  30. package/src/handlers/console/ConsoleClearToolHandler.js +36 -46
  31. package/src/handlers/editor/EditorLayersManageToolHandler.js +7 -6
  32. package/src/handlers/editor/EditorTagsManageToolHandler.js +20 -11
  33. package/src/handlers/editor/EditorToolsManageToolHandler.js +2 -2
  34. package/src/handlers/editor/EditorWindowsManageToolHandler.js +6 -5
  35. package/src/handlers/gameobject/GameObjectCreateToolHandler.js +62 -66
  36. package/src/handlers/gameobject/GameObjectDeleteToolHandler.js +9 -9
  37. package/src/handlers/gameobject/GameObjectFindToolHandler.js +13 -11
  38. package/src/handlers/gameobject/GameObjectGetHierarchyToolHandler.js +22 -16
  39. package/src/handlers/input/InputActionAddToolHandler.js +2 -2
  40. package/src/handlers/input/InputActionMapCreateToolHandler.js +2 -2
  41. package/src/handlers/input/InputActionMapRemoveToolHandler.js +2 -2
  42. package/src/handlers/input/InputActionRemoveToolHandler.js +2 -2
  43. package/src/handlers/input/InputBindingAddToolHandler.js +2 -2
  44. package/src/handlers/input/InputBindingCompositeCreateToolHandler.js +2 -2
  45. package/src/handlers/input/InputBindingRemoveAllToolHandler.js +2 -2
  46. package/src/handlers/input/InputBindingRemoveToolHandler.js +2 -2
  47. package/src/handlers/input/InputControlSchemesManageToolHandler.js +2 -2
  48. package/src/handlers/package/PackageManagerToolHandler.js +41 -44
  49. package/src/handlers/package/RegistryConfigToolHandler.js +28 -7
  50. package/src/handlers/playmode/PlaymodeGetStateToolHandler.js +12 -16
  51. package/src/handlers/playmode/PlaymodePauseToolHandler.js +8 -12
  52. package/src/handlers/playmode/PlaymodeWaitForStateToolHandler.js +6 -3
  53. package/src/handlers/scene/GetSceneInfoToolHandler.js +11 -11
  54. package/src/handlers/scene/SceneCreateToolHandler.js +28 -31
  55. package/src/handlers/scene/SceneListToolHandler.js +21 -24
  56. package/src/handlers/scene/SceneLoadToolHandler.js +27 -29
  57. package/src/handlers/scene/SceneSaveToolHandler.js +19 -22
  58. package/src/handlers/screenshot/ScreenshotCaptureToolHandler.js +88 -66
  59. package/src/handlers/script/CodeIndexStatusToolHandler.js +4 -3
  60. package/src/handlers/script/CodeIndexUpdateToolHandler.js +24 -14
  61. package/src/handlers/script/ScriptCreateClassToolHandler.js +44 -9
  62. package/src/handlers/script/ScriptPackagesListToolHandler.js +91 -91
  63. package/src/handlers/script/ScriptRefactorRenameToolHandler.js +80 -71
  64. package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +21 -7
  65. package/src/handlers/script/ScriptSearchToolHandler.js +299 -266
  66. package/src/handlers/script/ScriptSymbolsGetToolHandler.js +88 -79
  67. package/src/handlers/settings/SettingsGetToolHandler.js +28 -13
  68. package/src/handlers/settings/SettingsUpdateToolHandler.js +20 -6
  69. package/src/handlers/ui/UIClickElementToolHandler.js +87 -96
  70. package/src/handlers/ui/UIFindElementsToolHandler.js +45 -55
  71. package/src/handlers/ui/UIGetElementStateToolHandler.js +35 -43
  72. package/src/handlers/ui/UISetElementValueToolHandler.js +42 -49
  73. package/src/handlers/ui/UISimulateInputToolHandler.js +134 -136
  74. package/src/handlers/video/VideoCaptureForToolHandler.js +24 -7
  75. package/src/lsp/LspRpcClient.js +24 -12
  76. package/src/tools/analysis/analyzeSceneContents.js +85 -85
  77. package/src/tools/analysis/findByComponent.js +73 -73
  78. package/src/tools/analysis/getAnimatorState.js +287 -287
  79. package/src/tools/analysis/getComponentValues.js +161 -161
  80. package/src/tools/analysis/getGameObjectDetails.js +138 -138
  81. package/src/tools/analysis/getInputActionsState.js +291 -291
  82. package/src/tools/analysis/getObjectReferences.js +72 -72
  83. package/src/tools/input/inputActionsEditor.js +522 -474
  84. package/src/tools/scene/createScene.js +98 -97
  85. package/src/tools/scene/getSceneInfo.js +82 -81
  86. package/src/tools/scene/listScenes.js +70 -69
  87. package/src/tools/scene/loadScene.js +108 -106
  88. package/src/tools/scene/saveScene.js +78 -77
  89. package/src/tools/system/ping.js +9 -12
  90. package/src/utils/validators.js +2 -2
@@ -5,316 +5,349 @@ import { ProjectInfoProvider } from '../../core/projectInfo.js';
5
5
  import { logger, config } from '../../core/config.js';
6
6
 
7
7
  export class ScriptSearchToolHandler extends BaseToolHandler {
8
- constructor(unityConnection) {
9
- super(
10
- 'script_search',
11
- 'Search C# by substring/regex/glob with pagination and snippet context. PRIORITY: Use to locate symbols/files; avoid full contents. Use returnMode="snippets" (or "metadata") with small snippetContext (1–2). Narrow aggressively via include globs under Assets/** or Packages/** and semantic filters (namespace/container/identifier). Do NOT prefix repository folders.',
12
- {
13
- type: 'object',
14
- properties: {
15
- pattern: {
16
- type: 'string',
17
- description: 'Pattern to search (required unless patternType="glob"). For glob mode, use include/exclude.'
18
- },
19
- patternType: {
20
- type: 'string',
21
- enum: ['substring', 'regex', 'glob'],
22
- description: 'Pattern matching strategy: substring (default), regex, or glob-only scan.'
23
- },
24
- flags: {
25
- type: 'array',
26
- items: { type: 'string' },
27
- description: 'Regex flags (e.g., ["i","m","s","u"]). Ignored for substring/glob.'
28
- },
29
- scope: {
30
- type: 'string',
31
- enum: ['assets', 'packages', 'embedded', 'all'],
32
- description: 'Search scope: assets (Assets/, default), packages (Packages/), embedded, or all.'
33
- },
34
- include: {
35
- type: 'string',
36
- description: 'Include glob pattern (project-relative, default: **/*.cs). Examples: Assets/**/*.cs or Packages/unity-mcp-server/**/*.cs.'
37
- },
38
- exclude: {
39
- type: 'string',
40
- description: 'Exclude glob pattern (e.g., **/Tests/**).'
41
- },
42
- pageSize: {
43
- type: 'number',
44
- description: 'Maximum results per page for pagination.'
45
- },
46
- maxMatchesPerFile: {
47
- type: 'number',
48
- description: 'Cap matches returned per file.'
49
- },
50
- snippetContext: {
51
- type: 'number',
52
- description: 'Number of context lines around each match.'
53
- },
54
- maxBytes: {
55
- type: 'number',
56
- description: 'Maximum response size (bytes) to keep outputs LLM‑friendly.'
57
- },
58
- returnMode: {
59
- type: 'string',
60
- enum: ['metadata', 'snippets', 'full'],
61
- description: 'Result detail: metadata (fast), snippets (recommended), or full.'
62
- },
63
- detail: {
64
- type: 'string',
65
- enum: ['compact', 'metadata', 'snippets', 'full'],
66
- description: 'Alias of returnMode. `compact` maps to `snippets`.'
67
- },
68
- startAfter: {
69
- type: 'string',
70
- description: 'Opaque cursor for pagination (use value from previous page).'
71
- },
72
- maxFileSizeKB: {
73
- type: 'number',
74
- description: 'Skip files larger than this (KB).'
75
- },
76
- codeOnly: {
77
- type: 'boolean',
78
- description: 'If true, exclude comments/whitespace to reduce noise (default: true).'
79
- },
80
- container: {
81
- type: 'string',
82
- description: 'Semantic filter: container (e.g., class name).'
83
- },
84
- namespace: {
85
- type: 'string',
86
- description: 'Semantic filter: namespace.'
87
- },
88
- identifier: {
89
- type: 'string',
90
- description: 'Semantic filter: identifier (e.g., method or field name).'
91
- }
92
- },
93
- required: []
94
- }
95
- );
96
- this.unityConnection = unityConnection;
97
- this.projectInfo = new ProjectInfoProvider(unityConnection);
98
- this.configDefaultDetail = (config?.search?.defaultDetail || 'compact').toLowerCase();
99
- this.configSearchEngine = (config?.search?.engine || 'naive').toLowerCase();
100
- }
8
+ constructor(unityConnection) {
9
+ super(
10
+ 'script_search',
11
+ 'Search C# by substring/regex/glob with pagination and snippet context. PRIORITY: Use to locate symbols/files; avoid full contents. Use returnMode="snippets" (or "metadata") with small snippetContext (1–2). Narrow aggressively via include globs under Assets/** or Packages/** and semantic filters (namespace/container/identifier). Do NOT prefix repository folders.',
12
+ {
13
+ type: 'object',
14
+ properties: {
15
+ pattern: {
16
+ type: 'string',
17
+ description:
18
+ 'Pattern to search (required unless patternType="glob"). For glob mode, use include/exclude.'
19
+ },
20
+ patternType: {
21
+ type: 'string',
22
+ enum: ['substring', 'regex', 'glob'],
23
+ description: 'Pattern matching strategy: substring (default), regex, or glob-only scan.'
24
+ },
25
+ flags: {
26
+ type: 'array',
27
+ items: { type: 'string' },
28
+ description: 'Regex flags (e.g., ["i","m","s","u"]). Ignored for substring/glob.'
29
+ },
30
+ scope: {
31
+ type: 'string',
32
+ enum: ['assets', 'packages', 'embedded', 'all'],
33
+ description:
34
+ 'Search scope: assets (Assets/, default), packages (Packages/), embedded, or all.'
35
+ },
36
+ include: {
37
+ type: 'string',
38
+ description:
39
+ 'Include glob pattern (project-relative, default: **/*.cs). Examples: Assets/**/*.cs or Packages/unity-mcp-server/**/*.cs.'
40
+ },
41
+ exclude: {
42
+ type: 'string',
43
+ description: 'Exclude glob pattern (e.g., **/Tests/**).'
44
+ },
45
+ pageSize: {
46
+ type: 'number',
47
+ description: 'Maximum results per page for pagination.'
48
+ },
49
+ maxMatchesPerFile: {
50
+ type: 'number',
51
+ description: 'Cap matches returned per file.'
52
+ },
53
+ snippetContext: {
54
+ type: 'number',
55
+ description: 'Number of context lines around each match.'
56
+ },
57
+ maxBytes: {
58
+ type: 'number',
59
+ description: 'Maximum response size (bytes) to keep outputs LLM‑friendly.'
60
+ },
61
+ returnMode: {
62
+ type: 'string',
63
+ enum: ['metadata', 'snippets', 'full'],
64
+ description: 'Result detail: metadata (fast), snippets (recommended), or full.'
65
+ },
66
+ detail: {
67
+ type: 'string',
68
+ enum: ['compact', 'metadata', 'snippets', 'full'],
69
+ description: 'Alias of returnMode. `compact` maps to `snippets`.'
70
+ },
71
+ startAfter: {
72
+ type: 'string',
73
+ description: 'Opaque cursor for pagination (use value from previous page).'
74
+ },
75
+ maxFileSizeKB: {
76
+ type: 'number',
77
+ description: 'Skip files larger than this (KB).'
78
+ },
79
+ codeOnly: {
80
+ type: 'boolean',
81
+ description: 'If true, exclude comments/whitespace to reduce noise (default: true).'
82
+ },
83
+ container: {
84
+ type: 'string',
85
+ description: 'Semantic filter: container (e.g., class name).'
86
+ },
87
+ namespace: {
88
+ type: 'string',
89
+ description: 'Semantic filter: namespace.'
90
+ },
91
+ identifier: {
92
+ type: 'string',
93
+ description: 'Semantic filter: identifier (e.g., method or field name).'
94
+ }
95
+ },
96
+ required: []
97
+ }
98
+ );
99
+ this.unityConnection = unityConnection;
100
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
101
+ this.configDefaultDetail = (config?.search?.defaultDetail || 'compact').toLowerCase();
102
+ this.configSearchEngine = (config?.search?.engine || 'naive').toLowerCase();
103
+ }
101
104
 
102
- validate(params) {
103
- super.validate(params);
105
+ validate(params) {
106
+ super.validate(params);
104
107
 
105
- const { pattern, patternType } = params;
108
+ const { pattern, patternType } = params;
106
109
 
107
- // Pattern is required for non-glob pattern types
108
- if (patternType !== 'glob' && !pattern) {
109
- throw new Error('pattern is required for substring and regex search');
110
- }
110
+ // Pattern is required for non-glob pattern types
111
+ if (patternType !== 'glob' && !pattern) {
112
+ throw new Error('pattern is required for substring and regex search');
111
113
  }
114
+ }
112
115
 
113
- async execute(params) {
114
- try {
115
- const info = await this.projectInfo.get();
116
- const {
117
- pattern,
118
- patternType = 'substring',
119
- flags = [],
120
- scope = 'assets',
121
- include = '**/*.cs',
122
- exclude,
123
- pageSize = 20,
124
- maxMatchesPerFile = 5,
125
- snippetContext = 2,
126
- maxBytes = 1024 * 64,
127
- returnMode,
128
- detail,
129
- startAfter,
130
- maxFileSizeKB = 1024,
131
- codeOnly = true,
132
- } = params;
116
+ async execute(params) {
117
+ try {
118
+ const info = await this.projectInfo.get();
119
+ const {
120
+ pattern,
121
+ patternType = 'substring',
122
+ flags = [],
123
+ scope = 'assets',
124
+ include = '**/*.cs',
125
+ exclude,
126
+ pageSize = 20,
127
+ maxMatchesPerFile = 5,
128
+ snippetContext = 2,
129
+ maxBytes = 1024 * 64,
130
+ returnMode,
131
+ detail,
132
+ startAfter,
133
+ maxFileSizeKB = 1024,
134
+ codeOnly = true
135
+ } = params;
133
136
 
134
- // Resolve detail/returnMode default and mapping
135
- let effectiveDetail = (detail || '').toLowerCase();
136
- if (!effectiveDetail && !returnMode) {
137
- effectiveDetail = this.configDefaultDetail || 'compact';
138
- }
139
- const normalizedReturnMode = (() => {
140
- const d = (effectiveDetail || '').toLowerCase();
141
- if (d === 'compact') return 'snippets';
142
- if (d === 'metadata' || d === 'snippets' || d === 'full') return d;
143
- return (returnMode || 'snippets');
144
- })();
137
+ // Resolve detail/returnMode default and mapping
138
+ let effectiveDetail = (detail || '').toLowerCase();
139
+ if (!effectiveDetail && !returnMode) {
140
+ effectiveDetail = this.configDefaultDetail || 'compact';
141
+ }
142
+ const normalizedReturnMode = (() => {
143
+ const d = (effectiveDetail || '').toLowerCase();
144
+ if (d === 'compact') return 'snippets';
145
+ if (d === 'metadata' || d === 'snippets' || d === 'full') return d;
146
+ return returnMode || 'snippets';
147
+ })();
145
148
 
146
- // Resolve search roots
147
- const roots = [];
148
- if (scope === 'assets' || scope === 'all') roots.push(info.assetsPath);
149
- if (scope === 'packages' || scope === 'embedded' || scope === 'all') roots.push(info.packagesPath);
149
+ // Resolve search roots
150
+ const roots = [];
151
+ if (scope === 'assets' || scope === 'all') roots.push(info.assetsPath);
152
+ if (scope === 'packages' || scope === 'embedded' || scope === 'all')
153
+ roots.push(info.packagesPath);
150
154
 
151
- const includeRx = globToRegExp(include);
152
- const excludeRx = exclude ? globToRegExp(exclude) : null;
153
- // Engine selection (future: treesitter). Currently fallback to naive.
154
- if (this.configSearchEngine === 'treesitter') {
155
- logger.debug('[script_search] tree-sitter engine requested; falling back to naive matcher');
156
- }
157
- const matcher = buildMatcher(patternType, pattern, flags);
155
+ const includeRx = globToRegExp(include);
156
+ const excludeRx = exclude ? globToRegExp(exclude) : null;
157
+ // Engine selection (future: treesitter). Currently fallback to naive.
158
+ if (this.configSearchEngine === 'treesitter') {
159
+ logger.debug('[script_search] tree-sitter engine requested; falling back to naive matcher');
160
+ }
161
+ const matcher = buildMatcher(patternType, pattern, flags);
158
162
 
159
- const results = [];
160
- const pathTable = [];
161
- const pathId = new Map();
162
- let bytes = 0;
163
- let afterFound = !startAfter;
163
+ const results = [];
164
+ const pathTable = [];
165
+ const pathId = new Map();
166
+ let bytes = 0;
167
+ let afterFound = !startAfter;
164
168
 
165
- for await (const file of walk(roots)) {
166
- // Pagination cursor: skip until we see startAfter
167
- const rel = toRel(file, info.projectRoot);
168
- if (!afterFound) {
169
- if (rel === startAfter) afterFound = true;
170
- else continue;
171
- }
172
- // Filters
173
- if (!includeRx.test(rel)) continue;
174
- if (excludeRx && excludeRx.test(rel)) continue;
175
- if (!rel.toLowerCase().endsWith('.cs')) continue;
169
+ for await (const file of walk(roots)) {
170
+ // Pagination cursor: skip until we see startAfter
171
+ const rel = toRel(file, info.projectRoot);
172
+ if (!afterFound) {
173
+ if (rel === startAfter) afterFound = true;
174
+ else continue;
175
+ }
176
+ // Filters
177
+ if (!includeRx.test(rel)) continue;
178
+ if (excludeRx && excludeRx.test(rel)) continue;
179
+ if (!rel.toLowerCase().endsWith('.cs')) continue;
176
180
 
177
- // Size guard
178
- const st = await fs.stat(file).catch(() => null);
179
- if (!st || st.size > maxFileSizeKB * 1024) continue;
181
+ // Size guard
182
+ const st = await fs.stat(file).catch(() => null);
183
+ if (!st || st.size > maxFileSizeKB * 1024) continue;
180
184
 
181
- // Read content
182
- const text = await fs.readFile(file, 'utf8');
183
- const lines = text.split('\n');
184
- const filtered = codeOnly ? stripComments(lines) : lines;
185
+ // Read content
186
+ const text = await fs.readFile(file, 'utf8');
187
+ const lines = text.split('\n');
188
+ const filtered = codeOnly ? stripComments(lines) : lines;
185
189
 
186
- let matches = 0;
187
- const matchedLines = [];
188
- for (let i = 0; i < filtered.length; i++) {
189
- if (matches >= maxMatchesPerFile) break;
190
- const line = filtered[i];
191
- if (matcher(line)) {
192
- matches++;
193
- matchedLines.push(i + 1);
194
- }
195
- }
196
- if (matches === 0) continue;
190
+ let matches = 0;
191
+ const matchedLines = [];
192
+ for (let i = 0; i < filtered.length; i++) {
193
+ if (matches >= maxMatchesPerFile) break;
194
+ const line = filtered[i];
195
+ if (matcher(line)) {
196
+ matches++;
197
+ matchedLines.push(i + 1);
198
+ }
199
+ }
200
+ if (matches === 0) continue;
197
201
 
198
- const id = pathId.has(rel) ? pathId.get(rel) : (pathTable.push(rel) - 1, pathTable.length - 1);
199
- pathId.set(rel, id);
202
+ const id = pathId.has(rel)
203
+ ? pathId.get(rel)
204
+ : (pathTable.push(rel) - 1, pathTable.length - 1);
205
+ pathId.set(rel, id);
200
206
 
201
- const lineRanges = toRanges(matchedLines);
202
- const item = { fileId: id, lineRanges };
207
+ const lineRanges = toRanges(matchedLines);
208
+ const item = { fileId: id, lineRanges };
203
209
 
204
- if (normalizedReturnMode === 'snippets') {
205
- // Build minimal snippets around first few matches
206
- const snippets = [];
207
- for (const ln of matchedLines.slice(0, maxMatchesPerFile)) {
208
- const s = Math.max(1, ln - snippetContext);
209
- const e = Math.min(lines.length, ln + snippetContext);
210
- snippets.push({ line: ln, snippet: lines.slice(s - 1, e).join('\n') });
211
- }
212
- item.snippets = snippets;
213
- }
210
+ if (normalizedReturnMode === 'snippets') {
211
+ // Build minimal snippets around first few matches
212
+ const snippets = [];
213
+ for (const ln of matchedLines.slice(0, maxMatchesPerFile)) {
214
+ const s = Math.max(1, ln - snippetContext);
215
+ const e = Math.min(lines.length, ln + snippetContext);
216
+ snippets.push({ line: ln, snippet: lines.slice(s - 1, e).join('\n') });
217
+ }
218
+ item.snippets = snippets;
219
+ }
214
220
 
215
- const json = JSON.stringify(item);
216
- bytes += Buffer.byteLength(json, 'utf8');
217
- results.push(item);
221
+ const json = JSON.stringify(item);
222
+ bytes += Buffer.byteLength(json, 'utf8');
223
+ results.push(item);
218
224
 
219
- if (results.length >= pageSize || bytes >= maxBytes) break;
220
- }
225
+ if (results.length >= pageSize || bytes >= maxBytes) break;
226
+ }
221
227
 
222
- return {
223
- success: true,
224
- total: results.length,
225
- pathTable,
226
- results,
227
- cursor: results.length && results.length >= pageSize ? pathTable[pathTable.length - 1] : null
228
- };
229
- } catch (e) {
230
- logger.error(`[script_search] failed: ${e.message}`);
231
- return { error: e.message };
232
- }
228
+ return {
229
+ success: true,
230
+ total: results.length,
231
+ pathTable,
232
+ results,
233
+ cursor:
234
+ results.length && results.length >= pageSize ? pathTable[pathTable.length - 1] : null
235
+ };
236
+ } catch (e) {
237
+ logger.error(`[script_search] failed: ${e.message}`);
238
+ return { error: e.message };
233
239
  }
240
+ }
234
241
  }
235
242
 
236
243
  // --- helpers ---
237
244
  function globToRegExp(glob) {
238
- // Very small subset: **/* and * and ? handling
239
- const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&')
240
- .replace(/\*\*/g, '§§')
241
- .replace(/\*/g, '[^/]*')
242
- .replace(/§§/g, '.*')
243
- .replace(/\?/g, '.');
244
- return new RegExp('^' + esc + '$');
245
+ // Very small subset: **/* and * and ? handling
246
+ const esc = glob
247
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
248
+ .replace(/\*\*/g, '§§')
249
+ .replace(/\*/g, '[^/]*')
250
+ .replace(/§§/g, '.*')
251
+ .replace(/\?/g, '.');
252
+ return new RegExp('^' + esc + '$');
245
253
  }
246
254
 
247
255
  function buildMatcher(type, pattern, flags) {
248
- if (type === 'regex') {
249
- const fl = Array.isArray(flags) ? flags.join('') : '';
250
- const rx = new RegExp(pattern, fl);
251
- return (s) => rx.test(s);
252
- }
253
- if (type === 'glob') {
254
- // glob-only scan: no content matcher, treat every file as match
255
- return () => true;
256
- }
257
- // substring
258
- const p = pattern || '';
259
- return (s) => p && s.includes(p);
256
+ if (type === 'regex') {
257
+ const fl = Array.isArray(flags) ? flags.join('') : '';
258
+ const rx = new RegExp(pattern, fl);
259
+ return s => rx.test(s);
260
+ }
261
+ if (type === 'glob') {
262
+ // glob-only scan: no content matcher, treat every file as match
263
+ return () => true;
264
+ }
265
+ // substring
266
+ const p = pattern || '';
267
+ return s => p && s.includes(p);
260
268
  }
261
269
 
262
270
  async function* walk(roots) {
263
- for (const r of roots) {
264
- yield* walkDir(r);
265
- }
271
+ for (const r of roots) {
272
+ yield* walkDir(r);
273
+ }
266
274
  }
267
275
 
268
276
  async function* walkDir(dir) {
269
- let entries;
270
- try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
271
- for (const e of entries) {
272
- const p = path.join(dir, e.name);
273
- if (e.isDirectory()) {
274
- yield* walkDir(p);
275
- } else if (e.isFile()) {
276
- yield p.replace(/\\/g, '/');
277
- }
277
+ let entries;
278
+ try {
279
+ entries = await fs.readdir(dir, { withFileTypes: true });
280
+ } catch {
281
+ return;
282
+ }
283
+ for (const e of entries) {
284
+ const p = path.join(dir, e.name);
285
+ if (e.isDirectory()) {
286
+ yield* walkDir(p);
287
+ } else if (e.isFile()) {
288
+ yield p.replace(/\\/g, '/');
278
289
  }
290
+ }
279
291
  }
280
292
 
281
293
  function toRel(abs, projectRoot) {
282
- const n = abs.replace(/\\/g, '/');
283
- const base = projectRoot.replace(/\\/g, '/');
284
- return n.startsWith(base) ? n.substring(base.length + 1) : n;
294
+ const n = abs.replace(/\\/g, '/');
295
+ const base = projectRoot.replace(/\\/g, '/');
296
+ return n.startsWith(base) ? n.substring(base.length + 1) : n;
285
297
  }
286
298
 
287
299
  function toRanges(lines) {
288
- if (!lines.length) return '';
289
- const out = [];
290
- let s = lines[0], prev = lines[0];
291
- for (let i = 1; i < lines.length; i++) {
292
- const v = lines[i];
293
- if (v === prev + 1) { prev = v; continue; }
294
- out.push(s === prev ? `${s}` : `${s}-${prev}`);
295
- s = prev = v;
300
+ if (!lines.length) return '';
301
+ const out = [];
302
+ let s = lines[0],
303
+ prev = lines[0];
304
+ for (let i = 1; i < lines.length; i++) {
305
+ const v = lines[i];
306
+ if (v === prev + 1) {
307
+ prev = v;
308
+ continue;
296
309
  }
297
310
  out.push(s === prev ? `${s}` : `${s}-${prev}`);
298
- return out.join(',');
311
+ s = prev = v;
312
+ }
313
+ out.push(s === prev ? `${s}` : `${s}-${prev}`);
314
+ return out.join(',');
299
315
  }
300
316
 
301
317
  function stripComments(lines) {
302
- // naive removal of // line comments and /* */ blocks
303
- const out = [];
304
- let inBlock = false;
305
- for (const line of lines) {
306
- let s = line;
307
- if (inBlock) {
308
- const end = s.indexOf('*/');
309
- if (end >= 0) { s = s.slice(end + 2); inBlock = false; } else { out.push(''); continue; }
310
- }
311
- let i = 0; let res = '';
312
- while (i < s.length) {
313
- if (s.startsWith('/*', i)) { inBlock = true; const end = s.indexOf('*/', i + 2); if (end >= 0) { i = end + 2; inBlock = false; continue; } else break; }
314
- if (s.startsWith('//', i)) { break; }
315
- res += s[i++];
316
- }
317
- out.push(res);
318
+ // naive removal of // line comments and /* */ blocks
319
+ const out = [];
320
+ let inBlock = false;
321
+ for (const line of lines) {
322
+ let s = line;
323
+ if (inBlock) {
324
+ const end = s.indexOf('*/');
325
+ if (end >= 0) {
326
+ s = s.slice(end + 2);
327
+ inBlock = false;
328
+ } else {
329
+ out.push('');
330
+ continue;
331
+ }
332
+ }
333
+ let i = 0;
334
+ let res = '';
335
+ while (i < s.length) {
336
+ if (s.startsWith('/*', i)) {
337
+ inBlock = true;
338
+ const end = s.indexOf('*/', i + 2);
339
+ if (end >= 0) {
340
+ i = end + 2;
341
+ inBlock = false;
342
+ continue;
343
+ } else break;
344
+ }
345
+ if (s.startsWith('//', i)) {
346
+ break;
347
+ }
348
+ res += s[i++];
318
349
  }
319
- return out;
350
+ out.push(res);
351
+ }
352
+ return out;
320
353
  }