@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
|
@@ -31,6 +31,7 @@ local ctx = {
|
|
|
31
31
|
recentLogs = {},
|
|
32
32
|
HttpService = HttpService,
|
|
33
33
|
studioId = STUDIO_ID,
|
|
34
|
+
getInstanceFromPath = nil -- akan di-bind di bawah
|
|
34
35
|
}
|
|
35
36
|
local MAX_LOGS = 50
|
|
36
37
|
local isRunning = false -- apakah loop aktif
|
|
@@ -43,6 +44,7 @@ local serverReachable = false -- apakah server Node.js reachable
|
|
|
43
44
|
local RECONNECT_DELAY_INITIAL = 1.5
|
|
44
45
|
local RECONNECT_DELAY_MAX = 1.5
|
|
45
46
|
local reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
47
|
+
local currentLoopId = nil
|
|
46
48
|
|
|
47
49
|
-- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
|
|
48
50
|
local skills = {}
|
|
@@ -122,21 +124,41 @@ LogService.MessageOut:Connect(addSystemLog)
|
|
|
122
124
|
|
|
123
125
|
-- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
|
|
124
126
|
local function getInstanceFromPath(pathString)
|
|
127
|
+
if type(pathString) ~= "string" or pathString == "" then return nil end
|
|
125
128
|
local parts = string.split(pathString, ".")
|
|
129
|
+
if #parts == 0 then return nil end
|
|
126
130
|
local current = game
|
|
127
131
|
if parts[1] == "game" then table.remove(parts, 1) end
|
|
128
132
|
pcall(function()
|
|
129
133
|
local svc = game:GetService(parts[1])
|
|
130
134
|
if svc then current = svc; table.remove(parts, 1) end
|
|
131
135
|
end)
|
|
132
|
-
|
|
136
|
+
local i = 1
|
|
137
|
+
while i <= #parts do
|
|
138
|
+
local part = parts[i]
|
|
133
139
|
local found = current:FindFirstChild(part)
|
|
140
|
+
|
|
141
|
+
-- Jika tidak ketemu, coba gabungkan dengan part berikutnya (untuk nama yang mengandung titik)
|
|
142
|
+
local combinedPart = part
|
|
143
|
+
local lookAhead = i + 1
|
|
144
|
+
while not found and lookAhead <= #parts do
|
|
145
|
+
combinedPart = combinedPart .. "." .. parts[lookAhead]
|
|
146
|
+
found = current:FindFirstChild(combinedPart)
|
|
147
|
+
if found then
|
|
148
|
+
i = lookAhead
|
|
149
|
+
end
|
|
150
|
+
lookAhead = lookAhead + 1
|
|
151
|
+
end
|
|
152
|
+
|
|
134
153
|
if not found then return nil end
|
|
135
154
|
current = found
|
|
155
|
+
i = i + 1
|
|
136
156
|
end
|
|
137
157
|
return current
|
|
138
158
|
end
|
|
139
159
|
|
|
160
|
+
ctx.getInstanceFromPath = getInstanceFromPath
|
|
161
|
+
|
|
140
162
|
-- ── Proses Task dari Server ───────────────────────────────────────────────────
|
|
141
163
|
local function processTask(taskData)
|
|
142
164
|
if not taskData or not taskData.id or not taskData.command then return nil end
|
|
@@ -147,7 +169,11 @@ local function processTask(taskData)
|
|
|
147
169
|
|
|
148
170
|
local targetInstance = getInstanceFromPath(taskData.target)
|
|
149
171
|
if not targetInstance then
|
|
150
|
-
|
|
172
|
+
if taskData.target == "" or taskData.target == "nil" then
|
|
173
|
+
targetInstance = game
|
|
174
|
+
else
|
|
175
|
+
return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
|
|
176
|
+
end
|
|
151
177
|
end
|
|
152
178
|
|
|
153
179
|
local skillFunc = skills[taskData.command]
|
|
@@ -155,8 +181,16 @@ local function processTask(taskData)
|
|
|
155
181
|
return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
|
|
156
182
|
end
|
|
157
183
|
|
|
158
|
-
local result =
|
|
159
|
-
|
|
184
|
+
local ok, result = pcall(function()
|
|
185
|
+
return skillFunc(targetInstance, taskData.data, ctx, taskData.target)
|
|
186
|
+
end)
|
|
187
|
+
|
|
188
|
+
if not ok then
|
|
189
|
+
result = { status = "error", error = "Internal Plugin Error: " .. tostring(result) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
result = result or { status = "success", result = "Eksekusi selesai (Tidak ada return value)" }
|
|
193
|
+
result.id = taskData.id
|
|
160
194
|
|
|
161
195
|
if ui then
|
|
162
196
|
local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
|
|
@@ -188,24 +222,11 @@ local function sendResponse(taskId, resultData)
|
|
|
188
222
|
end)
|
|
189
223
|
end
|
|
190
224
|
|
|
191
|
-
-- ── Update UI berdasarkan state ───────────────────────────────────────────────
|
|
192
|
-
local function updateUIStatus()
|
|
193
|
-
if not ui then return end
|
|
194
|
-
if not serverReachable then
|
|
195
|
-
ui:setStatus("waiting")
|
|
196
|
-
elseif mcpAgentActive then
|
|
197
|
-
ui:setStatus("connected")
|
|
198
|
-
else
|
|
199
|
-
-- Server reachable tapi tidak ada AI agent
|
|
200
|
-
-- Tampilkan sebagai connected (karena server sse bridge aktif)
|
|
201
|
-
ui:setStatus("server_connected")
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
225
|
-- ── Proses satu event dari SSE stream ────────────────────────────────────────
|
|
206
226
|
local function handleSSELine(line)
|
|
207
227
|
-- Format SSE: "data: {...json...}"
|
|
208
228
|
local payload = line
|
|
229
|
+
if line:sub(1, 6) == "event:" then return end
|
|
209
230
|
if line:sub(1, 5) == "data:" then
|
|
210
231
|
payload = line:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
|
|
211
232
|
end
|
|
@@ -227,8 +248,12 @@ local function handleSSELine(line)
|
|
|
227
248
|
local changed = (mcpAgentActive ~= newActive)
|
|
228
249
|
mcpAgentActive = newActive
|
|
229
250
|
if ui then
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
if mcpAgentActive then
|
|
252
|
+
ui:setStatus("connected")
|
|
253
|
+
else
|
|
254
|
+
ui:setStatus("server_connected")
|
|
255
|
+
end
|
|
256
|
+
|
|
232
257
|
if newActive and changed then
|
|
233
258
|
ui:addLog("[System] ✅ IDE Agent (Antigravity) aktif.", Color3.fromRGB(100, 255, 100))
|
|
234
259
|
elseif not newActive and changed then
|
|
@@ -273,13 +298,16 @@ function startConnectionLoop()
|
|
|
273
298
|
isManualDisconnect = false
|
|
274
299
|
reconnectDelay = RECONNECT_DELAY_INITIAL
|
|
275
300
|
|
|
301
|
+
local loopId = HttpService:GenerateGUID(false)
|
|
302
|
+
currentLoopId = loopId
|
|
303
|
+
|
|
276
304
|
if ui then
|
|
277
305
|
ui:addLog("[System] NVStudio MCP v2.1.8 — Menghubungkan ke server...", Color3.fromRGB(200, 200, 200))
|
|
278
306
|
end
|
|
279
307
|
|
|
280
308
|
local consecutiveErrors = 0
|
|
281
309
|
|
|
282
|
-
while isRunning and not isManualDisconnect do
|
|
310
|
+
while isRunning and not isManualDisconnect and currentLoopId == loopId do
|
|
283
311
|
-- Stream / polling snapshot dari server
|
|
284
312
|
local streamOk, streamRaw = pcall(function()
|
|
285
313
|
return HttpService:GetAsync(
|
|
@@ -295,7 +323,7 @@ function startConnectionLoop()
|
|
|
295
323
|
if not serverReachable then
|
|
296
324
|
serverReachable = true
|
|
297
325
|
if ui then
|
|
298
|
-
ui:setStatus("connected")
|
|
326
|
+
ui:setStatus(mcpAgentActive and "connected" or "server_connected")
|
|
299
327
|
ui:addLog("[System] ✅ Server MCP terhubung & siap.", Color3.fromRGB(100, 255, 150))
|
|
300
328
|
end
|
|
301
329
|
end
|
|
@@ -7,18 +7,28 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
7
7
|
local newSuccess, newInst = pcall(function()
|
|
8
8
|
local inst = Instance.new(config.ClassName)
|
|
9
9
|
inst.Parent = targetInstance
|
|
10
|
-
|
|
11
|
-
if config.Properties then
|
|
12
|
-
for prop, val in pairs(config.Properties) do
|
|
13
|
-
pcall(function() inst[prop] = val end)
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
10
|
return inst
|
|
17
11
|
end)
|
|
18
12
|
|
|
19
|
-
if newSuccess then
|
|
20
|
-
return { status = "
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
if not newSuccess then
|
|
14
|
+
return { status = "error", error = "Gagal membuat Instance: " .. tostring(newInst) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
local propResultMsg = ""
|
|
18
|
+
if config.Properties and next(config.Properties) then
|
|
19
|
+
local setPropsFunc = require(script.Parent:FindFirstChild("set_properties"))
|
|
20
|
+
if setPropsFunc then
|
|
21
|
+
local propData = ctx.HttpService:JSONEncode(config.Properties)
|
|
22
|
+
local propRes = setPropsFunc(newInst, propData, ctx, "")
|
|
23
|
+
if propRes.status == "error" then
|
|
24
|
+
propResultMsg = " (Namun ada error properti: " .. propRes.result .. ")"
|
|
25
|
+
elseif propRes.result then
|
|
26
|
+
propResultMsg = " (" .. propRes.result .. ")"
|
|
27
|
+
end
|
|
28
|
+
else
|
|
29
|
+
propResultMsg = " (Gagal meload set_properties)"
|
|
30
|
+
end
|
|
23
31
|
end
|
|
32
|
+
|
|
33
|
+
return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath .. propResultMsg }
|
|
24
34
|
end
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
if targetInstance == game or targetInstance.Parent == game then
|
|
3
|
+
return { status = "error", error = "Tidak dapat menghapus root DataModel atau Service utama!" }
|
|
4
|
+
end
|
|
5
|
+
|
|
2
6
|
-- Guardrail: Cegah penghapusan Service utama
|
|
3
7
|
local protectedServices = {
|
|
4
8
|
"Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
|
|
@@ -19,6 +23,11 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
19
23
|
local className = targetInstance.ClassName
|
|
20
24
|
local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
|
|
21
25
|
|
|
26
|
+
local childrenCount = #targetInstance:GetDescendants()
|
|
27
|
+
if childrenCount > 10 and data ~= "force" then
|
|
28
|
+
return { status = "error", error = "GUARDRAIL: Instance '" .. name .. "' memiliki " .. childrenCount .. " descendants. Jika Anda yakin ingin menghapus seluruhnya beserta isinya, sertakan data='force'." }
|
|
29
|
+
end
|
|
30
|
+
|
|
22
31
|
local success, err = pcall(function()
|
|
23
32
|
targetInstance:Destroy()
|
|
24
33
|
end)
|
|
@@ -10,7 +10,7 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
10
10
|
local wrappedCode = "return function()\n" .. code .. "\nend"
|
|
11
11
|
|
|
12
12
|
local moduleScript = Instance.new("ModuleScript")
|
|
13
|
-
moduleScript.Name = "
|
|
13
|
+
moduleScript.Name = "AI_Execute_" .. HttpService:GenerateGUID(false):gsub("-", "")
|
|
14
14
|
moduleScript.Source = wrappedCode
|
|
15
15
|
|
|
16
16
|
-- Taruh di ReplicatedStorage sementara agar bisa di-require
|
|
@@ -21,10 +21,17 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
21
21
|
local isFinished = false
|
|
22
22
|
|
|
23
23
|
-- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
|
|
24
|
-
|
|
24
|
+
local logs = {}
|
|
25
|
+
|
|
26
|
+
local execThread = task.spawn(function()
|
|
25
27
|
local reqSuccess, func = pcall(function() return require(moduleScript) end)
|
|
26
28
|
if reqSuccess and type(func) == "function" then
|
|
29
|
+
local LogService = game:GetService("LogService")
|
|
30
|
+
local connection = LogService.MessageOut:Connect(function(msg, msgType)
|
|
31
|
+
table.insert(logs, msg)
|
|
32
|
+
end)
|
|
27
33
|
local execSuccess, res = pcall(func)
|
|
34
|
+
pcall(function() connection:Disconnect() end)
|
|
28
35
|
if execSuccess then
|
|
29
36
|
resultData = res
|
|
30
37
|
else
|
|
@@ -42,20 +49,31 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
42
49
|
timeWaited = timeWaited + task.wait()
|
|
43
50
|
end
|
|
44
51
|
|
|
52
|
+
|
|
53
|
+
if not isFinished then
|
|
54
|
+
pcall(function() task.cancel(execThread) end)
|
|
55
|
+
end
|
|
56
|
+
|
|
45
57
|
-- Pembersihan (Wajib dihancurkan)
|
|
46
58
|
pcall(function() moduleScript:Destroy() end)
|
|
47
59
|
|
|
48
60
|
if not isFinished then
|
|
49
|
-
return { status = "error", error = "Timeout 5 detik terlampaui.
|
|
61
|
+
return { status = "error", error = "Timeout 5 detik terlampaui. Thread mungkin tetap berjalan di background jika terjebak dalam infinite loop tanpa task.wait(). PASTIKAN menambahkan task.wait() di dalam loop komputasi berat." }
|
|
50
62
|
end
|
|
51
63
|
|
|
52
64
|
if execError then
|
|
53
|
-
return { status = "error", error = execError }
|
|
65
|
+
return { status = "error", error = execError, logs = #logs > 0 and logs or nil }
|
|
54
66
|
end
|
|
55
67
|
|
|
68
|
+
local returnPayload = { status = "success" }
|
|
69
|
+
if #logs > 0 then
|
|
70
|
+
returnPayload.logs = logs
|
|
71
|
+
end
|
|
72
|
+
|
|
56
73
|
-- Cegah return nil error
|
|
57
74
|
if resultData == nil then
|
|
58
|
-
|
|
75
|
+
returnPayload.result = "Eksekusi berhasil (Tidak ada nilai kembalian)."
|
|
76
|
+
return returnPayload
|
|
59
77
|
end
|
|
60
78
|
|
|
61
79
|
-- JSON Encode untuk mencegah raw Instance return crash
|
|
@@ -64,7 +82,8 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
64
82
|
end)
|
|
65
83
|
|
|
66
84
|
if serializeSuccess then
|
|
67
|
-
|
|
85
|
+
returnPayload.result = HttpService:JSONDecode(jsonRes) -- decode kembali agar format aslinya diteruskan oleh router
|
|
86
|
+
return returnPayload
|
|
68
87
|
else
|
|
69
88
|
return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
|
|
70
89
|
end
|
|
@@ -6,7 +6,8 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
6
6
|
|
|
7
7
|
local fillSuccess, err = pcall(function()
|
|
8
8
|
local terrain = game.Workspace.Terrain
|
|
9
|
-
local materialEnum = Enum.Material[config.Material]
|
|
9
|
+
local matOk, materialEnum = pcall(function() return Enum.Material[config.Material] end)
|
|
10
|
+
if not matOk then materialEnum = Enum.Material.Grass end
|
|
10
11
|
|
|
11
12
|
local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
|
|
12
13
|
local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
2
|
local children = {}
|
|
3
|
-
|
|
3
|
+
local allChildren = targetInstance:GetChildren()
|
|
4
|
+
local MAX_LIMIT = 200
|
|
5
|
+
for i, child in ipairs(allChildren) do
|
|
6
|
+
if i > MAX_LIMIT then
|
|
7
|
+
table.insert(children, "... (Terpotong, " .. tostring(#allChildren - MAX_LIMIT) .. " children lainnya tidak ditampilkan)")
|
|
8
|
+
break
|
|
9
|
+
end
|
|
4
10
|
table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
|
|
5
11
|
end
|
|
6
12
|
return { status = "success", result = children }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local maxDepth = 2
|
|
3
|
+
if data and data ~= "" then
|
|
4
|
+
pcall(function() maxDepth = tonumber(data) or 2 end)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local totalNodes = 0
|
|
8
|
+
local HARD_LIMIT = 2000 -- safeguard
|
|
9
|
+
|
|
10
|
+
local hardLimitReached = false
|
|
11
|
+
|
|
12
|
+
local function traverse(inst, currentDepth)
|
|
13
|
+
if hardLimitReached then return nil end
|
|
14
|
+
totalNodes = totalNodes + 1
|
|
15
|
+
local node = { name = inst.Name, className = inst.ClassName }
|
|
16
|
+
|
|
17
|
+
if totalNodes >= HARD_LIMIT then
|
|
18
|
+
hardLimitReached = true
|
|
19
|
+
return { name = "... (terpotong, melebihi limit " .. HARD_LIMIT .. " nodes)", className = "Warning" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if currentDepth >= maxDepth then
|
|
23
|
+
local childCount = #inst:GetChildren()
|
|
24
|
+
if childCount > 0 then
|
|
25
|
+
node.children = "... (" .. childCount .. " children tersembunyi)"
|
|
26
|
+
end
|
|
27
|
+
return node
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
local children = inst:GetChildren()
|
|
31
|
+
if #children > 0 then
|
|
32
|
+
node.children = {}
|
|
33
|
+
for _, child in ipairs(children) do
|
|
34
|
+
local childNode = traverse(child, currentDepth + 1)
|
|
35
|
+
if childNode then
|
|
36
|
+
table.insert(node.children, childNode)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
return node
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
local tree = traverse(targetInstance, 0)
|
|
45
|
+
return { status = "success", result = tree }
|
|
46
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
return function(targetInstance, data, ctx, targetPath)
|
|
2
2
|
local InsertService = game:GetService("InsertService")
|
|
3
|
-
local
|
|
3
|
+
local rawId = tostring(data or ""):gsub("rbxassetid://", ""):gsub("%s+", "")
|
|
4
|
+
local assetId = tonumber(rawId)
|
|
4
5
|
if not assetId then
|
|
5
6
|
return { status = "error", error = "Asset ID harus berupa angka yang valid." }
|
|
6
7
|
end
|
|
@@ -10,8 +11,12 @@ return function(targetInstance, data, ctx, targetPath)
|
|
|
10
11
|
end)
|
|
11
12
|
|
|
12
13
|
if success and model then
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
if targetInstance == game or targetInstance.ClassName == "DataModel" then
|
|
15
|
+
model.Parent = game.Workspace
|
|
16
|
+
else
|
|
17
|
+
model.Parent = targetInstance
|
|
18
|
+
end
|
|
19
|
+
return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
|
|
15
20
|
else
|
|
16
21
|
return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
|
|
17
22
|
end
|