rbxstudio-mcp 1.10.0 → 1.12.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.
@@ -17,6 +17,30 @@ end)
17
17
  local outputBuffer = {}
18
18
  local MAX_OUTPUT_BUFFER = 1000
19
19
 
20
+ -- Track action history for enhanced undo/redo
21
+ local actionHistory = {} -- Stack of actions that can be undone
22
+ local redoHistory = {} -- Stack of actions that can be redone
23
+ local MAX_ACTION_HISTORY = 100
24
+
25
+ -- Helper to log an action for undo tracking
26
+ local function logAction(actionType, target, summary, details)
27
+ -- Clear redo history when a new action is performed
28
+ redoHistory = {}
29
+
30
+ table.insert(actionHistory, {
31
+ action = actionType,
32
+ target = target,
33
+ summary = summary,
34
+ details = details or {},
35
+ timestamp = os.time()
36
+ })
37
+
38
+ -- Keep history from growing too large
39
+ if #actionHistory > MAX_ACTION_HISTORY then
40
+ table.remove(actionHistory, 1)
41
+ end
42
+ end
43
+
20
44
  -- Connect to LogService to capture output
21
45
  LogService.MessageOut:Connect(function(message, messageType)
22
46
  table.insert(outputBuffer, {
@@ -55,7 +79,7 @@ local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
55
79
  "MCPServerInterface",
56
80
  DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
57
81
  )
58
- screenGui.Title = "MCP Server v1.10.0"
82
+ screenGui.Title = "MCP Server v1.12.0"
59
83
 
60
84
  local mainFrame = Instance.new("Frame")
61
85
  mainFrame.Size = UDim2.new(1, 0, 1, 0)
@@ -108,7 +132,7 @@ local versionLabel = Instance.new("TextLabel")
108
132
  versionLabel.Size = UDim2.new(1, 0, 0, 16)
109
133
  versionLabel.Position = UDim2.new(0, 0, 0, 32)
110
134
  versionLabel.BackgroundTransparency = 1
111
- versionLabel.Text = "AI Integration • v1.10.0"
135
+ versionLabel.Text = "AI Integration • v1.12.0"
112
136
  versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
113
137
  versionLabel.TextScaled = false
114
138
  versionLabel.TextSize = 12
@@ -739,50 +763,11 @@ local mutationEndpoints = {
739
763
  ["/api/remove-tag"] = "Remove Tag",
740
764
  ["/api/clone-instance"] = "Clone Instance",
741
765
  ["/api/move-instance"] = "Move Instance",
766
+ ["/api/insert-asset"] = "Insert Asset",
742
767
  }
743
768
 
744
- -- Endpoint to handler mapping
745
- local endpointHandlers = {
746
- ["/api/file-tree"] = handlers.getFileTree,
747
- ["/api/search-files"] = handlers.searchFiles,
748
- ["/api/place-info"] = handlers.getPlaceInfo,
749
- ["/api/services"] = handlers.getServices,
750
- ["/api/search-objects"] = handlers.searchObjects,
751
- ["/api/instance-properties"] = handlers.getInstanceProperties,
752
- ["/api/instance-children"] = handlers.getInstanceChildren,
753
- ["/api/search-by-property"] = handlers.searchByProperty,
754
- ["/api/class-info"] = handlers.getClassInfo,
755
- ["/api/project-structure"] = handlers.getProjectStructure,
756
- ["/api/set-property"] = handlers.setProperty,
757
- ["/api/mass-set-property"] = handlers.massSetProperty,
758
- ["/api/mass-get-property"] = handlers.massGetProperty,
759
- ["/api/create-object"] = handlers.createObject,
760
- ["/api/mass-create-objects"] = handlers.massCreateObjects,
761
- ["/api/mass-create-objects-with-properties"] = handlers.massCreateObjectsWithProperties,
762
- ["/api/delete-object"] = handlers.deleteObject,
763
- ["/api/smart-duplicate"] = handlers.smartDuplicate,
764
- ["/api/mass-duplicate"] = handlers.massDuplicate,
765
- ["/api/set-calculated-property"] = handlers.setCalculatedProperty,
766
- ["/api/set-relative-property"] = handlers.setRelativeProperty,
767
- ["/api/get-script-source"] = handlers.getScriptSource,
768
- ["/api/set-script-source"] = handlers.setScriptSource,
769
- ["/api/edit-script-lines"] = handlers.editScriptLines,
770
- ["/api/insert-script-lines"] = handlers.insertScriptLines,
771
- ["/api/delete-script-lines"] = handlers.deleteScriptLines,
772
- ["/api/get-attribute"] = handlers.getAttribute,
773
- ["/api/set-attribute"] = handlers.setAttribute,
774
- ["/api/get-attributes"] = handlers.getAttributes,
775
- ["/api/delete-attribute"] = handlers.deleteAttribute,
776
- ["/api/get-tags"] = handlers.getTags,
777
- ["/api/add-tag"] = handlers.addTag,
778
- ["/api/remove-tag"] = handlers.removeTag,
779
- ["/api/get-tagged"] = handlers.getTagged,
780
- ["/api/get-selection"] = handlers.getSelection,
781
- ["/api/get-output"] = handlers.getOutput,
782
- ["/api/clone-instance"] = handlers.cloneInstance,
783
- ["/api/move-instance"] = handlers.moveInstance,
784
- ["/api/validate-script"] = handlers.validateScript,
785
- }
769
+ -- Endpoint to handler mapping (populated after handlers are defined below)
770
+ local endpointHandlers
786
771
 
787
772
  processRequest = function(request)
788
773
  local endpoint = request.endpoint
@@ -1504,6 +1489,11 @@ handlers.setProperty = function(requestData)
1504
1489
  end)
1505
1490
 
1506
1491
  if success and result ~= false then
1492
+ -- Log for undo tracking
1493
+ logAction("set_property", instancePath, propertyName .. " = " .. tostring(propertyValue), {
1494
+ propertyName = propertyName,
1495
+ newValue = propertyValue
1496
+ })
1507
1497
  return {
1508
1498
  success = true,
1509
1499
  instancePath = instancePath,
@@ -1554,11 +1544,17 @@ handlers.createObject = function(requestData)
1554
1544
  end)
1555
1545
 
1556
1546
  if success and newInstance then
1547
+ local newPath = getInstancePath(newInstance)
1548
+ -- Log for undo tracking
1549
+ logAction("create_object", newPath, "Created " .. className .. " '" .. newInstance.Name .. "'", {
1550
+ className = className,
1551
+ parent = parentPath
1552
+ })
1557
1553
  return {
1558
1554
  success = true,
1559
1555
  className = className,
1560
1556
  parent = parentPath,
1561
- instancePath = getInstancePath(newInstance),
1557
+ instancePath = newPath,
1562
1558
  name = newInstance.Name,
1563
1559
  message = "Object created successfully",
1564
1560
  }
@@ -1587,15 +1583,21 @@ handlers.deleteObject = function(requestData)
1587
1583
  return { error = "Cannot delete the game instance" }
1588
1584
  end
1589
1585
 
1586
+ local name = instance.Name
1587
+ local className = instance.ClassName
1588
+
1590
1589
  local success, result = pcall(function()
1591
- local name = instance.Name
1592
- local className = instance.ClassName
1593
1590
  instance:Destroy()
1594
1591
  ChangeHistoryService:SetWaypoint("Delete " .. className .. " (" .. name .. ")")
1595
1592
  return true
1596
1593
  end)
1597
1594
 
1598
1595
  if success then
1596
+ -- Log for undo tracking
1597
+ logAction("delete_object", instancePath, "Deleted " .. className .. " '" .. name .. "'", {
1598
+ className = className,
1599
+ name = name
1600
+ })
1599
1601
  return {
1600
1602
  success = true,
1601
1603
  instancePath = instancePath,
@@ -2498,6 +2500,10 @@ handlers.setScriptSource = function(requestData)
2498
2500
  end)
2499
2501
 
2500
2502
  if directSuccess then
2503
+ -- Log for undo tracking
2504
+ logAction("set_script_source", instancePath, "Script source replaced (" .. directResult.newSourceLength .. " chars)", {
2505
+ method = "direct"
2506
+ })
2501
2507
  return directResult
2502
2508
  end
2503
2509
 
@@ -2533,6 +2539,10 @@ handlers.setScriptSource = function(requestData)
2533
2539
  end)
