rbxstudio-mcp 1.11.0 → 1.12.1

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,62 @@ 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 serialize values nicely for logging
26
+ local function serializeValue(value)
27
+ local t = typeof(value)
28
+ if t == "Vector3" then
29
+ return string.format("Vector3(%.2f, %.2f, %.2f)", value.X, value.Y, value.Z)
30
+ elseif t == "Vector2" then
31
+ return string.format("Vector2(%.2f, %.2f)", value.X, value.Y)
32
+ elseif t == "Color3" then
33
+ return string.format("Color3(%.2f, %.2f, %.2f)", value.R, value.G, value.B)
34
+ elseif t == "BrickColor" then
35
+ return "BrickColor(" .. value.Name .. ")"
36
+ elseif t == "UDim2" then
37
+ return string.format("UDim2(%.2f, %d, %.2f, %d)", value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset)
38
+ elseif t == "UDim" then
39
+ return string.format("UDim(%.2f, %d)", value.Scale, value.Offset)
40
+ elseif t == "CFrame" then
41
+ return string.format("CFrame(%.1f, %.1f, %.1f)", value.X, value.Y, value.Z)
42
+ elseif t == "table" then
43
+ if #value > 0 then
44
+ local parts = {}
45
+ for i, v in ipairs(value) do
46
+ if i > 3 then table.insert(parts, "...") break end
47
+ table.insert(parts, serializeValue(v))
48
+ end
49
+ return "{" .. table.concat(parts, ", ") .. "}"
50
+ end
51
+ return "{...}"
52
+ else
53
+ return tostring(value)
54
+ end
55
+ end
56
+
57
+ -- Helper to log an action for undo tracking
58
+ local function logAction(actionType, target, summary, details)
59
+ -- Clear redo history when a new action is performed
60
+ redoHistory = {}
61
+
62
+ table.insert(actionHistory, {
63
+ action = actionType,
64
+ target = target,
65
+ summary = summary,
66
+ details = details or {},
67
+ timestamp = os.time()
68
+ })
69
+
70
+ -- Keep history from growing too large
71
+ if #actionHistory > MAX_ACTION_HISTORY then
72
+ table.remove(actionHistory, 1)
73
+ end
74
+ end
75
+
20
76
  -- Connect to LogService to capture output
