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.
- package/README.md +29 -1
- package/dist/index.js +120 -2
- package/dist/index.js.map +1 -1
- package/dist/tools/index.d.ts +37 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +103 -0
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/studio-plugin/plugin.luau +637 -10
|
@@ -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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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()
|