@chrrxs/robloxstudio-mcp-inspector 2.8.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 (30) hide show
  1. package/dist/index.js +4483 -0
  2. package/package.json +50 -0
  3. package/studio-plugin/INSTALLATION.md +150 -0
  4. package/studio-plugin/MCPInspectorPlugin.rbxmx +9074 -0
  5. package/studio-plugin/MCPPlugin.rbxmx +9074 -0
  6. package/studio-plugin/default.project.json +19 -0
  7. package/studio-plugin/dev.project.json +23 -0
  8. package/studio-plugin/inspector-icon.png +0 -0
  9. package/studio-plugin/package-lock.json +706 -0
  10. package/studio-plugin/package.json +19 -0
  11. package/studio-plugin/plugin.json +10 -0
  12. package/studio-plugin/src/modules/ClientBroker.ts +221 -0
  13. package/studio-plugin/src/modules/Communication.ts +399 -0
  14. package/studio-plugin/src/modules/Recording.ts +28 -0
  15. package/studio-plugin/src/modules/State.ts +94 -0
  16. package/studio-plugin/src/modules/UI.ts +725 -0
  17. package/studio-plugin/src/modules/Utils.ts +318 -0
  18. package/studio-plugin/src/modules/handlers/AssetHandlers.ts +241 -0
  19. package/studio-plugin/src/modules/handlers/BuildHandlers.ts +481 -0
  20. package/studio-plugin/src/modules/handlers/CaptureHandlers.ts +128 -0
  21. package/studio-plugin/src/modules/handlers/InputHandlers.ts +102 -0
  22. package/studio-plugin/src/modules/handlers/InstanceHandlers.ts +380 -0
  23. package/studio-plugin/src/modules/handlers/MetadataHandlers.ts +391 -0
  24. package/studio-plugin/src/modules/handlers/PropertyHandlers.ts +191 -0
  25. package/studio-plugin/src/modules/handlers/QueryHandlers.ts +827 -0
  26. package/studio-plugin/src/modules/handlers/ScriptHandlers.ts +530 -0
  27. package/studio-plugin/src/modules/handlers/TestHandlers.ts +277 -0
  28. package/studio-plugin/src/server/index.server.ts +63 -0
  29. package/studio-plugin/src/types/index.d.ts +44 -0
  30. package/studio-plugin/tsconfig.json +20 -0
