@chrrxs/robloxstudio-mcp 3.0.0 → 3.0.2

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 (48) hide show
  1. package/dist/index.js +9401 -9047
  2. package/package.json +2 -2
  3. package/studio-plugin/MCPPlugin.rbxmx +400 -97
  4. package/studio-plugin/.Carbon.rbxm.lock +0 -0
  5. package/studio-plugin/Carbon.rbxm +0 -0
  6. package/studio-plugin/INSTALLATION.md +0 -170
  7. package/studio-plugin/MCPInspectorPlugin.rbxmx +0 -169759
  8. package/studio-plugin/default.project.json +0 -19
  9. package/studio-plugin/dev.project.json +0 -23
  10. package/studio-plugin/include/LibMP.lua +0 -156378
  11. package/studio-plugin/inspector-icon.png +0 -0
  12. package/studio-plugin/package-lock.json +0 -706
  13. package/studio-plugin/package.json +0 -19
  14. package/studio-plugin/plugin.json +0 -10
  15. package/studio-plugin/src/modules/AssetSanitizationPolicy.ts +0 -127
  16. package/studio-plugin/src/modules/ClientBroker.ts +0 -450
  17. package/studio-plugin/src/modules/Communication.ts +0 -601
  18. package/studio-plugin/src/modules/EvalBridges.ts +0 -255
  19. package/studio-plugin/src/modules/HttpDiagnostics.ts +0 -50
  20. package/studio-plugin/src/modules/LuauExec.ts +0 -403
  21. package/studio-plugin/src/modules/Recording.ts +0 -28
  22. package/studio-plugin/src/modules/RenderMonitor.ts +0 -60
  23. package/studio-plugin/src/modules/RuntimeLogBuffer.ts +0 -210
  24. package/studio-plugin/src/modules/ServerUrlSettings.ts +0 -117
  25. package/studio-plugin/src/modules/State.ts +0 -39
  26. package/studio-plugin/src/modules/StopPlayMonitor.ts +0 -267
  27. package/studio-plugin/src/modules/UI.ts +0 -597
  28. package/studio-plugin/src/modules/Utils.ts +0 -527
  29. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +0 -391
  30. package/studio-plugin/src/modules/handlers/BreakpointHandlers.ts +0 -460
  31. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +0 -170
  32. package/studio-plugin/src/modules/handlers/EvalRuntimeHandlers.ts +0 -149
  33. package/studio-plugin/src/modules/handlers/GenerateModelHandlers.ts +0 -168
  34. package/studio-plugin/src/modules/handlers/InputHandlers.ts +0 -163
  35. package/studio-plugin/src/modules/handlers/LogHandlers.ts +0 -14
  36. package/studio-plugin/src/modules/handlers/MemoryHandlers.ts +0 -44
  37. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +0 -96
  38. package/studio-plugin/src/modules/handlers/MicroProfilerHandlers.ts +0 -1263
  39. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +0 -62
  40. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +0 -716
  41. package/studio-plugin/src/modules/handlers/SceneAnalysisHandlers.ts +0 -216
  42. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +0 -531
  43. package/studio-plugin/src/modules/handlers/ScriptProfilerHandlers.ts +0 -386
  44. package/studio-plugin/src/modules/handlers/SerializationHandlers.ts +0 -172
  45. package/studio-plugin/src/modules/handlers/TestHandlers.ts +0 -350
  46. package/studio-plugin/src/server/index.server.ts +0 -135
  47. package/studio-plugin/src/types/index.d.ts +0 -57
  48. package/studio-plugin/tsconfig.json +0 -20