21
77
  LogService.MessageOut:Connect(function(message, messageType)
22
78
  table.insert(outputBuffer, {
@@ -55,7 +111,7 @@ local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
55
111
  "MCPServerInterface",
56
112
  DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
57
113
  )
58
- screenGui.Title = "MCP Server v1.11.0"
114
+ screenGui.Title = "MCP Server v1.12.1"
59
115
 
60
116
  local mainFrame = Instance.new("Frame")
61
117
  mainFrame.Size = UDim2.new(1, 0, 1, 0)
@@ -108,7 +164,7 @@ local versionLabel = Instance.new("TextLabel")
108
164
  versionLabel.Size = UDim2.new(1, 0, 0, 16)
109
165
  versionLabel.Position = UDim2.new(0, 0, 0, 32)
110
166
  versionLabel.BackgroundTransparency = 1
111
- versionLabel.Text = "AI Integration • v1.11.0"
167
+ versionLabel.Text = "AI Integration • v1.12.1"
112
168
  versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
113
169
  versionLabel.TextScaled = false
114
170
  versionLabel.TextSize = 12
@@ -1465,6 +1521,11 @@ handlers.setProperty = function(requestData)
1465
1521
  end)
1466
1522
 
1467
1523
  if success and result ~= false then
1524
+ -- Log for undo tracking
1525
+ logAction("set_property", instancePath, propertyName .. " = " .. serializeValue(propertyValue), {
1526
+ propertyName = propertyName,
1527
+ newValue = propertyValue
1528
+ })
1468
1529
  return {
1469
1530
  success = true,
1470
1531
  instancePath = instancePath,
@@ -1515,11 +1576,17 @@ handlers.createObject = function(requestData)
1515
1576
  end)
1516
1577
 
1517
1578
  if success and newInstance then
1579
+ local newPath = getInstancePath(newInstance)
1580
+ -- Log for undo tracking
1581
+ logAction("create_object", newPath, "Created " .. className .. " '" .. newInstance.Name .. "'", {
1582
+ className = className,
1583
+ parent = parentPath
1584
+ })
1518
1585
  return {
1519
1586
  success = true,
1520
1587
  className = className,
1521
1588
  parent = parentPath,
1522
- instancePath = getInstancePath(newInstance),
1589
+ instancePath = newPath,
1523
1590
  name = newInstance.Name,
1524
1591
  message = "Object created successfully",
1525
1592
  }
@@ -1548,15 +1615,21 @@ handlers.deleteObject = function(requestData)
1548
1615
  return { error = "Cannot delete the game instance" }
1549
1616
  end
1550
1617
 
1618
+ local name = instance.Name
1619
+ local className = instance.ClassName
1620
+
1551
1621
  local success, result = pcall(function()
1552
- local name = instance.Name
1553
- local className = instance.ClassName
1554
1622
  instance:Destroy()
1555
1623
  ChangeHistoryService:SetWaypoint("Delete " .. className .. " (" .. name .. ")")
1556
1624
  return true
1557
1625
  end)
1558
1626
 
1559
1627
  if success then
1628
+ -- Log for undo tracking
1629
+ logAction("delete_object", instancePath, "Deleted " .. className .. " '" .. name .. "'", {
1630
+ className = className,
1631
+ name = name
1632
+ })
1560
1633
  return {
1561
1634
  success = true,
1562
1635
  instancePath = instancePath,
@@ -2459,6 +2532,10 @@ handlers.setScriptSource = function(requestData)
2459
2532
  end)
2460
2533
 
2461
2534
  if directSuccess then
2535
+ -- Log for undo tracking
2536
+ logAction("set_script_source", instancePath, "Script source replaced (" .. directResult.newSourceLength .. " chars)", {
2537
+ method = "direct"
2538
+ })
2462
2539
  return directResult
2463
2540
  end
2464
2541
 
@@ -2494,6 +2571,10 @@ handlers.setScriptSource = function(requestData)
2494
2571
  end)
2495
2572
 
2496
2573
  if replaceSuccess then
2574
+ -- Log for undo tracking
2575
+ logAction("set_script_source", replaceResult.instancePath, "Script replaced entirely", {
2576
+ method = "replace"
2577
+ })
2497
2578
  return replaceResult
2498
2579
  else