@@ -0,0 +1,827 @@
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
+ return {
100
+ placeName: game.Name,
101
+ placeId: game.PlaceId,
102
+ gameId: game.GameId,
103
+ jobId: game.JobId,
104
+ workspace: {
105
+ name: game.Workspace.Name,
106
+ className: game.Workspace.ClassName,
107
+ },
108
+ };
109
+ }
110
+
111
+ function getServices(requestData: Record<string, unknown>) {
112
+ const serviceName = requestData.serviceName as string | undefined;
113
+
114
+ if (serviceName) {
115
+ const [ok, service] = pcall(() => game.GetService(serviceName as keyof Services));
116
+ if (ok && service) {
117
+ return {
118
+ service: {
119
+ name: service.Name,
120
+ className: service.ClassName,
121
+ path: getInstancePath(service as Instance),
122
+ childCount: (service as Instance).GetChildren().size(),
123
+ },
124
+ };
125
+ } else {
126
+ return { error: `Service not found: ${serviceName}` };
127
+ }
128
+ } else {
129
+ const services: { name: string; className: string; path: string; childCount: number }[] = [];
130
+ const commonServices = [
131
+ "Workspace", "Players", "StarterGui", "StarterPack", "StarterPlayer",
132
+ "ReplicatedStorage", "ServerStorage", "ServerScriptService",
133
+ "HttpService", "TeleportService", "DataStoreService",
134
+ ];
135
+
136
+ for (const svcName of commonServices) {
137
+ const [ok, service] = pcall(() => game.GetService(svcName as keyof Services));
138
+ if (ok && service) {
139
+ services.push({
140
+ name: service.Name,
141
+ className: service.ClassName,
142
+ path: getInstancePath(service as Instance),
143
+ childCount: (service as Instance).GetChildren().size(),
144
+ });
145
+ }
146
+ }
147
+
148
+ return { services };
149
+ }
150
+ }
151
+
152
+ function searchObjects(requestData: Record<string, unknown>) {
153
+ const query = requestData.query as string;
154
+ const searchType = (requestData.searchType as string) ?? "name";
155
+ const propertyName = requestData.propertyName as string | undefined;
156
+
157
+ if (!query) return { error: "Query is required" };
158
+
159
+ const results: { name: string; className: string; path: string }[] = [];
160
+
161
+ function searchRecursive(instance: Instance) {
162
+ let match = false;
163
+
164
+ if (searchType === "name") {
165
+ match = instance.Name.lower().find(query.lower())[0] !== undefined;
166
+ } else if (searchType === "class") {
167
+ match = instance.ClassName.lower().find(query.lower())[0] !== undefined;
168
+ } else if (searchType === "property" && propertyName) {
169
+ const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
170
+ if (success) {
171
+ match = (value as string).lower().find(query.lower())[0] !== undefined;
172
+ }
173
+ }
174
+
175
+ if (match) {
176
+ results.push({
177
+ name: instance.Name,
178
+ className: instance.ClassName,
179
+ path: getInstancePath(instance),
180
+ });
181
+ }
182
+
183
+ for (const child of instance.GetChildren()) {
184
+ searchRecursive(child);
185
+ }
186
+ }
187
+
188
+ searchRecursive(game);
189
+
190
+ return { results, query, searchType, count: results.size() };
191
+ }
192
+
193
+ function getInstanceProperties(requestData: Record<string, unknown>) {
194
+ const instancePath = requestData.instancePath as string;
195
+ const excludeSource = (requestData.excludeSource as boolean) ?? false;
196
+ if (!instancePath) return { error: "Instance path is required" };
197
+
198
+ const instance = getInstanceByPath(instancePath);
199
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
200
+
201
+ const properties: Record<string, unknown> = {};
202
+ const [success, result] = pcall(() => {
203
+ const basicProps = ["Name", "ClassName", "Parent"];
204
+ for (const prop of basicProps) {
205
+ const [propSuccess, propValue] = pcall(() => {
206
+ const val = (instance as unknown as Record<string, unknown>)[prop];
207
+ if (prop === "Parent" && val) return getInstancePath(val as Instance);
208
+ if (val === undefined) return "nil";
209
+ return tostring(val);
210
+ });
211
+ if (propSuccess) properties[prop] = propValue;
212
+ }
213
+
214
+ const commonProps = [
215
+ "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
216
+ "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
217
+ "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
218
+ "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
219
+ "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
220
+ "Face", "SurfaceType",
221
+ ];
222
+
223
+ for (const prop of commonProps) {
224
+ const [propSuccess, propValue] = pcall(() => {
225
+ const val = (instance as unknown as Record<string, unknown>)[prop];
226
+ if (typeOf(val) === "UDim2") {
227
+ const udim = val as UDim2;
228
+ return {
229
+ X: { Scale: udim.X.Scale, Offset: udim.X.Offset },
230
+ Y: { Scale: udim.Y.Scale, Offset: udim.Y.Offset },
231
+ _type: "UDim2",
232
+ };
233
+ }
234
+ return tostring(val);
235
+ });
236
+ if (propSuccess) properties[prop] = propValue;
237
+ }
238
+
239
+ if (instance.IsA("LuaSourceContainer")) {
240
+ if (!excludeSource) {
241
+ properties.Source = readScriptSource(instance);
242
+ } else {
243
+ const src = readScriptSource(instance);
244
+ properties.SourceLength = src.size();
245
+ properties.LineCount = Utils.splitLines(src)[0].size();
246
+ }
247
+ if (instance.IsA("BaseScript")) {
248
+ properties.Enabled = tostring(instance.Enabled);
249
+ }
250
+ }
251
+
252
+ if (instance.IsA("Part")) {
253
+ properties.Shape = tostring(instance.Shape);
254
+ }
255
+
256
+ if (instance.IsA("BasePart")) {
257
+ properties.TopSurface = tostring(instance.TopSurface);
258
+ properties.BottomSurface = tostring(instance.BottomSurface);
259
+ }
260
+
261
+ if (instance.IsA("MeshPart")) {
262
+ properties.MeshId = tostring(instance.MeshId);
263
+ properties.TextureID = tostring(instance.TextureID);
264
+ }
265
+
266
+ if (instance.IsA("SpecialMesh")) {
267
+ properties.MeshId = tostring(instance.MeshId);
268
+ properties.TextureId = tostring(instance.TextureId);
269
+ properties.MeshType = tostring(instance.MeshType);
270
+ }
271
+
272
+ if (instance.IsA("Sound")) {
273
+ properties.SoundId = tostring(instance.SoundId);
274
+ properties.TimeLength = tostring(instance.TimeLength);
275
+ properties.IsPlaying = tostring(instance.IsPlaying);
276
+ }
277
+
278
+ if (instance.IsA("Animation")) {
279
+ properties.AnimationId = tostring(instance.AnimationId);
280
+ }
281
+
282
+ if (instance.IsA("Decal") || instance.IsA("Texture")) {
283
+ properties.Texture = tostring((instance as Decal | Texture).Texture);
284
+ }
285
+
286
+ if (instance.IsA("Shirt")) {
287
+ properties.ShirtTemplate = tostring(instance.ShirtTemplate);
288
+ } else if (instance.IsA("Pants")) {
289
+ properties.PantsTemplate = tostring(instance.PantsTemplate);
290
+ } else if (instance.IsA("ShirtGraphic")) {
291
+ properties.Graphic = tostring(instance.Graphic);
292
+ }
293
+
294
+ properties.ChildCount = tostring(instance.GetChildren().size());
295
+ });
296
+
297
+ if (success) {
298
+ return { instancePath, className: instance.ClassName, properties };
299
+ } else {
300
+ return { error: `Failed to get properties: ${result}` };
301
+ }
302
+ }
303
+
304
+ function getInstanceChildren(requestData: Record<string, unknown>) {
305
+ const instancePath = requestData.instancePath as string;
306
+ if (!instancePath) return { error: "Instance path is required" };
307
+
308
+ const instance = getInstanceByPath(instancePath);
309
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
310
+
311
+ const children: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean }[] = [];
312
+ for (const child of instance.GetChildren()) {
313
+ const entry: { name: string; className: string; path: string; hasChildren: boolean; hasSource: boolean; enabled?: boolean } = {
314
+ name: child.Name,
315
+ className: child.ClassName,
316
+ path: getInstancePath(child),
317
+ hasChildren: child.GetChildren().size() > 0,
318
+ hasSource: child.IsA("LuaSourceContainer"),
319
+ };
320
+ if (child.IsA("BaseScript")) {
321
+ entry.enabled = child.Enabled;
322
+ }
323
+ children.push(entry);
324
+ }
325
+
326
+ return { instancePath, children, count: children.size() };
327
+ }
328
+
329
+ function searchByProperty(requestData: Record<string, unknown>) {
330
+ const propertyName = requestData.propertyName as string;
331
+ const propertyValue = requestData.propertyValue as string;
332
+
333
+ if (!propertyName || !propertyValue) {
334
+ return { error: "Property name and value are required" };
335
+ }
336
+
337
+ const results: { name: string; className: string; path: string; propertyValue: string }[] = [];
338
+
339
+ function searchRecursive(instance: Instance) {
340
+ const [success, value] = pcall(() => tostring((instance as unknown as Record<string, unknown>)[propertyName]));
341
+ if (success && (value as string).lower().find(propertyValue.lower())[0] !== undefined) {
342
+ results.push({
343
+ name: instance.Name,
344
+ className: instance.ClassName,
345
+ path: getInstancePath(instance),
346
+ propertyValue: value as string,
347
+ });
348
+ }
349
+ for (const child of instance.GetChildren()) {
350
+ searchRecursive(child);
351
+ }
352
+ }
353
+
354
+ searchRecursive(game);
355
+ return { propertyName, propertyValue, results, count: results.size() };
356
+ }
357
+
358
+ function getClassInfo(requestData: Record<string, unknown>) {
359
+ const className = requestData.className as string;
360
+ if (!className) return { error: "Class name is required" };
361
+
362
+ let [success, tempInstance] = pcall(() => new Instance(className as keyof CreatableInstances));
363
+ let isService = false;
364
+
365
+ if (!success) {
366
+ const [serviceSuccess, serviceInstance] = pcall(() =>
367
+ game.GetService(className as keyof Services),
368
+ );
369
+ if (serviceSuccess && serviceInstance) {
370
+ success = true;
371
+ tempInstance = serviceInstance as unknown as Instance;
372
+ isService = true;
373
+ }
374
+ }
375
+
376
+ if (!success) return { error: `Invalid class name: ${className}` };
377
+
378
+ const classInfo: {
379
+ className: string;
380
+ isService: boolean;
381
+ properties: string[];
382
+ methods: string[];
383
+ events: string[];
384
+ } = { className, isService, properties: [], methods: [], events: [] };
385
+
386
+ const commonProps = [
387
+ "Name", "ClassName", "Parent", "Size", "Position", "Rotation", "CFrame",
388
+ "Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color",
389
+ "Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible",
390
+ "Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency",
391
+ "ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness",
392
+ "Range", "Shadows",
393
+ ];
394
+
395
+ for (const prop of commonProps) {
396
+ const [propSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[prop]);
397
+ if (propSuccess) classInfo.properties.push(prop);
398
+ }
399
+
400
+ const commonMethods = [
401
+ "Destroy", "Clone", "FindFirstChild", "FindFirstChildOfClass",
402
+ "GetChildren", "IsA", "IsAncestorOf", "IsDescendantOf", "WaitForChild",
403
+ ];
404
+
405
+ for (const method of commonMethods) {
406
+ const [methodSuccess] = pcall(() => (tempInstance as unknown as Record<string, unknown>)[method]);
407
+ if (methodSuccess) classInfo.methods.push(method);
408
+ }
409
+
410
+ if (!isService) {
411
+ (tempInstance as Instance).Destroy();
412
+ }
413
+
414
+ return classInfo;
415
+ }
416
+
417
+ function getProjectStructure(requestData: Record<string, unknown>) {
418
+ const startPath = (requestData.path as string) ?? "";
419
+ const maxDepth = (requestData.maxDepth as number) ?? 3;
420
+ const showScriptsOnly = (requestData.scriptsOnly as boolean) ?? false;
421
+
422
+ if (startPath === "" || startPath === "game") {
423
+ const services: Record<string, unknown>[] = [];
424
+ const mainServices = [
425
+ "Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage",
426
+ "StarterGui", "StarterPack", "StarterPlayer", "Players",
427
+ ];
428
+
429
+ for (const serviceName of mainServices) {
430
+ const [svcOk, service] = pcall(() => game.GetService(serviceName as keyof Services));
431
+ if (svcOk && service) {
432
+ services.push({
433
+ name: service.Name,
434
+ className: service.ClassName,
435
+ path: getInstancePath(service as Instance),
436
+ childCount: (service as Instance).GetChildren().size(),
437
+ hasChildren: (service as Instance).GetChildren().size() > 0,
438
+ });
439
+ }
440
+ }
441
+
442
+ return {
443
+ type: "service_overview",
444
+ services,
445
+ timestamp: tick(),
446
+ note: "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
447
+ };
448
+ }
449
+
450
+ const startInstance = getInstanceByPath(startPath);
451
+ if (!startInstance) return { error: `Path not found: ${startPath}` };
452
+
453
+ function getStructure(instance: Instance, depth: number): Record<string, unknown> {
454
+ if (depth > maxDepth) {
455
+ return {
456
+ name: instance.Name,
457
+ className: instance.ClassName,
458
+ path: getInstancePath(instance),
459
+ childCount: instance.GetChildren().size(),
460
+ hasMore: true,
461
+ note: "Max depth reached - use this path to explore further",
462
+ };
463
+ }
464
+
465
+ const node: Record<string, unknown> = {
466
+ name: instance.Name,
467
+ className: instance.ClassName,
468
+ path: getInstancePath(instance),
469
+ children: [] as Record<string, unknown>[],
470
+ };
471
+
472
+ if (instance.IsA("LuaSourceContainer")) {
473
+ node.hasSource = true;
474
+ node.scriptType = instance.ClassName;
475
+ if (instance.IsA("BaseScript")) {
476
+ node.enabled = instance.Enabled;
477
+ }
478
+ }
479
+
480
+ if (instance.IsA("GuiObject")) {
481
+ node.visible = instance.Visible;
482
+ if (instance.IsA("Frame") || instance.IsA("ScreenGui")) {
483
+ node.guiType = "container";
484
+ } else if (instance.IsA("TextLabel") || instance.IsA("TextButton")) {
485
+ node.guiType = "text";
486
+ const textInst = instance as TextLabel | TextButton;
487
+ if (textInst.Text !== "") node.text = textInst.Text;
488
+ } else if (instance.IsA("ImageLabel") || instance.IsA("ImageButton")) {
489
+ node.guiType = "image";
490
+ }
491
+ }
492
+
493
+ let children = instance.GetChildren();
494
+ if (showScriptsOnly) {
495
+ children = children.filter(
496
+ (child) => child.IsA("BaseScript") || child.IsA("Folder") || child.IsA("ModuleScript"),
497
+ );
498
+ }
499
+
500
+ const nodeChildren = node.children as Record<string, unknown>[];
501
+ const childCount = children.size();
502
+ if (childCount > 20 && depth < maxDepth) {
503
+ const classGroups = new Map<string, Instance[]>();
504
+ for (const child of children) {
505
+ const cn = child.ClassName;
506
+ if (!classGroups.has(cn)) classGroups.set(cn, []);
507
+ classGroups.get(cn)!.push(child);
508
+ }
509
+
510
+ const childSummary: Record<string, unknown>[] = [];
511
+ classGroups.forEach((classChildren, cn) => {
512
+ childSummary.push({
513
+ className: cn,
514
+ count: classChildren.size(),
515
+ examples: [classChildren[0]?.Name, classChildren[1]?.Name],
516
+ });
517
+ });
518
+ node.childSummary = childSummary;
519
+
520
+ classGroups.forEach((classChildren, cn) => {
521
+ const limit = math.min(3, classChildren.size());
522
+ for (let i = 0; i < limit; i++) {
523
+ nodeChildren.push(getStructure(classChildren[i], depth + 1));
524
+ }
525
+ if (classChildren.size() > 3) {
526
+ nodeChildren.push({
527
+ name: `... ${classChildren.size() - 3} more ${cn} objects`,
528
+ className: "MoreIndicator",
529
+ path: `${getInstancePath(instance)} [${cn} children]`,
530
+ note: "Use specific path to explore these objects",
531
+ });
532
+ }
533
+ });
534
+ } else {
535
+ for (const child of children) {
536
+ nodeChildren.push(getStructure(child, depth + 1));
537
+ }
538
+ }
539
+
540
+ return node;
541
+ }
542
+
543
+ const result = getStructure(startInstance, 0);
544
+ result.requestedPath = startPath;
545
+ result.maxDepth = maxDepth;
546
+ result.scriptsOnly = showScriptsOnly;
547
+ result.timestamp = tick();
548
+
549
+ return result;
550
+ }
551
+
552
+ function grepScripts(requestData: Record<string, unknown>) {
553
+ const pattern = requestData.pattern as string;
554
+ if (!pattern) return { error: "pattern is required" };
555
+
556
+ const caseSensitive = (requestData.caseSensitive as boolean) ?? false;
557
+ const contextLines = (requestData.contextLines as number) ?? 0;
558
+ const maxResults = (requestData.maxResults as number) ?? 100;
559
+ const maxResultsPerScript = (requestData.maxResultsPerScript as number) ?? 0;
560
+ const usePattern = (requestData.usePattern as boolean) ?? false;
561
+ const filesOnly = (requestData.filesOnly as boolean) ?? false;
562
+ const searchPath = (requestData.path as string) ?? "";
563
+ const classFilter = requestData.classFilter as string | undefined;
564
+
565
+ const startInstance = searchPath !== "" ? getInstanceByPath(searchPath) : game;
566
+ if (!startInstance) return { error: `Path not found: ${searchPath}` };
567
+
568
+ // Prepare pattern for matching
569
+ const searchPattern = caseSensitive ? pattern : pattern.lower();
570
+
571
+ interface LineMatch {
572
+ line: number;
573
+ column: number;
574
+ text: string;
575
+ before: string[];
576
+ after: string[];
577
+ }
578
+
579
+ interface ScriptResult {
580
+ instancePath: string;
581
+ name: string;
582
+ className: string;
583
+ enabled?: boolean;
584
+ matches: LineMatch[];
585
+ }
586
+
587
+ const results: ScriptResult[] = [];
588
+ let totalMatches = 0;
589
+ let scriptsSearched = 0;
590
+ let hitLimit = false;
591
+
592
+ function searchInstance(instance: Instance) {
593
+ if (hitLimit) return;
594
+
595
+ if (instance.IsA("LuaSourceContainer")) {
596
+ // Apply class filter
597
+ if (classFilter) {
598
+ if (!instance.ClassName.lower().find(classFilter.lower())[0]) return;
599
+ }
600
+
601
+ scriptsSearched++;
602
+ const source = readScriptSource(instance);
603
+ const [lines] = Utils.splitLines(source);
604
+ const scriptMatches: LineMatch[] = [];
605
+ let scriptMatchCount = 0;
606
+
607
+ for (let i = 0; i < lines.size(); i++) {
608
+ if (hitLimit) break;
609
+ if (maxResultsPerScript > 0 && scriptMatchCount >= maxResultsPerScript) break;
610
+
611
+ const line = lines[i];
612
+ const searchLine = caseSensitive ? line : line.lower();
613
+
614
+ let matchStart: number | undefined;
615
+ let matchEnd: number | undefined;
616
+
617
+ if (usePattern) {
618
+ [matchStart, matchEnd] = string.find(searchLine, searchPattern);
619
+ } else {
620
+ [matchStart, matchEnd] = string.find(searchLine, searchPattern, 1, true);
621
+ }
622
+
623
+ if (matchStart !== undefined) {
624
+ scriptMatchCount++;
625
+ totalMatches++;
626
+
627
+ if (totalMatches > maxResults) {
628
+ hitLimit = true;
629
+ break;
630
+ }
631
+
632
+ if (!filesOnly) {
633
+ // Gather context lines
634
+ const before: string[] = [];
635
+ const after: string[] = [];
636
+
637
+ if (contextLines > 0) {
638
+ const beforeStart = math.max(0, i - contextLines);
639
+ for (let j = beforeStart; j < i; j++) {
640
+ before.push(lines[j]);
641
+ }
642
+ const afterEnd = math.min(lines.size() - 1, i + contextLines);
643
+ for (let j = i + 1; j <= afterEnd; j++) {
644
+ after.push(lines[j]);
645
+ }
646
+ }
647
+
648
+ scriptMatches.push({
649
+ line: i + 1, // 1-indexed
650
+ column: matchStart,
651
+ text: line,
652
+ before,
653
+ after,
654
+ });
655
+ }
656
+ }
657
+ }
658
+
659
+ if (scriptMatchCount > 0) {
660
+ const scriptResult: ScriptResult = {
661
+ instancePath: getInstancePath(instance),
662
+ name: instance.Name,
663
+ className: instance.ClassName,
664
+ matches: scriptMatches,
665
+ };
666
+ if (instance.IsA("BaseScript")) {
667
+ scriptResult.enabled = instance.Enabled;
668
+ }
669
+ results.push(scriptResult);
670
+ }
671
+ }
672
+
673
+ for (const child of instance.GetChildren()) {
674
+ if (hitLimit) return;
675
+ searchInstance(child);
676
+ }
677
+ }
678
+
679
+ searchInstance(startInstance);
680
+
681
+ return {
682
+ results,
683
+ pattern,
684
+ totalMatches: hitLimit ? `>${maxResults}` : totalMatches,
685
+ scriptsSearched,
686
+ scriptsMatched: results.size(),
687
+ truncated: hitLimit,
688
+ options: { caseSensitive, contextLines, usePattern, filesOnly, maxResults, maxResultsPerScript },
689
+ };
690
+ }
691
+
692
+ function getDescendants(requestData: Record<string, unknown>) {
693
+ const instancePath = requestData.instancePath as string;
694
+ if (!instancePath) return { error: "Instance path is required" };
695
+
696
+ const maxDepth = (requestData.maxDepth as number) ?? 10;
697
+ const classFilter = requestData.classFilter as string | undefined;
698
+
699
+ const instance = getInstanceByPath(instancePath);
700
+ if (!instance) return { error: `Instance not found: ${instancePath}` };
701
+
702
+ const descendants: { name: string; className: string; path: string; depth: number }[] = [];
703
+
704
+ function collect(inst: Instance, depth: number) {
705
+ if (depth > maxDepth) return;
706
+ for (const child of inst.GetChildren()) {
707
+ if (classFilter && !child.IsA(classFilter as keyof Instances)) continue;
708
+ descendants.push({
709
+ name: child.Name,
710
+ className: child.ClassName,
711
+ path: getInstancePath(child),
712
+ depth,
713
+ });
714
+ collect(child, depth + 1);
715
+ }
716
+ }
717
+
718
+ collect(instance, 1);
719
+
720
+ return { instancePath, descendants, count: descendants.size(), maxDepth };
721
+ }
722
+
723
+ function compareInstances(requestData: Record<string, unknown>) {
724
+ const instancePathA = requestData.instancePathA as string;
725
+ const instancePathB = requestData.instancePathB as string;
726
+
727
+ if (!instancePathA || !instancePathB) {
728
+ return { error: "Both instancePathA and instancePathB are required" };
729
+ }
730
+
731
+ const instA = getInstanceByPath(instancePathA);
732
+ if (!instA) return { error: `Instance not found: ${instancePathA}` };
733
+
734
+ const instB = getInstanceByPath(instancePathB);
735
+ if (!instB) return { error: `Instance not found: ${instancePathB}` };
736
+
737
+ const commonProps = [
738
+ "Name", "ClassName",
739
+ "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide",
740
+ "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3",
741
+ "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex",
742
+ "BorderSizePixel", "BackgroundTransparency", "ImageTransparency",
743
+ "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows",
744
+ ];
745
+
746
+ const matching: Record<string, string> = {};
747
+ const differing: Record<string, { a: string; b: string }> = {};
748
+ const onlyA: string[] = [];
749
+ const onlyB: string[] = [];
750
+
751
+ for (const prop of commonProps) {
752
+ const [okA, valA] = pcall(() => tostring((instA as unknown as Record<string, unknown>)[prop]));
753
+ const [okB, valB] = pcall(() => tostring((instB as unknown as Record<string, unknown>)[prop]));
754
+
755
+ if (okA && okB) {
756
+ if (valA === valB) {
757
+ matching[prop] = valA as string;
758
+ } else {
759
+ differing[prop] = { a: valA as string, b: valB as string };
760
+ }
761
+ } else if (okA) {
762
+ onlyA.push(prop);
763
+ } else if (okB) {
764
+ onlyB.push(prop);
765
+ }
766
+ }
767
+
768
+ return {
769
+ instancePathA,
770
+ instancePathB,
771
+ classNameA: instA.ClassName,
772
+ classNameB: instB.ClassName,
773
+ matching,
774
+ differing,
775
+ onlyA,
776
+ onlyB,
777
+ };
778
+ }
779
+
780
+ function getOutputLog(requestData: Record<string, unknown>) {
781
+ const maxEntries = (requestData.maxEntries as number) ?? 100;
782
+ const messageTypeFilter = requestData.messageType as string | undefined;
783
+
784
+ const [success, result] = pcall(() => {
785
+ const LogService = game.GetService("LogService");
786
+ const history = LogService.GetLogHistory();
787
+ const allEntries: Record<string, unknown>[] = [];
788
+
789
+ for (const entry of history) {
790
+ const msgType = tostring(entry.messageType);
791
+ if (messageTypeFilter && msgType !== messageTypeFilter) continue;
792
+ allEntries.push({
793
+ message: entry.message,
794
+ messageType: msgType,
795
+ timestamp: entry.timestamp,
796
+ });
797
+ }
798
+
799
+ const startIdx = math.max(0, allEntries.size() - maxEntries);
800
+ const finalEntries: Record<string, unknown>[] = [];
801
+ for (let i = startIdx; i < allEntries.size(); i++) {
802
+ finalEntries.push(allEntries[i]);
803
+ }
804
+
805
+ return { entries: finalEntries, count: finalEntries.size(), totalAvailable: allEntries.size() };
806
+ });
807
+
808
+ if (success) return result;
809
+ return { error: `Failed to get output log: ${result}` };
810
+ }
811
+
812
+ export = {
813
+ getFileTree,
814
+ searchFiles,
815
+ getPlaceInfo,
816
+ getServices,
817
+ searchObjects,
818
+ getInstanceProperties,
819
+ getInstanceChildren,
820
+ searchByProperty,
821
+ getClassInfo,
822
+ getProjectStructure,
823
+ grepScripts,
824
+ getDescendants,
825
+ compareInstances,
826
+ getOutputLog,
827
+ };