2534
2540
 
2535
2541
  if replaceSuccess then
2542
+ -- Log for undo tracking
2543
+ logAction("set_script_source", replaceResult.instancePath, "Script replaced entirely", {
2544
+ method = "replace"
2545
+ })
2536
2546
  return replaceResult
2537
2547
  else
2538
2548
  return {
@@ -2775,6 +2785,509 @@ handlers.deleteScriptLines = function(requestData)
2775
2785
  end
2776
2786
  end
2777
2787
 
2788
+ -- ============================================
2789
+ -- CLAUDE CODE-STYLE SCRIPT EDITING TOOLS
2790
+ -- ============================================
2791
+
2792
+ -- Helper: Count occurrences of a substring
2793
+ local function countOccurrences(source, searchStr)
2794
+ local count = 0
2795
+ local start = 1
2796
+ while true do
2797
+ local pos = string.find(source, searchStr, start, true) -- plain search
2798
+ if pos then
2799
+ count = count + 1
2800
+ start = pos + 1
2801
+ else
2802
+ break
2803
+ end
2804
+ end
2805
+ return count
2806
+ end
2807
+
2808
+ -- Helper: Simple Lua syntax validation (checks for balanced blocks)
2809
+ local function validateLuaSyntax(source)
2810
+ local errors = {}
2811
+
2812
+ -- Check for balanced keywords
2813
+ local blockStarters = { "function", "if", "for", "while", "do", "repeat" }
2814
+ local openBlocks = 0
2815
+ local repeatBlocks = 0
2816
+
2817
+ -- Simple pattern-based checking for common errors
2818
+ for line in string.gmatch(source .. "\n", "([^\n]*)\n") do
2819
+ -- Count block starters
2820
+ if string.match(line, "^%s*function%s") or string.match(line, "=%s*function%s*%(") or string.match(line, "^%s*local%s+function%s") then
2821
+ openBlocks = openBlocks + 1
2822
+ end
2823
+ if string.match(line, "^%s*if%s") and not string.match(line, "%sthen%s+.+%send%s*$") then
2824
+ openBlocks = openBlocks + 1
2825
+ end
2826
+ if string.match(line, "^%s*for%s") and string.match(line, "%sdo%s*$") then
2827
+ openBlocks = openBlocks + 1
2828
+ end
2829
+ if string.match(line, "^%s*while%s") and string.match(line, "%sdo%s*$") then
2830
+ openBlocks = openBlocks + 1
2831
+ end
2832
+ if string.match(line, "^%s*repeat%s*$") then
2833
+ repeatBlocks = repeatBlocks + 1
2834
+ end
2835
+
2836
+ -- Count block enders
2837
+ if string.match(line, "^%s*end%s*$") or string.match(line, "^%s*end[%)%],;%s]") or string.match(line, "%send%s*$") then
2838
+ if not string.match(line, "^%s*%-%-") then -- Not a comment
2839
+ openBlocks = openBlocks - 1
2840
+ end
2841
+ end
2842
+ if string.match(line, "^%s*until%s") then
2843
+ repeatBlocks = repeatBlocks - 1
2844
+ end
2845
+ end
2846
+
2847
+ if openBlocks > 0 then
2848
+ table.insert(errors, "Missing " .. openBlocks .. " 'end' statement(s) - check your function/if/for/while blocks")
2849
+ elseif openBlocks < 0 then
2850
+ table.insert(errors, "Extra " .. math.abs(openBlocks) .. " 'end' statement(s) - you have more 'end' keywords than block openers")
2851
+ end
2852
+
2853
+ if repeatBlocks > 0 then
2854
+ table.insert(errors, "Missing " .. repeatBlocks .. " 'until' statement(s) for repeat blocks")
2855
+ elseif repeatBlocks < 0 then
2856
+ table.insert(errors, "Extra " .. math.abs(repeatBlocks) .. " 'until' statement(s)")
2857
+ end
2858
+
2859
+ -- Check for unclosed strings (simple check)
2860
+ local inString = false
2861
+ local stringChar = nil
2862
+ local lineNum = 0
2863
+ for line in string.gmatch(source .. "\n", "([^\n]*)\n") do
2864
+ lineNum = lineNum + 1
2865
+ -- Skip long strings and comments for now
2866
+ if not string.match(line, "%[%[") and not string.match(line, "%-%-") then
2867
+ for i = 1, #line do
2868
+ local char = string.sub(line, i, i)
2869
+ if not inString then
2870
+ if char == '"' or char == "'" then
2871
+ inString = true
2872
+ stringChar = char
2873
+ end
2874
+ else
2875
+ if char == stringChar and string.sub(line, i-1, i-1) ~= "\\" then
2876
+ inString = false
2877
+ stringChar = nil
2878
+ end
2879
+ end
2880
+ end
2881
+ end
2882
+ end
2883
+
2884
+ return #errors == 0, errors
2885
+ end
2886
+
2887
+ -- edit_script: String-based editing like Claude Code's Edit tool
2888
+ handlers.editScript = function(requestData)
2889
+ local instancePath = requestData.instancePath
2890
+ local oldString = requestData.oldString
2891
+ local newString = requestData.newString
2892
+ local replaceAll = requestData.replaceAll or false
2893
+ local validateAfter = requestData.validateAfter
2894
+ if validateAfter == nil then validateAfter = true end
2895
+
2896
+ if not instancePath then
2897
+ return { error = "Instance path is required" }
2898
+ end
2899
+ if not oldString or oldString == "" then
2900
+ return { error = "old_string is required and cannot be empty" }
2901
+ end
2902
+ if newString == nil then
2903
+ return { error = "new_string is required" }
2904
+ end
2905
+ if oldString == newString then
2906
+ return { error = "old_string and new_string must be different" }
2907
+ end
2908
+
2909
+ local instance = getInstanceByPath(instancePath)
2910
+ if not instance then
2911
+ return { error = "Instance not found: " .. instancePath }
2912
+ end
2913
+
2914
+ if not instance:IsA("LuaSourceContainer") then
2915
+ return { error = "Instance is not a script: " .. instance.ClassName }
2916
+ end
2917
+
2918
+ local success, result = pcall(function()
2919
+ local source = instance.Source
2920
+
2921
+ -- Normalize escape sequences that may have been double-escaped
2922
+ local searchStr = oldString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
2923
+ local replaceStr = newString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
2924
+
2925
+ -- Count occurrences
2926
+ local occurrences = countOccurrences(source, searchStr)
2927
+
2928
+ if occurrences == 0 then
2929
+ return {
2930
+ success = false,
2931
+ error = "old_string not found in script. Make sure the text matches exactly, including whitespace and indentation.",
2932
+ hint = "Tip: Use get_script_source first to see the exact content, then copy the text precisely."
2933
+ }
2934
+ end
2935
+
2936
+ if occurrences > 1 and not replaceAll then
2937
+ return {
2938
+ success = false,
2939
+ error = "old_string appears " .. occurrences .. " times in the script. Either provide more context to make it unique, or set replace_all=true to replace all occurrences.",
2940
+ occurrences = occurrences
2941
+ }
2942
+ end
2943
+
2944
+ -- Perform the replacement
2945
+ local newSource
2946
+ if replaceAll then
2947
+ newSource = string.gsub(source, searchStr:gsub("([^%w])", "%%%1"), replaceStr) -- Escape pattern chars
2948
+ else
2949
+ -- Replace just the first occurrence
2950
+ local pos = string.find(source, searchStr, 1, true)
2951
+ newSource = string.sub(source, 1, pos - 1) .. replaceStr .. string.sub(source, pos + #searchStr)
2952
+ end
2953
+
2954
+ -- Validate syntax if requested
2955
+ if validateAfter then
2956
+ local isValid, syntaxErrors = validateLuaSyntax(newSource)
2957
+ if not isValid then
2958
+ return {
2959
+ success = false,
2960
+ error = "Edit would create invalid Lua syntax",
2961
+ syntaxErrors = syntaxErrors,
2962
+ hint = "The edit was NOT applied. Fix the syntax issues in new_string and try again."
2963
+ }
2964
+ end
2965
+ end
2966
+
2967
+ -- Apply the change
2968
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
2969
+ return newSource
2970
+ end)
2971
+
2972
+ ChangeHistoryService:SetWaypoint("Edit script: " .. instance.Name)
2973
+
2974
+ -- Prepare summary for logging
2975
+ local shortOld = #searchStr > 30 and (string.sub(searchStr, 1, 30) .. "...") or searchStr
2976
+ local shortNew = #replaceStr > 30 and (string.sub(replaceStr, 1, 30) .. "...") or replaceStr
2977
+
2978
+ return {
2979
+ success = true,
2980
+ instancePath = instancePath,
2981
+ replacements = replaceAll and occurrences or 1,
2982
+ oldLength = #source,
2983
+ newLength = #newSource,
2984
+ validated = validateAfter,
2985
+ message = replaceAll
2986
+ and ("Replaced " .. occurrences .. " occurrence(s) successfully")
2987
+ or "Edit applied successfully",
2988
+ _logSummary = shortOld:gsub("\n", "\\n") .. " → " .. shortNew:gsub("\n", "\\n")
2989
+ }
2990
+ end)
2991
+
2992
+ if success then
2993
+ if result.success then
2994
+ -- Log for undo tracking
2995
+ logAction("edit_script", instancePath, result._logSummary or "Script edited", {
2996
+ replacements = result.replacements
2997
+ })
2998
+ result._logSummary = nil -- Remove internal field
2999
+ end
3000
+ return result
3001
+ else
3002
+ return { error = "Failed to edit script: " .. tostring(result) }
3003
+ end
3004
+ end
3005
+
3006
+ -- search_script: Search for patterns within a script (like grep)
3007
+ handlers.searchScript = function(requestData)
3008
+ local instancePath = requestData.instancePath
3009
+ local pattern = requestData.pattern
3010
+ local useRegex = requestData.useRegex or false
3011
+ local contextLines = requestData.contextLines or 0
3012
+
3013
+ if not instancePath or not pattern then
3014
+ return { error = "Instance path and pattern are required" }
3015
+ end
3016
+
3017
+ local instance = getInstanceByPath(instancePath)
3018
+ if not instance then
3019
+ return { error = "Instance not found: " .. instancePath }
3020
+ end
3021
+
3022
+ if not instance:IsA("LuaSourceContainer") then
3023
+ return { error = "Instance is not a script: " .. instance.ClassName }
3024
+ end
3025
+
3026
+ local success, result = pcall(function()
3027
+ local source = instance.Source
3028
+ local lines, _ = splitLines(source)
3029
+ local matches = {}
3030
+
3031
+ for lineNum, line in ipairs(lines) do
3032
+ local found = false
3033
+ if useRegex then
3034
+ found = string.match(line, pattern) ~= nil
3035
+ else
3036
+ found = string.find(line, pattern, 1, true) ~= nil
3037
+ end
3038
+
3039
+ if found then
3040
+ local match = {
3041
+ lineNumber = lineNum,
3042
+ content = line,
3043
+ context = {}
3044
+ }
3045
+
3046
+ -- Add context lines
3047
+ if contextLines > 0 then
3048
+ for i = math.max(1, lineNum - contextLines), lineNum - 1 do
3049
+ table.insert(match.context, { lineNumber = i, content = lines[i], type = "before" })
3050
+ end
3051
+ for i = lineNum + 1, math.min(#lines, lineNum + contextLines) do
3052
+ table.insert(match.context, { lineNumber = i, content = lines[i], type = "after" })
3053
+ end
3054
+ end
3055
+
3056
+ table.insert(matches, match)
3057
+ end
3058
+ end
3059
+
3060
+ return {
3061
+ instancePath = instancePath,
3062
+ pattern = pattern,
3063
+ useRegex = useRegex,
3064
+ matches = matches,
3065
+ matchCount = #matches,
3066
+ totalLines = #lines
3067
+ }
3068
+ end)
3069
+
3070
+ if success then
3071
+ return result
3072
+ else
3073
+ return { error = "Failed to search script: " .. tostring(result) }
3074
+ end
3075
+ end
3076
+
3077
+ -- get_script_function: Extract a specific function by name
3078
+ handlers.getScriptFunction = function(requestData)
3079
+ local instancePath = requestData.instancePath
3080
+ local functionName = requestData.functionName
3081
+
3082
+ if not instancePath or not functionName then
3083
+ return { error = "Instance path and function name are required" }
3084
+ end
3085
+
3086
+ local instance = getInstanceByPath(instancePath)
3087
+ if not instance then
3088
+ return { error = "Instance not found: " .. instancePath }
3089
+ end
3090
+
3091
+ if not instance:IsA("LuaSourceContainer") then
3092
+ return { error = "Instance is not a script: " .. instance.ClassName }
3093
+ end
3094
+
3095
+ local success, result = pcall(function()
3096
+ local source = instance.Source
3097
+ local lines, _ = splitLines(source)
3098
+
3099
+ local functionStart = nil
3100
+ local functionEnd = nil
3101
+ local depth = 0
3102
+ local inFunction = false
3103
+
3104
+ -- Patterns to match function definitions
3105
+ local patterns = {
3106
+ "^%s*function%s+" .. functionName .. "%s*%(", -- function name(
3107
+ "^%s*local%s+function%s+" .. functionName .. "%s*%(", -- local function name(
3108
+ functionName .. "%s*=%s*function%s*%(", -- name = function(
3109
+ "local%s+" .. functionName .. "%s*=%s*function%s*%(" -- local name = function(
3110
+ }
3111
+
3112
+ for lineNum, line in ipairs(lines) do
3113
+ -- Check if this line starts the function we're looking for
3114
+ if not inFunction then
3115
+ for _, pat in ipairs(patterns) do
3116
+ if string.match(line, pat) then
3117
+ functionStart = lineNum
3118
+ inFunction = true
3119
+ depth = 1
3120
+ -- Check if function also ends on this line (one-liner)
3121
+ if string.match(line, "%send%s*$") or string.match(line, "%send%s*[%)%];,]") then
3122
+ functionEnd = lineNum
3123
+ inFunction = false
3124
+ end
3125
+ break
3126
+ end
3127
+ end
3128
+ else
3129
+ -- We're inside the function, track depth
3130
+ -- Count block openers
3131
+ if string.match(line, "^%s*function%s") or string.match(line, "=%s*function%s*%(") then
3132
+ depth = depth + 1
3133
+ end
3134
+ if string.match(line, "^%s*if%s") and string.match(line, "%sthen%s*$") then
3135
+ depth = depth + 1
3136
+ end
3137
+ if string.match(line, "^%s*for%s") and string.match(line, "%sdo%s*$") then
3138
+ depth = depth + 1
3139
+ end
3140
+ if string.match(line, "^%s*while%s") and string.match(line, "%sdo%s*$") then
3141
+ depth = depth + 1
3142
+ end
3143
+ if string.match(line, "^%s*do%s*$") then
3144
+ depth = depth + 1
3145
+ end
3146
+
3147
+ -- Count block closers
3148
+ if string.match(line, "^%s*end%s*$") or string.match(line, "^%s*end[%)%],;%s]") or string.match(line, "%send%s*$") then
3149
+ depth = depth - 1
3150
+ if depth == 0 then
3151
+ functionEnd = lineNum
3152
+ break
3153
+ end
3154
+ end
3155
+ end
3156
+ end
3157
+
3158
+ if not functionStart then
3159
+ return {
3160
+ success = false,
3161
+ error = "Function '" .. functionName .. "' not found in script",
3162
+ hint = "Check the function name spelling. Use search_script to find available functions."
3163
+ }
3164
+ end
3165
+
3166
+ if not functionEnd then
3167
+ return {
3168
+ success = false,
3169
+ error = "Could not find the end of function '" .. functionName .. "' - the function may be malformed",
3170
+ startLine = functionStart
3171
+ }
3172
+ end
3173
+
3174
+ -- Extract the function source
3175
+ local functionLines = {}
3176
+ for i = functionStart, functionEnd do
3177
+ table.insert(functionLines, lines[i])
3178
+ end
3179
+
3180
+ local functionSource = table.concat(functionLines, "\n")
3181
+
3182
+ -- Create numbered version
3183
+ local numberedLines = {}
3184
+ for i = functionStart, functionEnd do
3185
+ table.insert(numberedLines, string.format("%4d: %s", i, lines[i]))
3186
+ end
3187
+ local numberedSource = table.concat(numberedLines, "\n")
3188
+
3189
+ return {
3190
+ success = true,
3191
+ instancePath = instancePath,
3192
+ functionName = functionName,
3193
+ startLine = functionStart,
3194
+ endLine = functionEnd,
3195
+ lineCount = functionEnd - functionStart + 1,
3196
+ source = functionSource,
3197
+ numberedSource = numberedSource
3198
+ }
3199
+ end)
3200
+
3201
+ if success then
3202
+ return result
3203
+ else
3204
+ return { error = "Failed to get function: " .. tostring(result) }
3205
+ end
3206
+ end
3207
+
3208
+ -- find_and_replace_in_scripts: Batch replace across multiple scripts
3209
+ handlers.findAndReplaceInScripts = function(requestData)
3210
+ local paths = requestData.paths
3211
+ local oldString = requestData.oldString
3212
+ local newString = requestData.newString
3213
+ local validateAfter = requestData.validateAfter
3214
+ if validateAfter == nil then validateAfter = true end
3215
+
3216
+ if not paths or #paths == 0 then
3217
+ return { error = "Paths array is required" }
3218
+ end
3219
+ if not oldString or oldString == "" then
3220
+ return { error = "old_string is required" }
3221
+ end
3222
+ if newString == nil then
3223
+ return { error = "new_string is required" }
3224
+ end
3225
+
3226
+ local results = {}
3227
+ local successCount = 0
3228
+ local failCount = 0
3229
+ local skippedCount = 0
3230
+
3231
+ for _, path in ipairs(paths) do
3232
+ local instance = getInstanceByPath(path)
3233
+ if not instance then
3234
+ table.insert(results, { path = path, success = false, error = "Instance not found" })
3235
+ failCount = failCount + 1
3236
+ elseif not instance:IsA("LuaSourceContainer") then
3237
+ table.insert(results, { path = path, success = false, error = "Not a script" })
3238
+ failCount = failCount + 1
3239
+ else
3240
+ local source = instance.Source
3241
+ local searchStr = oldString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
3242
+ local replaceStr = newString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
3243
+
3244
+ local occurrences = countOccurrences(source, searchStr)
3245
+
3246
+ if occurrences == 0 then
3247
+ table.insert(results, { path = path, success = true, skipped = true, reason = "Pattern not found" })
3248
+ skippedCount = skippedCount + 1
3249
+ else
3250
+ local newSource = string.gsub(source, searchStr:gsub("([^%w])", "%%%1"), replaceStr)
3251
+
3252
+ if validateAfter then
3253
+ local isValid, syntaxErrors = validateLuaSyntax(newSource)
3254
+ if not isValid then
3255
+ table.insert(results, { path = path, success = false, error = "Would create invalid syntax", syntaxErrors = syntaxErrors })
3256
+ failCount = failCount + 1
3257
+ else
3258
+ pcall(function()
3259
+ ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end)
3260
+ end)
3261
+ table.insert(results, { path = path, success = true, replacements = occurrences })
3262
+ successCount = successCount + 1
3263
+ end
3264
+ else
3265
+ pcall(function()
3266
+ ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end)
3267
+ end)
3268
+ table.insert(results, { path = path, success = true, replacements = occurrences })
3269
+ successCount = successCount + 1
3270
+ end
3271
+ end
3272
+ end
3273
+ end
3274
+
3275
+ if successCount > 0 then
3276
+ ChangeHistoryService:SetWaypoint("Batch replace in " .. successCount .. " scripts")
3277
+ end
3278
+
3279
+ return {
3280
+ success = failCount == 0,
3281
+ results = results,
3282
+ summary = {
3283
+ total = #paths,
3284
+ successful = successCount,
3285
+ failed = failCount,
3286
+ skipped = skippedCount
3287
+ }
3288
+ }
3289
+ end
3290
+
2778
3291
  -- Attribute Tools: Get a single attribute
2779
3292
  handlers.getAttribute = function(requestData)
2780
3293
  local instancePath = requestData.instancePath
@@ -3451,15 +3964,31 @@ handlers.validateScript = function(requestData)
3451
3964
  end
3452
3965
 
3453
3966
  -- ============================================
3454
- -- UNDO/REDO HANDLERS
3967
+ -- UNDO/REDO HANDLERS (Enhanced with action tracking)
3455
3968
  -- ============================================
3456
3969
 
3457
3970
  handlers.undo = function(requestData)
3458
3971
  local success, result = pcall(function()
3972
+ -- Pop the last action from history
3973
+ local lastAction = nil
3974
+ if #actionHistory > 0 then
3975
+ lastAction = table.remove(actionHistory)
3976
+ -- Move it to redo history
3977
+ table.insert(redoHistory, lastAction)
3978
+ end
3979
+
3459
3980
  ChangeHistoryService:Undo()
3981
+
3460
3982
  return {
3461
3983
  success = true,
3462
- message = "Undo executed"
3984
+ undone = lastAction and (lastAction.action .. " " .. lastAction.target .. " (" .. lastAction.summary .. ")") or "Unknown action (possibly manual change)",
3985
+ action = lastAction and lastAction.action or nil,
3986
+ target = lastAction and lastAction.target or nil,
3987
+ summary = lastAction and lastAction.summary or nil,
3988
+ details = lastAction and lastAction.details or nil,
3989
+ remaining_undos = #actionHistory,
3990
+ available_redos = #redoHistory,
3991
+ message = lastAction and ("Undone: " .. lastAction.summary) or "Undo executed"
3463
3992
  }
3464
3993
  end)
3465
3994
 
@@ -3468,17 +3997,35 @@ handlers.undo = function(requestData)
3468
3997
  else
3469
3998
  return {
3470
3999
  success = false,
3471
- error = "Failed to undo: " .. tostring(result)
4000
+ error = "Failed to undo: " .. tostring(result),
4001
+ remaining_undos = #actionHistory,
4002
+ available_redos = #redoHistory
3472
4003
  }
3473
4004
  end
3474
4005
  end
3475
4006
 
3476
4007
  handlers.redo = function(requestData)
3477
4008
  local success, result = pcall(function()
4009
+ -- Pop the last undone action from redo history
4010
+ local redoneAction = nil
4011
+ if #redoHistory > 0 then
4012
+ redoneAction = table.remove(redoHistory)
4013
+ -- Move it back to action history
4014
+ table.insert(actionHistory, redoneAction)
4015
+ end
4016
+
3478
4017
  ChangeHistoryService:Redo()
4018
+
3479
4019
  return {
3480
4020
  success = true,
3481
- message = "Redo executed"
4021
+ redone = redoneAction and (redoneAction.action .. " " .. redoneAction.target .. " (" .. redoneAction.summary .. ")") or "Unknown action",
4022
+ action = redoneAction and redoneAction.action or nil,
4023
+ target = redoneAction and redoneAction.target or nil,
4024
+ summary = redoneAction and redoneAction.summary or nil,
4025
+ details = redoneAction and redoneAction.details or nil,
4026
+ remaining_undos = #actionHistory,
4027
+ available_redos = #redoHistory,
4028
+ message = redoneAction and ("Redone: " .. redoneAction.summary) or "Redo executed"
3482
4029
  }
3483
4030
  end)
3484
4031
 
@@ -3487,11 +4034,142 @@ handlers.redo = function(requestData)
3487
4034
  else
3488
4035
  return {
3489
4036
  success = false,
3490
- error = "Failed to redo: " .. tostring(result)
4037
+ error = "Failed to redo: " .. tostring(result),
4038
+ remaining_undos = #actionHistory,
4039
+ available_redos = #redoHistory
3491
4040
  }
3492
4041
  end
3493
4042
  end
3494
4043
 
4044
+ -- ============================================
4045
+ -- INSERT ASSET HANDLER (Creator Store)
4046
+ -- ============================================
4047
+
4048
+ handlers.insertAsset = function(requestData)
4049
+ local assetId = requestData.assetId
4050
+ local folderName = requestData.folderName or "AIReferences"
4051
+ local targetParent = requestData.targetParent or "game.Workspace"
4052
+
4053
+ if not assetId then
4054
+ return { error = "Asset ID is required" }
4055
+ end
4056
+
4057
+ -- Convert to number if string
4058
+ assetId = tonumber(assetId)
4059
+ if not assetId then
4060
+ return { error = "Asset ID must be a valid number" }
4061
+ end
4062
+
4063
+ local success, result = pcall(function()
4064
+ -- Get or create the reference folder
4065
+ local parentInstance = getInstanceByPath(targetParent)
4066
+ if not parentInstance then
4067
+ parentInstance = game.Workspace
4068
+ end
4069
+
4070
+ local folder = parentInstance:FindFirstChild(folderName)
4071
+ if not folder then
4072
+ folder = Instance.new("Folder")
4073
+ folder.Name = folderName
4074
+ folder.Parent = parentInstance
4075
+ end
4076
+
4077
+ -- Use game:GetObjects to load the asset (works with any free asset in plugins)
4078
+ local assetUrl = "rbxassetid://" .. tostring(assetId)
4079
+ local objects = game:GetObjects(assetUrl)
4080
+
4081
+ if not objects or #objects == 0 then
4082
+ return {
4083
+ success = false,
4084
+ error = "Failed to load asset - it may be private, unavailable, or not a model asset"
4085
+ }
4086
+ end
4087
+
4088
+ -- Parent all loaded objects to the folder
4089
+ local insertedPaths = {}
4090
+ for i, obj in ipairs(objects) do
4091
+ -- Name the object with the asset ID for easy identification
4092
+ if #objects == 1 then
4093
+ obj.Name = "Asset_" .. tostring(assetId)
4094
+ else
4095
+ obj.Name = "Asset_" .. tostring(assetId) .. "_" .. tostring(i)
4096
+ end
4097
+ obj.Parent = folder
4098
+ table.insert(insertedPaths, getInstancePath(obj))
4099
+ end
4100
+
4101
+ return {
4102
+ success = true,
4103
+ assetId = assetId,
4104
+ folderPath = getInstancePath(folder),
4105
+ insertedObjects = insertedPaths,
4106
+ objectCount = #objects,
4107
+ message = "Successfully loaded " .. #objects .. " object(s) from asset " .. tostring(assetId)
4108
+ }
4109
+ end)
4110
+
4111
+ if success then
4112
+ return result
4113
+ else
4114
+ return {
4115
+ success = false,
4116
+ error = "Failed to insert asset: " .. tostring(result),
4117
+ assetId = assetId
4118
+ }
4119
+ end
4120
+ end
4121
+
4122
+ -- Now that all handlers are defined, populate the endpoint mapping table
4123
+ endpointHandlers = {
4124
+ ["/api/file-tree"] = handlers.getFileTree,
4125
+ ["/api/search-files"] = handlers.searchFiles,
4126
+ ["/api/place-info"] = handlers.getPlaceInfo,
4127
+ ["/api/services"] = handlers.getServices,
4128
+ ["/api/search-objects"] = handlers.searchObjects,
4129
+ ["/api/instance-properties"] = handlers.getInstanceProperties,
4130
+ ["/api/instance-children"] = handlers.getInstanceChildren,
4131
+ ["/api/search-by-property"] = handlers.searchByProperty,
4132
+ ["/api/class-info"] = handlers.getClassInfo,
4133
+ ["/api/project-structure"] = handlers.getProjectStructure,
4134
+ ["/api/set-property"] = handlers.setProperty,
4135
+ ["/api/mass-set-property"] = handlers.massSetProperty,
4136
+ ["/api/mass-get-property"] = handlers.massGetProperty,
4137
+ ["/api/create-object"] = handlers.createObject,
4138
+ ["/api/mass-create-objects"] = handlers.massCreateObjects,
4139
+ ["/api/mass-create-objects-with-properties"] = handlers.massCreateObjectsWithProperties,
4140
+ ["/api/delete-object"] = handlers.deleteObject,
4141
+ ["/api/smart-duplicate"] = handlers.smartDuplicate,
4142
+ ["/api/mass-duplicate"] = handlers.massDuplicate,
4143
+ ["/api/set-calculated-property"] = handlers.setCalculatedProperty,
4144
+ ["/api/set-relative-property"] = handlers.setRelativeProperty,
4145
+ ["/api/get-script-source"] = handlers.getScriptSource,
4146
+ ["/api/set-script-source"] = handlers.setScriptSource,
4147
+ ["/api/edit-script-lines"] = handlers.editScriptLines,
4148
+ ["/api/insert-script-lines"] = handlers.insertScriptLines,
4149
+ ["/api/delete-script-lines"] = handlers.deleteScriptLines,
4150
+ -- Claude Code-style script editing tools
4151
+ ["/api/edit-script"] = handlers.editScript,
4152
+ ["/api/search-script"] = handlers.searchScript,
4153
+ ["/api/get-script-function"] = handlers.getScriptFunction,
4154
+ ["/api/find-and-replace-in-scripts"] = handlers.findAndReplaceInScripts,
4155
+ ["/api/get-attribute"] = handlers.getAttribute,
4156
+ ["/api/set-attribute"] = handlers.setAttribute,
4157
+ ["/api/get-attributes"] = handlers.getAttributes,
4158
+ ["/api/delete-attribute"] = handlers.deleteAttribute,
4159
+ ["/api/get-tags"] = handlers.getTags,
4160
+ ["/api/add-tag"] = handlers.addTag,
4161
+ ["/api/remove-tag"] = handlers.removeTag,
4162
+ ["/api/get-tagged"] = handlers.getTagged,
4163
+ ["/api/get-selection"] = handlers.getSelection,
4164
+ ["/api/get-output"] = handlers.getOutput,
4165
+ ["/api/clone-instance"] = handlers.cloneInstance,
4166
+ ["/api/move-instance"] = handlers.moveInstance,
4167
+ ["/api/validate-script"] = handlers.validateScript,
4168
+ ["/api/insert-asset"] = handlers.insertAsset,
4169
+ ["/api/undo"] = handlers.undo,
4170
+ ["/api/redo"] = handlers.redo,
4171
+ }
4172
+
3495
4173
  local function updateUIState()
3496
4174
  if pluginState.isActive then
3497
4175
  statusLabel.Text = "Connecting..."