2499
2580
  return {
@@ -2736,6 +2817,509 @@ handlers.deleteScriptLines = function(requestData)
2736
2817
  end
2737
2818
  end
2738
2819
 
2820
+ -- ============================================
2821
+ -- CLAUDE CODE-STYLE SCRIPT EDITING TOOLS
2822
+ -- ============================================
2823
+
2824
+ -- Helper: Count occurrences of a substring
2825
+ local function countOccurrences(source, searchStr)
2826
+ local count = 0
2827
+ local start = 1
2828
+ while true do
2829
+ local pos = string.find(source, searchStr, start, true) -- plain search
2830
+ if pos then
2831
+ count = count + 1
2832
+ start = pos + 1
2833
+ else
2834
+ break
2835
+ end
2836
+ end
2837
+ return count
2838
+ end
2839
+
2840
+ -- Helper: Simple Lua syntax validation (checks for balanced blocks)
2841
+ local function validateLuaSyntax(source)
2842
+ local errors = {}
2843
+
2844
+ -- Check for balanced keywords
2845
+ local blockStarters = { "function", "if", "for", "while", "do", "repeat" }
2846
+ local openBlocks = 0
2847
+ local repeatBlocks = 0
2848
+
2849
+ -- Simple pattern-based checking for common errors
2850
+ for line in string.gmatch(source .. "\n", "([^\n]*)\n") do
2851
+ -- Count block starters
2852
+ if string.match(line, "^%s*function%s") or string.match(line, "=%s*function%s*%(") or string.match(line, "^%s*local%s+function%s") then
2853
+ openBlocks = openBlocks + 1
2854
+ end
2855
+ if string.match(line, "^%s*if%s") and not string.match(line, "%sthen%s+.+%send%s*$") then
2856
+ openBlocks = openBlocks + 1
2857
+ end
2858
+ if string.match(line, "^%s*for%s") and string.match(line, "%sdo%s*$") then
2859
+ openBlocks = openBlocks + 1
2860
+ end
2861
+ if string.match(line, "^%s*while%s") and string.match(line, "%sdo%s*$") then
2862
+ openBlocks = openBlocks + 1
2863
+ end
2864
+ if string.match(line, "^%s*repeat%s*$") then
2865
+ repeatBlocks = repeatBlocks + 1
2866
+ end
2867
+
2868
+ -- Count block enders
2869
+ if string.match(line, "^%s*end%s*$") or string.match(line, "^%s*end[%)%],;%s]") or string.match(line, "%send%s*$") then
2870
+ if not string.match(line, "^%s*%-%-") then -- Not a comment
2871
+ openBlocks = openBlocks - 1
2872
+ end
2873
+ end
2874
+ if string.match(line, "^%s*until%s") then
2875
+ repeatBlocks = repeatBlocks - 1
2876
+ end
2877
+ end
2878
+
2879
+ if openBlocks > 0 then
2880
+ table.insert(errors, "Missing " .. openBlocks .. " 'end' statement(s) - check your function/if/for/while blocks")
2881
+ elseif openBlocks < 0 then
2882
+ table.insert(errors, "Extra " .. math.abs(openBlocks) .. " 'end' statement(s) - you have more 'end' keywords than block openers")
2883
+ end
2884
+
2885
+ if repeatBlocks > 0 then
2886
+ table.insert(errors, "Missing " .. repeatBlocks .. " 'until' statement(s) for repeat blocks")
2887
+ elseif repeatBlocks < 0 then
2888
+ table.insert(errors, "Extra " .. math.abs(repeatBlocks) .. " 'until' statement(s)")
2889
+ end
2890
+
2891
+ -- Check for unclosed strings (simple check)
2892
+ local inString = false
2893
+ local stringChar = nil
2894
+ local lineNum = 0
2895
+ for line in string.gmatch(source .. "\n", "([^\n]*)\n") do
2896
+ lineNum = lineNum + 1
2897
+ -- Skip long strings and comments for now
2898
+ if not string.match(line, "%[%[") and not string.match(line, "%-%-") then
2899
+ for i = 1, #line do
2900
+ local char = string.sub(line, i, i)
2901
+ if not inString then
2902
+ if char == '"' or char == "'" then
2903
+ inString = true
2904
+ stringChar = char
2905
+ end
2906
+ else
2907
+ if char == stringChar and string.sub(line, i-1, i-1) ~= "\\" then
2908
+ inString = false
2909
+ stringChar = nil
2910
+ end
2911
+ end
2912
+ end
2913
+ end
2914
+ end
2915
+
2916
+ return #errors == 0, errors
2917
+ end
2918
+
2919
+ -- edit_script: String-based editing like Claude Code's Edit tool
2920
+ handlers.editScript = function(requestData)
2921
+ local instancePath = requestData.instancePath
2922
+ local oldString = requestData.oldString
2923
+ local newString = requestData.newString
2924
+ local replaceAll = requestData.replaceAll or false
2925
+ local validateAfter = requestData.validateAfter
2926
+ if validateAfter == nil then validateAfter = true end
2927
+
2928
+ if not instancePath then
2929
+ return { error = "Instance path is required" }
2930
+ end
2931
+ if not oldString or oldString == "" then
2932
+ return { error = "old_string is required and cannot be empty" }
2933
+ end
2934
+ if newString == nil then
2935
+ return { error = "new_string is required" }
2936
+ end
2937
+ if oldString == newString then
2938
+ return { error = "old_string and new_string must be different" }
2939
+ end
2940
+
2941
+ local instance = getInstanceByPath(instancePath)
2942
+ if not instance then
2943
+ return { error = "Instance not found: " .. instancePath }
2944
+ end
2945
+
2946
+ if not instance:IsA("LuaSourceContainer") then
2947
+ return { error = "Instance is not a script: " .. instance.ClassName }
2948
+ end
2949
+
2950
+ local success, result = pcall(function()
2951
+ local source = instance.Source
2952
+
2953
+ -- Normalize escape sequences that may have been double-escaped
2954
+ local searchStr = oldString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
2955
+ local replaceStr = newString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
2956
+
2957
+ -- Count occurrences
2958
+ local occurrences = countOccurrences(source, searchStr)
2959
+
2960
+ if occurrences == 0 then
2961
+ return {
2962
+ success = false,
2963
+ error = "old_string not found in script. Make sure the text matches exactly, including whitespace and indentation.",
2964
+ hint = "Tip: Use get_script_source first to see the exact content, then copy the text precisely."
2965
+ }
2966
+ end
2967
+
2968
+ if occurrences > 1 and not replaceAll then
2969
+ return {
2970
+ success = false,
2971
+ 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.",
2972
+ occurrences = occurrences
2973
+ }
2974
+ end
2975
+
2976
+ -- Perform the replacement
2977
+ local newSource
2978
+ if replaceAll then
2979
+ newSource = string.gsub(source, searchStr:gsub("([^%w])", "%%%1"), replaceStr) -- Escape pattern chars
2980
+ else
2981
+ -- Replace just the first occurrence
2982
+ local pos = string.find(source, searchStr, 1, true)
2983
+ newSource = string.sub(source, 1, pos - 1) .. replaceStr .. string.sub(source, pos + #searchStr)
2984
+ end
2985
+
2986
+ -- Validate syntax if requested
2987
+ if validateAfter then
2988
+ local isValid, syntaxErrors = validateLuaSyntax(newSource)
2989
+ if not isValid then
2990
+ return {
2991
+ success = false,
2992
+ error = "Edit would create invalid Lua syntax",
2993
+ syntaxErrors = syntaxErrors,
2994
+ hint = "The edit was NOT applied. Fix the syntax issues in new_string and try again."
2995
+ }
2996
+ end
2997
+ end
2998
+
2999
+ -- Apply the change
3000
+ ScriptEditorService:UpdateSourceAsync(instance, function(oldContent)
3001
+ return newSource
3002
+ end)
3003
+
3004
+ ChangeHistoryService:SetWaypoint("Edit script: " .. instance.Name)
3005
+
3006
+ -- Prepare summary for logging
3007
+ local shortOld = #searchStr > 30 and (string.sub(searchStr, 1, 30) .. "...") or searchStr
3008
+ local shortNew = #replaceStr > 30 and (string.sub(replaceStr, 1, 30) .. "...") or replaceStr
3009
+
3010
+ return {
3011
+ success = true,
3012
+ instancePath = instancePath,
3013
+ replacements = replaceAll and occurrences or 1,
3014
+ oldLength = #source,
3015
+ newLength = #newSource,
3016
+ validated = validateAfter,
3017
+ message = replaceAll
3018
+ and ("Replaced " .. occurrences .. " occurrence(s) successfully")
3019
+ or "Edit applied successfully",
3020
+ _logSummary = shortOld:gsub("\n", "\\n") .. " → " .. shortNew:gsub("\n", "\\n")
3021
+ }
3022
+ end)
3023
+
3024
+ if success then
3025
+ if result.success then
3026
+ -- Log for undo tracking
3027
+ logAction("edit_script", instancePath, result._logSummary or "Script edited", {
3028
+ replacements = result.replacements
3029
+ })
3030
+ result._logSummary = nil -- Remove internal field
3031
+ end
3032
+ return result
3033
+ else
3034
+ return { error = "Failed to edit script: " .. tostring(result) }
3035
+ end
3036
+ end
3037
+
3038
+ -- search_script: Search for patterns within a script (like grep)
3039
+ handlers.searchScript = function(requestData)
3040
+ local instancePath = requestData.instancePath
3041
+ local pattern = requestData.pattern
3042
+ local useRegex = requestData.useRegex or false
3043
+ local contextLines = requestData.contextLines or 0
3044
+
3045
+ if not instancePath or not pattern then
3046
+ return { error = "Instance path and pattern are required" }
3047
+ end
3048
+
3049
+ local instance = getInstanceByPath(instancePath)
3050
+ if not instance then
3051
+ return { error = "Instance not found: " .. instancePath }
3052
+ end
3053
+
3054
+ if not instance:IsA("LuaSourceContainer") then
3055
+ return { error = "Instance is not a script: " .. instance.ClassName }
3056
+ end
3057
+
3058
+ local success, result = pcall(function()
3059
+ local source = instance.Source
3060
+ local lines, _ = splitLines(source)
3061
+ local matches = {}
3062
+
3063
+ for lineNum, line in ipairs(lines) do
3064
+ local found = false
3065
+ if useRegex then
3066
+ found = string.match(line, pattern) ~= nil
3067
+ else
3068
+ found = string.find(line, pattern, 1, true) ~= nil
3069
+ end
3070
+
3071
+ if found then
3072
+ local match = {
3073
+ lineNumber = lineNum,
3074
+ content = line,
3075
+ context = {}
3076
+ }
3077
+
3078
+ -- Add context lines
3079
+ if contextLines > 0 then
3080
+ for i = math.max(1, lineNum - contextLines), lineNum - 1 do
3081
+ table.insert(match.context, { lineNumber = i, content = lines[i], type = "before" })
3082
+ end
3083
+ for i = lineNum + 1, math.min(#lines, lineNum + contextLines) do
3084
+ table.insert(match.context, { lineNumber = i, content = lines[i], type = "after" })
3085
+ end
3086
+ end
3087
+
3088
+ table.insert(matches, match)
3089
+ end
3090
+ end
3091
+
3092
+ return {
3093
+ instancePath = instancePath,
3094
+ pattern = pattern,
3095
+ useRegex = useRegex,
3096
+ matches = matches,
3097
+ matchCount = #matches,
3098
+ totalLines = #lines
3099
+ }
3100
+ end)
3101
+
3102
+ if success then
3103
+ return result
3104
+ else
3105
+ return { error = "Failed to search script: " .. tostring(result) }
3106
+ end
3107
+ end
3108
+
3109
+ -- get_script_function: Extract a specific function by name
3110
+ handlers.getScriptFunction = function(requestData)
3111
+ local instancePath = requestData.instancePath
3112
+ local functionName = requestData.functionName
3113
+
3114
+ if not instancePath or not functionName then
3115
+ return { error = "Instance path and function name are required" }
3116
+ end
3117
+
3118
+ local instance = getInstanceByPath(instancePath)
3119
+ if not instance then
3120
+ return { error = "Instance not found: " .. instancePath }
3121
+ end
3122
+
3123
+ if not instance:IsA("LuaSourceContainer") then
3124
+ return { error = "Instance is not a script: " .. instance.ClassName }
3125
+ end
3126
+
3127
+ local success, result = pcall(function()
3128
+ local source = instance.Source
3129
+ local lines, _ = splitLines(source)
3130
+
3131
+ local functionStart = nil
3132
+ local functionEnd = nil
3133
+ local depth = 0
3134
+ local inFunction = false
3135
+
3136
+ -- Patterns to match function definitions
3137
+ local patterns = {
3138
+ "^%s*function%s+" .. functionName .. "%s*%(", -- function name(
3139
+ "^%s*local%s+function%s+" .. functionName .. "%s*%(", -- local function name(
3140
+ functionName .. "%s*=%s*function%s*%(", -- name = function(
3141
+ "local%s+" .. functionName .. "%s*=%s*function%s*%(" -- local name = function(
3142
+ }
3143
+
3144
+ for lineNum, line in ipairs(lines) do
3145
+ -- Check if this line starts the function we're looking for
3146
+ if not inFunction then
3147
+ for _, pat in ipairs(patterns) do
3148
+ if string.match(line, pat) then
3149
+ functionStart = lineNum
3150
+ inFunction = true
3151
+ depth = 1
3152
+ -- Check if function also ends on this line (one-liner)
3153
+ if string.match(line, "%send%s*$") or string.match(line, "%send%s*[%)%];,]") then
3154
+ functionEnd = lineNum
3155
+ inFunction = false
3156
+ end
3157
+ break
3158
+ end
3159
+ end
3160
+ else
3161
+ -- We're inside the function, track depth
3162
+ -- Count block openers
3163
+ if string.match(line, "^%s*function%s") or string.match(line, "=%s*function%s*%(") then
3164
+ depth = depth + 1
3165
+ end
3166
+ if string.match(line, "^%s*if%s") and string.match(line, "%sthen%s*$") then
3167
+ depth = depth + 1
3168
+ end
3169
+ if string.match(line, "^%s*for%s") and string.match(line, "%sdo%s*$") then
3170
+ depth = depth + 1
3171
+ end
3172
+ if string.match(line, "^%s*while%s") and string.match(line, "%sdo%s*$") then
3173
+ depth = depth + 1
3174
+ end
3175
+ if string.match(line, "^%s*do%s*$") then
3176
+ depth = depth + 1
3177
+ end
3178
+
3179
+ -- Count block closers
3180
+ if string.match(line, "^%s*end%s*$") or string.match(line, "^%s*end[%)%],;%s]") or string.match(line, "%send%s*$") then
3181
+ depth = depth - 1
3182
+ if depth == 0 then
3183
+ functionEnd = lineNum
3184
+ break
3185
+ end
3186
+ end
3187
+ end
3188
+ end
3189
+
3190
+ if not functionStart then
3191
+ return {
3192
+ success = false,
3193
+ error = "Function '" .. functionName .. "' not found in script",
3194
+ hint = "Check the function name spelling. Use search_script to find available functions."
3195
+ }
3196
+ end
3197
+
3198
+ if not functionEnd then
3199
+ return {
3200
+ success = false,
3201
+ error = "Could not find the end of function '" .. functionName .. "' - the function may be malformed",
3202
+ startLine = functionStart
3203
+ }
3204
+ end
3205
+
3206
+ -- Extract the function source
3207
+ local functionLines = {}
3208
+ for i = functionStart, functionEnd do
3209
+ table.insert(functionLines, lines[i])
3210
+ end
3211
+
3212
+ local functionSource = table.concat(functionLines, "\n")
3213
+
3214
+ -- Create numbered version
3215
+ local numberedLines = {}
3216
+ for i = functionStart, functionEnd do
3217
+ table.insert(numberedLines, string.format("%4d: %s", i, lines[i]))
3218
+ end
3219
+ local numberedSource = table.concat(numberedLines, "\n")
3220
+
3221
+ return {
3222
+ success = true,
3223
+ instancePath = instancePath,
3224
+ functionName = functionName,
3225
+ startLine = functionStart,
3226
+ endLine = functionEnd,
3227
+ lineCount = functionEnd - functionStart + 1,
3228
+ source = functionSource,
3229
+ numberedSource = numberedSource
3230
+ }
3231
+ end)
3232
+
3233
+ if success then
3234
+ return result
3235
+ else
3236
+ return { error = "Failed to get function: " .. tostring(result) }
3237
+ end
3238
+ end
3239
+
3240
+ -- find_and_replace_in_scripts: Batch replace across multiple scripts
3241
+ handlers.findAndReplaceInScripts = function(requestData)
3242
+ local paths = requestData.paths
3243
+ local oldString = requestData.oldString
3244
+ local newString = requestData.newString
3245
+ local validateAfter = requestData.validateAfter
3246
+ if validateAfter == nil then validateAfter = true end
3247
+
3248
+ if not paths or #paths == 0 then
3249
+ return { error = "Paths array is required" }
3250
+ end
3251
+ if not oldString or oldString == "" then
3252
+ return { error = "old_string is required" }
3253
+ end
3254
+ if newString == nil then
3255
+ return { error = "new_string is required" }
3256
+ end
3257
+
3258
+ local results = {}
3259
+ local successCount = 0
3260
+ local failCount = 0
3261
+ local skippedCount = 0
3262
+
3263
+ for _, path in ipairs(paths) do
3264
+ local instance = getInstanceByPath(path)
3265
+ if not instance then
3266
+ table.insert(results, { path = path, success = false, error = "Instance not found" })
3267
+ failCount = failCount + 1
3268
+ elseif not instance:IsA("LuaSourceContainer") then
3269
+ table.insert(results, { path = path, success = false, error = "Not a script" })
3270
+ failCount = failCount + 1
3271
+ else
3272
+ local source = instance.Source
3273
+ local searchStr = oldString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
3274
+ local replaceStr = newString:gsub("\\n", "\n"):gsub("\\t", "\t"):gsub("\\r", "\r"):gsub("\\\\", "\\")
3275
+
3276
+ local occurrences = countOccurrences(source, searchStr)
3277
+
3278
+ if occurrences == 0 then
3279
+ table.insert(results, { path = path, success = true, skipped = true, reason = "Pattern not found" })
3280
+ skippedCount = skippedCount + 1
3281
+ else
3282
+ local newSource = string.gsub(source, searchStr:gsub("([^%w])", "%%%1"), replaceStr)
3283
+
3284
+ if validateAfter then
3285
+ local isValid, syntaxErrors = validateLuaSyntax(newSource)
3286
+ if not isValid then
3287
+ table.insert(results, { path = path, success = false, error = "Would create invalid syntax", syntaxErrors = syntaxErrors })
3288
+ failCount = failCount + 1
3289
+ else
3290
+ pcall(function()
3291
+ ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end)
3292
+ end)
3293
+ table.insert(results, { path = path, success = true, replacements = occurrences })
3294
+ successCount = successCount + 1
3295
+ end
3296
+ else
3297
+ pcall(function()
3298
+ ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end)
3299
+ end)
3300
+ table.insert(results, { path = path, success = true, replacements = occurrences })
3301
+ successCount = successCount + 1
3302
+ end
3303
+ end
3304
+ end
3305
+ end
3306
+
3307
+ if successCount > 0 then
3308
+ ChangeHistoryService:SetWaypoint("Batch replace in " .. successCount .. " scripts")
3309
+ end
3310
+
3311
+ return {
3312
+ success = failCount == 0,
3313
+ results = results,
3314
+ summary = {
3315
+ total = #paths,
3316
+ successful = successCount,
3317
+ failed = failCount,
3318
+ skipped = skippedCount
3319
+ }
3320
+ }
3321
+ end
3322
+
2739
3323
  -- Attribute Tools: Get a single attribute
2740
3324
  handlers.getAttribute = function(requestData)
2741
3325
  local instancePath = requestData.instancePath
@@ -3412,15 +3996,31 @@ handlers.validateScript = function(requestData)
3412
3996
  end
3413
3997
 
3414
3998
  -- ============================================
3415
- -- UNDO/REDO HANDLERS
3999
+ -- UNDO/REDO HANDLERS (Enhanced with action tracking)
3416
4000
  -- ============================================
3417
4001
 
3418
4002
  handlers.undo = function(requestData)
3419
4003
  local success, result = pcall(function()
4004
+ -- Pop the last action from history
4005
+ local lastAction = nil
4006
+ if #actionHistory > 0 then
4007
+ lastAction = table.remove(actionHistory)
4008
+ -- Move it to redo history
4009
+ table.insert(redoHistory, lastAction)
4010
+ end
4011
+
3420
4012
  ChangeHistoryService:Undo()
4013
+
3421
4014
  return {
3422
4015
  success = true,
3423
- message = "Undo executed"
4016
+ undone = lastAction and (lastAction.action .. " " .. lastAction.target .. " (" .. lastAction.summary .. ")") or "Unknown action (possibly manual change)",
4017
+ action = lastAction and lastAction.action or nil,
4018
+ target = lastAction and lastAction.target or nil,
4019
+ summary = lastAction and lastAction.summary or nil,
4020
+ details = lastAction and lastAction.details or nil,
4021
+ remaining_undos = #actionHistory,
4022
+ available_redos = #redoHistory,
4023
+ message = lastAction and ("Undone: " .. lastAction.summary) or "Undo executed"
3424
4024
  }
3425
4025
  end)
3426
4026
 
@@ -3429,17 +4029,35 @@ handlers.undo = function(requestData)
3429
4029
  else
3430
4030
  return {
3431
4031
  success = false,
3432
- error = "Failed to undo: " .. tostring(result)
4032
+ error = "Failed to undo: " .. tostring(result),
4033
+ remaining_undos = #actionHistory,
4034
+ available_redos = #redoHistory
3433
4035
  }
3434
4036
  end
3435
4037
  end
3436
4038
 
3437
4039
  handlers.redo = function(requestData)
3438
4040
  local success, result = pcall(function()
4041
+ -- Pop the last undone action from redo history
4042
+ local redoneAction = nil
4043
+ if #redoHistory > 0 then
4044
+ redoneAction = table.remove(redoHistory)
4045
+ -- Move it back to action history
4046
+ table.insert(actionHistory, redoneAction)
4047
+ end
4048
+
3439
4049
  ChangeHistoryService:Redo()
4050
+
3440
4051
  return {
3441
4052
  success = true,
3442
- message = "Redo executed"
4053
+ redone = redoneAction and (redoneAction.action .. " " .. redoneAction.target .. " (" .. redoneAction.summary .. ")") or "Unknown action",
4054
+ action = redoneAction and redoneAction.action or nil,
4055
+ target = redoneAction and redoneAction.target or nil,
4056
+ summary = redoneAction and redoneAction.summary or nil,
4057
+ details = redoneAction and redoneAction.details or nil,
4058
+ remaining_undos = #actionHistory,
4059
+ available_redos = #redoHistory,
4060
+ message = redoneAction and ("Redone: " .. redoneAction.summary) or "Redo executed"
3443
4061
  }
3444
4062
  end)
3445
4063
 
@@ -3448,7 +4066,9 @@ handlers.redo = function(requestData)
3448
4066
  else
3449
4067
  return {
3450
4068
  success = false,
3451
- error = "Failed to redo: " .. tostring(result)
4069
+ error = "Failed to redo: " .. tostring(result),
4070
+ remaining_undos = #actionHistory,
4071
+ available_redos = #redoHistory
3452
4072
  }
3453
4073
  end
3454
4074
  end
@@ -3559,6 +4179,11 @@ endpointHandlers = {
3559
4179
  ["/api/edit-script-lines"] = handlers.editScriptLines,
3560
4180
  ["/api/insert-script-lines"] = handlers.insertScriptLines,
3561
4181
  ["/api/delete-script-lines"] = handlers.deleteScriptLines,
4182
+ -- Claude Code-style script editing tools
4183
+ ["/api/edit-script"] = handlers.editScript,
4184
+ ["/api/search-script"] = handlers.searchScript,
4185
+ ["/api/get-script-function"] = handlers.getScriptFunction,
4186
+ ["/api/find-and-replace-in-scripts"] = handlers.findAndReplaceInScripts,
3562
4187
  ["/api/get-attribute"] = handlers.getAttribute,
3563
4188
  ["/api/set-attribute"] = handlers.setAttribute,
3564
4189
  ["/api/get-attributes"] = handlers.getAttributes,
@@ -3573,6 +4198,8 @@ endpointHandlers = {
3573
4198
  ["/api/move-instance"] = handlers.moveInstance,
3574
4199
  ["/api/validate-script"] = handlers.validateScript,
3575
4200
  ["/api/insert-asset"] = handlers.insertAsset,
4201
+ ["/api/undo"] = handlers.undo,
4202
+ ["/api/redo"] = handlers.redo,
3576
4203
  }
3577
4204
 
3578
4205
  local function updateUIState()