@@ -1,716 +0,0 @@
1
- import Utils from "../Utils";
2
-
3
- const { getInstancePath, getInstanceByPath, readScriptSource } = Utils;
4
-
5
- interface TreeNode {
6
- name: string;
7
- className: string;
8
- path?: string;
9
- children: TreeNode[];
10
- hasSource?: boolean;
11
- scriptType?: string;
12
- enabled?: boolean;
13
- }
14
-
15
- function getFileTree(requestData: Record<string, unknown>) {
16
- const path = (requestData.path as string) ?? "";
17
- const startInstance = getInstanceByPath(path);
18
-
19
- if (!startInstance) {
20
- return { error: `Path not found: ${path}` };
21
- }
22
-
23
- function buildTree(instance: Instance, depth: number): TreeNode {
24
- if (depth > 10) {
25
- return { name: instance.Name, className: instance.ClassName, children: [] };
26
- }
27
-
28
- const node: TreeNode = {
29
- name: instance.Name,
30
- className: instance.ClassName,
31
- path: getInstancePath(instance),
32
- children: [],
33
- };
34
-
35
- if (instance.IsA("LuaSourceContainer")) {
36
- node.hasSource = true;
37
- node.scriptType = instance.ClassName;
38
- if (instance.IsA("BaseScript")) {
39
- node.enabled = instance.Enabled;
40
- }
41
- }
42
-
43
- for (const child of instance.GetChildren()) {
44
- node.children.push(buildTree(child, depth + 1));
45
- }
46
-
47
- return node;
48
- }
49
-
50
- return {
51
- tree: buildTree(startInstance, 0),
52
- timestamp: tick(),
53
- };
54
- }
55
-
56
- function searchFiles(requestData: Record<string, unknown>) {
57
- const query = requestData.query as string;
58
- const searchType = (requestData.searchType as string) ?? "name";
59
-
60
- if (!query) return { error: "Query is required" };
61
-
62
- const results: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean }[] = [];
63
-
64
- function searchRecursive(instance: Instance) {
65
- let match = false;
66
-
67
- if (searchType === "name") {
68
- match = instance.Name.lower().find(query.lower())[0] !== undefined;
69
- } else if (searchType === "type") {
70
- match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
71
- } else if (searchType === "content" && instance.IsA("LuaSourceContainer")) {
72
- match = readScriptSource(instance).lower().find(query.lower())[0] !== undefined;
73
- }
74
-
75
- if (match) {
76
- const entry: { name: string; className: string; path: string; hasSource: boolean; enabled?: boolean } = {
77
- name: instance.Name,
78
- className: instance.ClassName,
79
- path: getInstancePath(instance),
80
- hasSource: instance.IsA("LuaSourceContainer"),
81
- };
82
- if (instance.IsA("BaseScript")) {
83
- entry.enabled = instance.Enabled;
84
- }
85
- results.push(entry);
86
- }
87
-
88
- for (const child of instance.GetChildren()) {
89
- searchRecursive(child);
90
- }
91
- }
92
-
93
- searchRecursive(game);
94
-
95
- return { results, query, searchType, count: results.size() };
96
- }
97
-
98
- function getPlaceInfo(_requestData: Record<string, unknown>) {
99
- const dataModelName = game.Name;
100
- let placeName = dataModelName;
101
-
102
- if (game.PlaceId > 0) {
103
- const MarketplaceService = game.GetService("MarketplaceService");
104
- const [ok, info] = pcall(() => MarketplaceService.GetProductInfo(game.PlaceId));
105
- if (ok && info !== undefined) {
106
- const name = (info as { Name?: string }).Name;
107
- if (typeIs(name, "string") && name !== "") {
108
- placeName = name;
109
- }
110
- }
111
- }
112
-
113
- return {
114
- placeName,
115
- dataModelName,
116
- placeId: game.PlaceId,
117
- gameId: game.GameId,
118
- jobId: game.JobId,
119
- workspace: {
120
- name: game.Workspace.Name,
121
- className: game.Workspace.ClassName,
122
- },
123
- };
124
- }
125
-
126
- function searchObjects(requestData: Record<string, unknown>) {
127
- const query = requestData.query as string;
128
- const searchType = (requestData.searchType as string) ?? "name";
129
- const propertyName = requestData.propertyName as string | undefined;
130
-
131
- if (!query) return { error: "Query is required" };
132
-
133
- const results: { name: string; className: string; path: string }[] = [];
134
-
135
- function searchRecursive(instance: Instance) {
136
- let match = false;
137
-
138
- if (searchType === "name") {
139
- match = instance.Name.lower().find(query.lower())[0] !== undefined;
140
- } else if (searchType === "class") {
141
- match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
142
- } else if (searchType === "property" && propertyName) {
143
- const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
144
- if (success) {
145
- match = (value as string).lower().find(query.lower())[0] !== undefined;
146
- }
147
- }
148
-
149
- if (match) {
150
- results.push({
151
- name: instance.Name,
152
- className: instance.ClassName,
153
- path: getInstancePath(instance),
154
- });
155
- }
156
-
157
- for (const child of instance.GetChildren()) {
158
- searchRecursive(child);
159
- }
160
- }
161
-
162
- searchRecursive(game);
163
-
164
- return { results, query, searchType, count: results.size() };
165
- }
166
-
167
- function getInstanceProperties(requestData: Record<string, unknown>) {
168
- const instancePath = requestData.instancePath as string;
169
- const excludeSource = (requestData.excludeSource as boolean) ?? false;
170
- if (!instancePath) return { error: "Instance path is required" };
171
-
172
- const instance = getInstanceByPath(instancePath);
173
- if (!instance) return { error: `Instance not found: ${instancePath}` };
174
-
175
- const properties: Record<string, unknown> = {};
176
- const [success, result] = pcall(() => {
177
- const basicProps = ["Name", "ClassName", "Parent"];
178
- for (const prop of basicProps) {
179
- const [propSuccess, propValue] = pcall(() => {
180
- const val = (instance as unknown as Record<string, unknown>)[prop];
181
- if (prop === "Parent" && val) return getInstancePath(val as Instance);
182
- if (val === undefined) return "nil";
183
- return tostring(val);
184
- });
185
- if (propSuccess) properties[prop] = propValue;
186
- }
187
-
188
- const commonProps = [
189
- "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
190
- "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
191
- "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
192
- "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
193
- "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
194
- "Face", "SurfaceType",
195
- ];
196
-
197
- for (const prop of commonProps) {
198
- const [propSuccess, propValue] = pcall(() => {
199
- const val = (instance as unknown as Record<string, unknown>)[prop];
200
- if (typeOf(val) === "UDim2") {
201
- const udim = val as UDim2;
202
- return {
203
- X: { Scale: udim.X.Scale, Offset: udim.X.Offset },
204
- Y: { Scale: udim.Y.Scale, Offset: udim.Y.Offset },
205
- _type: "UDim2",
206
- };
207
- }
208
- return tostring(val);
209
- });
210
- if (propSuccess) properties[prop] = propValue;
211
- }
212
-
213
- if (instance.IsA("LuaSourceContainer")) {
214
- if (!excludeSource) {
215
- properties.Source = readScriptSource(instance);
216
- } else {
217
- const src = readScriptSource(instance);
218
- properties.SourceLength = src.size();
219
- properties.LineCount = Utils.splitLines(src)[0].size();
220
- }
221
- if (instance.IsA("BaseScript")) {
222
- properties.Enabled = tostring(instance.Enabled);
223
- }
224
- }
225
-
226
- if (instance.IsA("Part")) {
227
- properties.Shape = tostring(instance.Shape);
228
- }
229
-
230
- if (instance.IsA("BasePart")) {
231
- properties.TopSurface = tostring(instance.TopSurface);
232
- properties.BottomSurface = tostring(instance.BottomSurface);
233
- }
234
-
235
- if (instance.IsA("MeshPart")) {
236
- properties.MeshId = tostring(instance.MeshId);
237
- properties.TextureID = tostring(instance.TextureID);
238
- }
239
-
240
- if (instance.IsA("SpecialMesh")) {
241
- properties.MeshId = tostring(instance.MeshId);
242
- properties.TextureId = tostring(instance.TextureId);
243
- properties.MeshType = tostring(instance.MeshType);
244
- }
245
-
246
- if (instance.IsA("Sound")) {
247
- properties.SoundId = tostring(instance.SoundId);
248
- properties.TimeLength = tostring(instance.TimeLength);
249
- properties.IsPlaying = tostring(instance.IsPlaying);
250
- }
251
-
252
- if (instance.IsA("Animation")) {
253
- properties.AnimationId = tostring(instance.AnimationId);
254
- }
255
-
256
- if (instance.IsA("Decal") || instance.IsA("Texture")) {
257
- properties.Texture = tostring((instance as Decal | Texture).Texture);
258
- }
259
-
260
- if (instance.IsA("Shirt")) {
261
- properties.ShirtTemplate = tostring(instance.ShirtTemplate);
262
- } else if (instance.IsA("Pants")) {
263
- properties.PantsTemplate = tostring(instance.PantsTemplate);
264
- } else if (instance.IsA("ShirtGraphic")) {
265
- properties.Graphic = tostring(instance.Graphic);
266
- }
267
-
268
- properties.ChildCount = tostring(instance.GetChildren().size());
269
- });
270
-
271
- if (success) {
272
- return { instancePath, className: instance.ClassName, properties };
273
- } else {
274
- return { error: `Failed to get properties: ${result}` };
275
- }
276
- }
277
-
278
- function searchByProperty(requestData: Record<string, unknown>) {
279
- const propertyName = requestData.propertyName as string;
280
- const propertyValue = requestData.propertyValue as string;
281
-
282
- if (!propertyName || !propertyValue) {
283
- return { error: "Property name and value are required" };
284
- }
285
-
286
- const results: { name: string; className: string; path: string; propertyValue: string }[] = [];
287
-
288
- function searchRecursive(instance: Instance) {
289
- const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
290
- if (success && (value as string).lower().find(propertyValue.lower())[0] !== undefined) {
291
- results.push({
292
- name: instance.Name,
293
- className: instance.ClassName,
294
- path: getInstancePath(instance),
295
- propertyValue: value as string,
296
- });
297
- }
298
- for (const child of instance.GetChildren()) {
299
- searchRecursive(child);
300
- }
301
- }
302
-
303
- searchRecursive(game);
304
- return { propertyName, propertyValue, results, count: results.size() };
305
- }
306
-
307
- function getClassInfo(requestData: Record<string, unknown>) {
308
- const className = requestData.className as string;
309
- if (!className) return { error: "Class name is required" };
310
-
311
- let [success, tempInstance] = pcall(() => new Instance(className as keyof CreatableInstances));
312
- let isService = false;
313
-
314
- if (!success) {
315
- const [serviceSuccess, serviceInstance] = pcall(() =>
316
- game.GetService(className as keyof Services),
317
- );
318
- if (serviceSuccess && serviceInstance) {
319
- success = true;
320
- tempInstance = serviceInstance as unknown as Instance;
321
- isService = true;
322
- }
323
- }
324
-
325
- if (!success) return { error: `Invalid class name: ${className}` };
326
-
327
- const classInfo: {
328
- className: string;
329
- isService: boolean;
330
- properties: string[];
331
- methods: string[];
332
- events: string[];
333
- } = { className, isService, properties: [], methods: [], events: [] };
334
-
335
- const commonProps = [
336
- "Name", "ClassName", "Parent", "Size", "Position", "Rotation", "CFrame",
337
- "Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color",
338
- "Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible",
339
- "Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency",
340
- "ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness",
341
- "Range", "Shadows",
342
- ];
343
-
344
- for (const prop of commonProps) {
345
- const [propSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[prop]);
346
- if (propSuccess) classInfo.properties.push(prop);
347
- }
348
-
349
- const commonMethods = [
350
- "Destroy", "Clone", "FindFirstChild", "FindFirstChildOfClass",
351
- "GetChildren", "IsA", "IsAncestorOf", "IsDescendantOf", "WaitForChild",
352
- ];
353
-
354
- for (const method of commonMethods) {
355
- const [methodSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[method]);
356
- if (methodSuccess) classInfo.methods.push(method);
357
- }
358
-
359
- if (!isService) {
360
- (tempInstance as Instance).Destroy();
361
- }
362
-
363
- return classInfo;
364
- }
365
-
366
- function getProjectStructure(requestData: Record<string, unknown>) {
367
- const startPath = (requestData.path as string) ?? "";
368
- const maxDepth = (requestData.maxDepth as number) ?? 3;
369
- const showScriptsOnly = (requestData.scriptsOnly as boolean) ?? false;
370
-
371
- if (startPath === "" || startPath === "game") {
372
- const services: Record<string, unknown>[] = [];
373
- const mainServices = [
374
- "Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage",
375
- "StarterGui", "StarterPack", "StarterPlayer", "Players",
376
- ];
377
-
378
- for (const serviceName of mainServices) {
379
- const [svcOk, service] = pcall(() => game.GetService(serviceName as keyof Services));
380
- if (svcOk && service) {
381
- services.push({
382
- name: service.Name,
383
- className: service.ClassName,
384
- path: getInstancePath(service as Instance),
385
- childCount: (service as Instance).GetChildren().size(),
386
- hasChildren: (service as Instance).GetChildren().size() > 0,
387
- });
388
- }
389
- }
390
-
391
- return {
392
- type: "service_overview",
393
- services,
394
- timestamp: tick(),
395
- note: "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
396
- };
397
- }
398
-
399
- const startInstance = getInstanceByPath(startPath);
400
- if (!startInstance) return { error: `Path not found: ${startPath}` };
401
-
402
- function getStructure(instance: Instance, depth: number): Record<string, unknown> {
403
- if (depth > maxDepth) {
404
- return {
405
- name: instance.Name,
406
- className: instance.ClassName,
407
- path: getInstancePath(instance),
408
- childCount: instance.GetChildren().size(),
409
- hasMore: true,
410
- note: "Max depth reached - use this path to explore further",
411
- };
412
- }
413
-
414
- const node: Record<string, unknown> = {
415
- name: instance.Name,
416
- className: instance.ClassName,
417
- path: getInstancePath(instance),
418
- children: [] as Record<string, unknown>[],
419
- };
420
-
421
- if (instance.IsA("LuaSourceContainer")) {
422
- node.hasSource = true;
423
- node.scriptType = instance.ClassName;
424
- if (instance.IsA("BaseScript")) {
425
- node.enabled = instance.Enabled;
426
- }
427
- }
428
-
429
- if (instance.IsA("GuiObject")) {
430
- node.visible = instance.Visible;
431
- if (instance.IsA("Frame") || instance.IsA("ScreenGui")) {
432
- node.guiType = "container";
433
- } else if (instance.IsA("TextLabel") || instance.IsA("TextButton")) {
434
- node.guiType = "text";
435
- const textInst = instance as TextLabel | TextButton;
436
- if (textInst.Text !== "") node.text = textInst.Text;
437
- } else if (instance.IsA("ImageLabel") || instance.IsA("ImageButton")) {
438
- node.guiType = "image";
439
- }
440
- }
441
-
442
- let children = instance.GetChildren();
443
- if (showScriptsOnly) {
444
- children = children.filter(
445
- (child) => child.IsA("BaseScript") || child.IsA("Folder") || child.IsA("ModuleScript"),
446
- );
447
- }
448
-
449
- const nodeChildren = node.children as Record<string, unknown>[];
450
- const childCount = children.size();
451
- if (childCount > 20 && depth < maxDepth) {
452
- const classGroups = new Map<string, Instance[]>();
453
- for (const child of children) {
454
- const cn = child.ClassName;
455
- if (!classGroups.has(cn)) classGroups.set(cn, []);
456
- classGroups.get(cn)!.push(child);
457
- }
458
-
459
- const childSummary: Record<string, unknown>[] = [];
460
- classGroups.forEach((classChildren, cn) => {
461
- childSummary.push({
462
- className: cn,
463
- count: classChildren.size(),
464
- examples: [classChildren[0]?.Name, classChildren[1]?.Name],
465
- });
466
- });
467
- node.childSummary = childSummary;
468
-
469
- classGroups.forEach((classChildren, cn) => {
470
- const limit = math.min(3, classChildren.size());
471
- for (let i = 0; i < limit; i++) {
472
- nodeChildren.push(getStructure(classChildren[i], depth + 1));
473
- }
474
- if (classChildren.size() > 3) {
475
- nodeChildren.push({
476
- name: `... ${classChildren.size() - 3} more ${cn} objects`,
477
- className: "MoreIndicator",
478
- path: `${getInstancePath(instance)} [${cn} children]`,
479
- note: "Use specific path to explore these objects",
480
- });
481
- }
482
- });
483
- } else {
484
- for (const child of children) {
485
- nodeChildren.push(getStructure(child, depth + 1));
486
- }
487
- }
488
-
489
- return node;
490
- }
491
-
492
- const result = getStructure(startInstance, 0);
493
- result.requestedPath = startPath;
494
- result.maxDepth = maxDepth;
495
- result.scriptsOnly = showScriptsOnly;
496
- result.timestamp = tick();
497
-
498
- return result;
499
- }
500
-
501
- // Split a Lua pattern on TOP-LEVEL "|" into alternatives. Lua patterns have no
502
- // alternation operator, so "foo|bar" would otherwise be matched as the literal
503
- // text "foo|bar" and silently never hit. "%|" stays a literal pipe, and "%bxy"
504
- // keeps both balanced-match delimiter characters.
505
- function splitLuaAlternation(pattern: string): string[] {
506
- const parts: string[] = [];
507
- let current = "";
508
- let i = 1;
509
- const n = pattern.size();
510
- let inCharClass = false;
511
- while (i <= n) {
512
- const c = string.sub(pattern, i, i);
513
- if (c === "%") {
514
- if (string.sub(pattern, i + 1, i + 1) === "b") {
515
- current += string.sub(pattern, i, math.min(i + 3, n));
516
- i += 4;
517
- continue;
518
- }
519
- // Preserve an escape pair (e.g. %|, %., %d) intact.
520
- current += string.sub(pattern, i, i + 1);
521
- i += 2;
522
- } else if (c === "[") {
523
- inCharClass = true;
524
- current += c;
525
- i += 1;
526
- } else if (c === "]") {
527
- inCharClass = false;
528
- current += c;
529
- i += 1;
530
- } else if (c === "|" && !inCharClass) {
531
- parts.push(current);
532
- current = "";
533
- i += 1;
534
- } else {
535
- current += c;
536
- i += 1;
537
- }
538
- }
539
- parts.push(current);
540
- return parts;
541
- }
542
-
543
- // Return the earliest match across alternatives (mirrors regex alternation).
544
- function findFirstPattern(line: string, alternatives: string[]): [number | undefined, number | undefined] {
545
- let bestStart: number | undefined;
546
- let bestEnd: number | undefined;
547
- for (const alt of alternatives) {
548
- if (alt === "") continue;
549
- const [s, e] = string.find(line, alt);
550
- if (s !== undefined && (bestStart === undefined || s < bestStart)) {
551
- bestStart = s;
552
- bestEnd = e as number;
553
- }
554
- }
555
- return [bestStart, bestEnd];
556
- }
557
-
558
- function grepScripts(requestData: Record<string, unknown>) {
559
- const pattern = requestData.pattern as string;
560
- if (!pattern) return { error: "pattern is required" };
561
-
562
- const usePattern = (requestData.usePattern as boolean) ?? false;
563
- if (usePattern && requestData.caseSensitive === false) {
564
- return {
565
- error: "Case-insensitive Lua pattern search is not supported. Omit caseSensitive or pass caseSensitive: true with usePattern: true, or use literal search.",
566
- };
567
- }
568
-
569
- const caseSensitive = usePattern ? true : ((requestData.caseSensitive as boolean) ?? false);
570
- const contextLines = (requestData.contextLines as number) ?? 0;
571
- const maxResults = (requestData.maxResults as number) ?? 100;
572
- const maxResultsPerScript = (requestData.maxResultsPerScript as number) ?? 0;
573
- const filesOnly = (requestData.filesOnly as boolean) ?? false;
574
- const searchPath = (requestData.path as string) ?? "";
575
- const classFilter = requestData.classFilter as string | undefined;
576
-
577
- const startInstance = searchPath !== "" ? getInstanceByPath(searchPath) : game;
578
- if (!startInstance) return { error: `Path not found: ${searchPath}` };
579
-
580
- // Prepare pattern for matching
581
- const searchPattern = caseSensitive ? pattern : pattern.lower();
582
- // Pre-split top-level "|" alternation once (pattern mode only).
583
- const patternAlternatives = usePattern ? splitLuaAlternation(searchPattern) : undefined;
584
-
585
- interface LineMatch {
586
- line: number;
587
- column: number;
588
- text: string;
589
- before: string[];
590
- after: string[];
591
- }
592
-
593
- interface ScriptResult {
594
- instancePath: string;
595
- name: string;
596
- className: string;
597
- enabled?: boolean;
598
- matches: LineMatch[];
599
- }
600
-
601
- const results: ScriptResult[] = [];
602
- let totalMatches = 0;
603
- let scriptsSearched = 0;
604
- let hitLimit = false;
605
-
606
- function searchInstance(instance: Instance) {
607
- if (hitLimit) return;
608
-
609
- if (instance.IsA("LuaSourceContainer")) {
610
- // Apply class filter
611
- if (classFilter) {
612
- if (!instance.ClassName.lower().find(classFilter.lower())[0]) return;
613
- }
614
-
615
- scriptsSearched++;
616
- const source = readScriptSource(instance);
617
- const [lines] = Utils.splitLines(source);
618
- const scriptMatches: LineMatch[] = [];
619
- let scriptMatchCount = 0;
620
-
621
- for (let i = 0; i < lines.size(); i++) {
622
- if (hitLimit) break;
623
- if (maxResultsPerScript > 0 && scriptMatchCount >= maxResultsPerScript) break;
624
-
625
- const line = lines[i];
626
- const searchLine = caseSensitive ? line : line.lower();
627
-
628
- let matchStart: number | undefined;
629
- let matchEnd: number | undefined;
630
-
631
- if (usePattern) {
632
- [matchStart, matchEnd] = findFirstPattern(searchLine, patternAlternatives!);
633
- } else {
634
- [matchStart, matchEnd] = string.find(searchLine, searchPattern, 1, true);
635
- }
636
-
637
- if (matchStart !== undefined) {
638
- scriptMatchCount++;
639
- totalMatches++;
640
-
641
- if (totalMatches > maxResults) {
642
- hitLimit = true;
643
- break;
644
- }
645
-
646
- if (!filesOnly) {
647
- // Gather context lines
648
- const before: string[] = [];
649
- const after: string[] = [];
650
-
651
- if (contextLines > 0) {
652
- const beforeStart = math.max(0, i - contextLines);
653
- for (let j = beforeStart; j < i; j++) {
654
- before.push(lines[j]);
655
- }
656
- const afterEnd = math.min(lines.size() - 1, i + contextLines);
657
- for (let j = i + 1; j <= afterEnd; j++) {
658
- after.push(lines[j]);
659
- }
660
- }
661
-
662
- scriptMatches.push({
663
- line: i + 1, // 1-indexed
664
- column: matchStart,
665
- text: line,
666
- before,
667
- after,
668
- });
669
- }
670
- }
671
- }
672
-
673
- if (scriptMatchCount > 0) {
674
- const scriptResult: ScriptResult = {
675
- instancePath: getInstancePath(instance),
676
- name: instance.Name,
677
- className: instance.ClassName,
678
- matches: scriptMatches,
679
- };
680
- if (instance.IsA("BaseScript")) {
681
- scriptResult.enabled = instance.Enabled;
682
- }
683
- results.push(scriptResult);
684
- }
685
- }
686
-
687
- for (const child of instance.GetChildren()) {
688
- if (hitLimit) return;
689
- searchInstance(child);
690
- }
691
- }
692
-
693
- searchInstance(startInstance);
694
-
695
- return {
696
- results,
697
- pattern,
698
- totalMatches: hitLimit ? `>${maxResults}` : totalMatches,
699
- scriptsSearched,
700
- scriptsMatched: results.size(),
701
- truncated: hitLimit,
702
- options: { caseSensitive, contextLines, usePattern, filesOnly, maxResults, maxResultsPerScript },
703
- };
704
- }
705
-
706
- export = {
707
- getFileTree,
708
- searchFiles,
709
- getPlaceInfo,
710
- searchObjects,
711
- getInstanceProperties,
712
- searchByProperty,
713
- getClassInfo,
714
- getProjectStructure,
715
- grepScripts,
716
- };