@adiholick/nvstudio-mcp 2.1.8 → 2.2.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 +30 -8
- package/dist/index.js +31 -15
- package/dist/luau-validator.js +26 -57
- package/dist/roblox-bridge.js +89 -56
- package/dist/task-queue.js +15 -1
- package/package.json +1 -1
- package/studio-plugin/Main.server.lua +50 -22
- package/studio-plugin/create_instance.lua +20 -10
- package/studio-plugin/delete_instance.lua +9 -0
- package/studio-plugin/execute_luau.lua +25 -6
- package/studio-plugin/generate_terrain.lua +2 -1
- package/studio-plugin/get_children.lua +7 -1
- package/studio-plugin/get_full_tree.lua +46 -0
- package/studio-plugin/insert_asset.lua +8 -3
- package/studio-plugin/nvstudio_mcp.rbxmx +356 -69
- package/studio-plugin/patch_script_source.lua +24 -4
- package/studio-plugin/rollback_script.lua +9 -3
- package/studio-plugin/script_grep.lua +24 -9
- package/studio-plugin/search_instance.lua +24 -5
- package/studio-plugin/set_properties.lua +73 -0
- package/studio-plugin/ui_manager.lua +4 -4
- package/studio-plugin/update_script_source.lua +5 -1
|
@@ -24,6 +24,10 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
24
24
|
local replacementContent = payload.replacement_content
|
|
25
25
|
local allowMultiple = payload.allow_multiple or false
|
|
26
26
|
|
|
27
|
+
if targetContent == "" then
|
|
28
|
+
return { status = "error", error = "target_content tidak boleh berupa string kosong." }
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
-- Escape magic characters for string.gsub if needed, or use string.find manually.
|
|
28
32
|
-- string.gsub doesn't do plain text replacement directly without escaping.
|
|
29
33
|
-- We'll write a plain text replace function.
|
|
@@ -45,16 +49,28 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
45
49
|
return res, count
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
local
|
|
52
|
+
local function countOccurrences(str, pattern)
|
|
53
|
+
local count = 0
|
|
54
|
+
local startIdx = 1
|
|
55
|
+
while true do
|
|
56
|
+
local findStart, findEnd = string.find(str, pattern, startIdx, true)
|
|
57
|
+
if not findStart then break end
|
|
58
|
+
count = count + 1
|
|
59
|
+
startIdx = findEnd + 1
|
|
60
|
+
if count > 1 and not allowMultiple then break end
|
|
61
|
+
end
|
|
62
|
+
return count
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local occurrences = countOccurrences(currentSource, targetContent)
|
|
49
66
|
|
|
50
67
|
if occurrences == 0 then
|
|
51
68
|
return { status = "error", error = "target_content tidak ditemukan di dalam script." }
|
|
52
69
|
elseif occurrences > 1 and not allowMultiple then
|
|
53
|
-
return { status = "error", error = "target_content ditemukan
|
|
70
|
+
return { status = "error", error = "target_content ditemukan lebih dari 1 kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
|
|
54
71
|
end
|
|
55
72
|
|
|
56
73
|
newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
|
|
57
|
-
affectedLines = affectedLines -- not exact line count, but occurrences count
|
|
58
74
|
|
|
59
75
|
-- Mode 2: Line Range Replace
|
|
60
76
|
elseif payload.start_line and payload.end_line and payload.replacement_content then
|
|
@@ -84,7 +100,11 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
84
100
|
end
|
|
85
101
|
|
|
86
102
|
-- Simpan backup snapshot sebelum mengaplikasikan patch
|
|
87
|
-
ctx.scriptHistory[targetPath]
|
|
103
|
+
if type(ctx.scriptHistory[targetPath]) ~= "table" then
|
|
104
|
+
ctx.scriptHistory[targetPath] = {}
|
|
105
|
+
end
|
|
106
|
+
table.insert(ctx.scriptHistory[targetPath], currentSource)
|
|
107
|
+
if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
|
|
88
108
|
|
|
89
109
|
-- Terapkan perubahan
|
|
90
110
|
targetInstance.Source = newSource
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
2
|
if targetInstance:IsA("LuaSourceContainer") then
|
|
3
|
-
local
|
|
4
|
-
if
|
|
3
|
+
local historyStack = ctx.scriptHistory[targetPath]
|
|
4
|
+
if historyStack and type(historyStack) == "table" and #historyStack > 0 then
|
|
5
|
+
local prevSource = table.remove(historyStack)
|
|
5
6
|
targetInstance.Source = prevSource
|
|
6
|
-
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
|
|
7
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya. Sisa backup: " .. #historyStack }
|
|
8
|
+
elseif type(historyStack) == "string" then
|
|
9
|
+
-- Kompatibilitas mundur dengan data lama (sebelum FEAT-04)
|
|
10
|
+
targetInstance.Source = historyStack
|
|
11
|
+
ctx.scriptHistory[targetPath] = nil
|
|
12
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback (versi tunggal/legacy)." }
|
|
7
13
|
else
|
|
8
14
|
return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
|
|
9
15
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
-
|
|
3
|
-
if not query or query == "" then
|
|
2
|
+
if not data or data == "" or data == "nil" then
|
|
4
3
|
return { status = "error", error = "Query pencarian tidak boleh kosong." }
|
|
5
4
|
end
|
|
5
|
+
local query = tostring(data)
|
|
6
6
|
|
|
7
7
|
local matches = {}
|
|
8
8
|
local matchCount = 0
|
|
@@ -13,7 +13,9 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
13
13
|
game:GetService("ServerScriptService"),
|
|
14
14
|
game:GetService("ReplicatedStorage"),
|
|
15
15
|
game:GetService("StarterPlayer"),
|
|
16
|
-
game:GetService("StarterGui")
|
|
16
|
+
game:GetService("StarterGui"),
|
|
17
|
+
game:GetService("ServerStorage"),
|
|
18
|
+
game:GetService("Lighting")
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
local function searchInScript(scriptObj)
|
|
@@ -43,19 +45,32 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
43
45
|
return false
|
|
44
46
|
end
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
if targetInstance and targetInstance ~= game then
|
|
49
|
+
local descendants = {}
|
|
50
|
+
pcall(function() descendants = targetInstance:GetDescendants() end)
|
|
51
|
+
for _, desc in ipairs(descendants) do
|
|
52
|
+
if matchCount >= MAX_MATCHES then break end
|
|
53
|
+
if desc:IsA("LuaSourceContainer") then
|
|
54
|
+
if searchInScript(desc) then
|
|
55
|
+
break
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
for _, service in ipairs(searchableServices) do
|
|
61
|
+
local descendants = {}
|
|
62
|
+
pcall(function() descendants = service:GetDescendants() end)
|
|
49
63
|
for _, desc in ipairs(descendants) do
|
|
64
|
+
if matchCount >= MAX_MATCHES then break end
|
|
50
65
|
if desc:IsA("LuaSourceContainer") then
|
|
51
66
|
if searchInScript(desc) then
|
|
52
67
|
break
|
|
53
68
|
end
|
|
54
69
|
end
|
|
55
70
|
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
if matchCount >= MAX_MATCHES then
|
|
72
|
+
break
|
|
73
|
+
end
|
|
59
74
|
end
|
|
60
75
|
end
|
|
61
76
|
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
if not data or data == "" or data == "nil" then
|
|
3
|
+
return { status = "error", error = "Query pencarian tidak boleh kosong." }
|
|
4
|
+
end
|
|
2
5
|
local searchName = tostring(data)
|
|
3
6
|
local results = {}
|
|
4
7
|
|
|
5
8
|
local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
|
|
9
|
+
local lowerSearch = string.lower(searchName)
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
local nodesChecked = 0
|
|
12
|
+
local function search(inst)
|
|
13
|
+
nodesChecked = nodesChecked + 1
|
|
14
|
+
if nodesChecked % 1000 == 0 then task.wait() end
|
|
15
|
+
if string.find(inst.Name:lower(), searchName:lower(), 1, true) then
|
|
16
|
+
table.insert(results, inst:GetFullName() .. " (" .. inst.ClassName .. ")")
|
|
17
|
+
end
|
|
18
|
+
if #results >= 50 then return end
|
|
19
|
+
for _, child in ipairs(inst:GetChildren()) do
|
|
20
|
+
search(child)
|
|
21
|
+
if #results >= 50 then break end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if targetInstance and targetInstance ~= game then
|
|
26
|
+
search(targetInstance)
|
|
27
|
+
else
|
|
28
|
+
for _, service in ipairs(searchableServices) do
|
|
29
|
+
search(service)
|
|
30
|
+
if #results >= 50 then break end
|
|
12
31
|
end
|
|
13
32
|
end
|
|
14
33
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
|
|
3
|
+
if not success or type(config) ~= "table" then
|
|
4
|
+
return { status = "error", error = "Format data JSON tidak valid untuk set_properties." }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local applied = {}
|
|
8
|
+
local errors = {}
|
|
9
|
+
|
|
10
|
+
for prop, val in pairs(config) do
|
|
11
|
+
local ok, err = pcall(function()
|
|
12
|
+
local currentVal = targetInstance[prop]
|
|
13
|
+
if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
|
|
14
|
+
targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
|
|
15
|
+
elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
|
|
16
|
+
local r, g, b = val[1], val[2], val[3]
|
|
17
|
+
if r > 1 or g > 1 or b > 1 then
|
|
18
|
+
targetInstance[prop] = Color3.fromRGB(r, g, b)
|
|
19
|
+
else
|
|
20
|
+
targetInstance[prop] = Color3.new(r, g, b)
|
|
21
|
+
end
|
|
22
|
+
elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
|
|
23
|
+
targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
|
|
24
|
+
elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
|
|
25
|
+
targetInstance[prop] = CFrame.new(unpack(val))
|
|
26
|
+
elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
|
|
27
|
+
local enumType = tostring(currentVal.EnumType)
|
|
28
|
+
local successEnum, enumValue = pcall(function() return Enum[enumType][val] end)
|
|
29
|
+
if successEnum and enumValue then
|
|
30
|
+
targetInstance[prop] = enumValue
|
|
31
|
+
else
|
|
32
|
+
error("Nilai Enum '" .. val .. "' tidak valid untuk EnumType '" .. enumType .. "'")
|
|
33
|
+
end
|
|
34
|
+
elseif prop == "Parent" or prop == "PrimaryPart" or string.match(prop, ".*Value$") then
|
|
35
|
+
-- Evaluasi properti relasional
|
|
36
|
+
if typeof(currentVal) == "Instance" or typeof(targetInstance[prop]) == "Instance" or val == nil then
|
|
37
|
+
if type(val) == "string" and ctx.getInstanceFromPath then
|
|
38
|
+
local resolved = ctx.getInstanceFromPath(val)
|
|
39
|
+
if resolved then
|
|
40
|
+
targetInstance[prop] = resolved
|
|
41
|
+
else
|
|
42
|
+
error("Instance target tidak ditemukan pada path: " .. val)
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
targetInstance[prop] = val
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
targetInstance[prop] = val
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
targetInstance[prop] = val
|
|
52
|
+
end
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
if ok then
|
|
56
|
+
table.insert(applied, prop)
|
|
57
|
+
else
|
|
58
|
+
table.insert(errors, prop .. ": " .. tostring(err))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if #errors > 0 then
|
|
63
|
+
local successMsg = ""
|
|
64
|
+
if #applied > 0 then
|
|
65
|
+
successMsg = "Berhasil mengubah: " .. table.concat(applied, ", ") .. ".\n"
|
|
66
|
+
end
|
|
67
|
+
return {
|
|
68
|
+
status = #applied > 0 and "success" or "error",
|
|
69
|
+
result = successMsg .. "Namun beberapa properti gagal diubah:\n" .. table.concat(errors, "\n")
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
return { status = "success", result = "Berhasil mengubah properti: " .. table.concat(applied, ", ") }
|
|
73
|
+
end
|
|
@@ -95,7 +95,7 @@ function UIManager.init(pluginInstance, studioId)
|
|
|
95
95
|
|
|
96
96
|
-- Version
|
|
97
97
|
local versionLabel = Instance.new("TextLabel")
|
|
98
|
-
versionLabel.Text = "v2.
|
|
98
|
+
versionLabel.Text = "v2.2.0"
|
|
99
99
|
versionLabel.Font = Enum.Font.Gotham
|
|
100
100
|
versionLabel.TextSize = 11
|
|
101
101
|
versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
|
|
@@ -419,9 +419,9 @@ function UIManager:setStatus(stateStr)
|
|
|
419
419
|
-- Server reachable, tampilkan Connected meskipun agent (IDE) belum konek via stdio
|
|
420
420
|
self.connectBtn.Text = " Disconnect"
|
|
421
421
|
self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
|
|
422
|
-
self.statusLabel.Text = "●
|
|
423
|
-
self.statusLabel.TextColor3 = Color3.fromRGB(
|
|
424
|
-
self.termStroke.Color = Color3.fromRGB(
|
|
422
|
+
self.statusLabel.Text = "● IDE Disconnected"
|
|
423
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(255, 180, 50)
|
|
424
|
+
self.termStroke.Color = Color3.fromRGB(120, 80, 20)
|
|
425
425
|
|
|
426
426
|
elseif stateStr == "connected" then
|
|
427
427
|
self.connectBtn.Text = " Disconnect"
|
|
@@ -7,7 +7,11 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
7
7
|
-- hanya akan membuat AI gagal terus menerus untuk menulis script.
|
|
8
8
|
-- Error syntax akan ditangkap secara native oleh Roblox Output.
|
|
9
9
|
|
|
10
|
-
ctx.scriptHistory[targetPath]
|
|
10
|
+
if type(ctx.scriptHistory[targetPath]) ~= "table" then
|
|
11
|
+
ctx.scriptHistory[targetPath] = {}
|
|
12
|
+
end
|
|
13
|
+
table.insert(ctx.scriptHistory[targetPath], targetInstance.Source)
|
|
14
|
+
if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
|
|
11
15
|
targetInstance.Source = newCode
|
|
12
16
|
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
|
|
13
17
|
else
|