@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.
@@ -29,20 +29,30 @@
29
29
  local newSuccess, newInst = pcall(function()
30
30
  local inst = Instance.new(config.ClassName)
31
31
  inst.Parent = targetInstance
32
-
33
- if config.Properties then
34
- for prop, val in pairs(config.Properties) do
35
- pcall(function() inst[prop] = val end)
36
- end
37
- end
38
32
  return inst
39
33
  end)
40
34
 
41
- if newSuccess then
42
- return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath }
43
- else
44
- return { status = "error", error = "Gagal membuat Instance. Pastikan ClassName valid." }
35
+ if not newSuccess then
36
+ return { status = "error", error = "Gagal membuat Instance: " .. tostring(newInst) }
37
+ end
38
+
39
+ local propResultMsg = ""
40
+ if config.Properties and next(config.Properties) then
41
+ local setPropsFunc = require(script.Parent:FindFirstChild("set_properties"))
42
+ if setPropsFunc then
43
+ local propData = ctx.HttpService:JSONEncode(config.Properties)
44
+ local propRes = setPropsFunc(newInst, propData, ctx, "")
45
+ if propRes.status == "error" then
46
+ propResultMsg = " (Namun ada error properti: " .. propRes.result .. ")"
47
+ elseif propRes.result then
48
+ propResultMsg = " (" .. propRes.result .. ")"
49
+ end
50
+ else
51
+ propResultMsg = " (Gagal meload set_properties)"
52
+ end
45
53
  end
54
+
55
+ return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath .. propResultMsg }
46
56
  end
47
57
  ]]></ProtectedString>
48
58
  </Properties>
@@ -58,6 +68,10 @@ end
58
68
  <int64 name="SourceAssetId">-1</int64>
59
69
  <BinaryString name="Tags"></BinaryString>
60
70
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
71
+ if targetInstance == game or targetInstance.Parent == game then
72
+ return { status = "error", error = "Tidak dapat menghapus root DataModel atau Service utama!" }
73
+ end
74
+
61
75
  -- Guardrail: Cegah penghapusan Service utama
62
76
  local protectedServices = {
63
77
  "Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
@@ -78,6 +92,11 @@ end
78
92
  local className = targetInstance.ClassName
79
93
  local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
80
94
 
95
+ local childrenCount = #targetInstance:GetDescendants()
96
+ if childrenCount > 10 and data ~= "force" then
97
+ return { status = "error", error = "GUARDRAIL: Instance '" .. name .. "' memiliki " .. childrenCount .. " descendants. Jika Anda yakin ingin menghapus seluruhnya beserta isinya, sertakan data='force'." }
98
+ end
99
+
81
100
  local success, err = pcall(function()
82
101
  targetInstance:Destroy()
83
102
  end)
@@ -113,7 +132,7 @@ end
113
132
  local wrappedCode = "return function()\n" .. code .. "\nend"
114
133
 
115
134
  local moduleScript = Instance.new("ModuleScript")
116
- moduleScript.Name = "AI_Execute_Temp"
135
+ moduleScript.Name = "AI_Execute_" .. HttpService:GenerateGUID(false):gsub("-", "")
117
136
  moduleScript.Source = wrappedCode
118
137
 
119
138
  -- Taruh di ReplicatedStorage sementara agar bisa di-require
@@ -124,10 +143,17 @@ end
124
143
  local isFinished = false
125
144
 
126
145
  -- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
127
- task.spawn(function()
146
+ local logs = {}
147
+
148
+ local execThread = task.spawn(function()
128
149
  local reqSuccess, func = pcall(function() return require(moduleScript) end)
129
150
  if reqSuccess and type(func) == "function" then
151
+ local LogService = game:GetService("LogService")
152
+ local connection = LogService.MessageOut:Connect(function(msg, msgType)
153
+ table.insert(logs, msg)
154
+ end)
130
155
  local execSuccess, res = pcall(func)
156
+ pcall(function() connection:Disconnect() end)
131
157
  if execSuccess then
132
158
  resultData = res
133
159
  else
@@ -145,20 +171,31 @@ end
145
171
  timeWaited = timeWaited + task.wait()
146
172
  end
147
173
 
174
+
175
+ if not isFinished then
176
+ pcall(function() task.cancel(execThread) end)
177
+ end
178
+
148
179
  -- Pembersihan (Wajib dihancurkan)
149
180
  pcall(function() moduleScript:Destroy() end)
150
181
 
151
182
  if not isFinished then
152
- return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
183
+ 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." }
153
184
  end
154
185
 
155
186
  if execError then
156
- return { status = "error", error = execError }
187
+ return { status = "error", error = execError, logs = #logs > 0 and logs or nil }
157
188
  end
158
189
 
190
+ local returnPayload = { status = "success" }
191
+ if #logs > 0 then
192
+ returnPayload.logs = logs
193
+ end
194
+
159
195
  -- Cegah return nil error
160
196
  if resultData == nil then
161
- return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
197
+ returnPayload.result = "Eksekusi berhasil (Tidak ada nilai kembalian)."
198
+ return returnPayload
162
199
  end
163
200
 
164
201
  -- JSON Encode untuk mencegah raw Instance return crash
@@ -167,7 +204,8 @@ end
167
204
  end)
168
205
 
169
206
  if serializeSuccess then
170
- return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
207
+ returnPayload.result = HttpService:JSONDecode(jsonRes) -- decode kembali agar format aslinya diteruskan oleh router
208
+ return returnPayload
171
209
  else
172
210
  return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
173
211
  end
@@ -193,7 +231,8 @@ end
193
231
 
194
232
  local fillSuccess, err = pcall(function()
195
233
  local terrain = game.Workspace.Terrain
196
- local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
234
+ local matOk, materialEnum = pcall(function() return Enum.Material[config.Material] end)
235
+ if not matOk then materialEnum = Enum.Material.Grass end
197
236
 
198
237
  local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
199
238
  local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
@@ -223,11 +262,76 @@ end
223
262
  <BinaryString name="Tags"></BinaryString>
224
263
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
225
264
  local children = {}
226
- for _, child in ipairs(targetInstance:GetChildren()) do
265
+ local allChildren = targetInstance:GetChildren()
266
+ local MAX_LIMIT = 200
267
+ for i, child in ipairs(allChildren) do
268
+ if i > MAX_LIMIT then
269
+ table.insert(children, "... (Terpotong, " .. tostring(#allChildren - MAX_LIMIT) .. " children lainnya tidak ditampilkan)")
270
+ break
271
+ end
227
272
  table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
228
273
  end
229
274
  return { status = "success", result = children }
230
275
  end
276
+ ]]></ProtectedString>
277
+ </Properties>
278
+ </Item>
279
+ <Item class="ModuleScript" referent="RBX_get_full_tree">
280
+ <Properties>
281
+ <BinaryString name="AttributesSerialize"></BinaryString>
282
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
283
+ <bool name="DefinesCapabilities">false</bool>
284
+ <bool name="Disabled">false</bool>
285
+ <Content name="LinkedSource"><null></null></Content>
286
+ <string name="Name">get_full_tree</string>
287
+ <int64 name="SourceAssetId">-1</int64>
288
+ <BinaryString name="Tags"></BinaryString>
289
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
290
+ local maxDepth = 2
291
+ if data and data ~= "" then
292
+ pcall(function() maxDepth = tonumber(data) or 2 end)
293
+ end
294
+
295
+ local totalNodes = 0
296
+ local HARD_LIMIT = 2000 -- safeguard
297
+
298
+ local hardLimitReached = false
299
+
300
+ local function traverse(inst, currentDepth)
301
+ if hardLimitReached then return nil end
302
+ totalNodes = totalNodes + 1
303
+ local node = { name = inst.Name, className = inst.ClassName }
304
+
305
+ if totalNodes >= HARD_LIMIT then
306
+ hardLimitReached = true
307
+ return { name = "... (terpotong, melebihi limit " .. HARD_LIMIT .. " nodes)", className = "Warning" }
308
+ end
309
+
310
+ if currentDepth >= maxDepth then
311
+ local childCount = #inst:GetChildren()
312
+ if childCount > 0 then
313
+ node.children = "... (" .. childCount .. " children tersembunyi)"
314
+ end
315
+ return node
316
+ end
317
+
318
+ local children = inst:GetChildren()
319
+ if #children > 0 then
320
+ node.children = {}
321
+ for _, child in ipairs(children) do
322
+ local childNode = traverse(child, currentDepth + 1)
323
+ if childNode then
324
+ table.insert(node.children, childNode)
325
+ end
326
+ end
327
+ end
328
+
329
+ return node
330
+ end
331
+
332
+ local tree = traverse(targetInstance, 0)
333
+ return { status = "success", result = tree }
334
+ end
231
335
  ]]></ProtectedString>
232
336
  </Properties>
233
337
  </Item>
@@ -348,7 +452,8 @@ end
348
452
  <BinaryString name="Tags"></BinaryString>
349
453
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
350
454
  local InsertService = game:GetService("InsertService")
351
- local assetId = tonumber(data)
455
+ local rawId = tostring(data or ""):gsub("rbxassetid://", ""):gsub("%s+", "")
456
+ local assetId = tonumber(rawId)
352
457
  if not assetId then
353
458
  return { status = "error", error = "Asset ID harus berupa angka yang valid." }
354
459
  end
@@ -358,8 +463,12 @@ end
358
463
  end)
359
464
 
360
465
  if success and model then
361
- model.Parent = game.Workspace
362
- return { status = "success", result = "Aset berhasil dimasukkan ke Workspace." }
466
+ if targetInstance == game or targetInstance.ClassName == "DataModel" then
467
+ model.Parent = game.Workspace
468
+ else
469
+ model.Parent = targetInstance
470
+ end
471
+ return { status = "success", result = "Aset berhasil dimasukkan ke " .. model.Parent.Name .. "." }
363
472
  else
364
473
  return { status = "error", error = "Gagal memuat aset. Pastikan Asset ID benar dan akun/plugin Anda memiliki izin (Ownership/Public)." }
365
474
  end
@@ -411,6 +520,7 @@ local ctx = {
411
520
  recentLogs = {},
412
521
  HttpService = HttpService,
413
522
  studioId = STUDIO_ID,
523
+ getInstanceFromPath = nil -- akan di-bind di bawah
414
524
  }
415
525
  local MAX_LOGS = 50
416
526
  local isRunning = false -- apakah loop aktif
@@ -423,6 +533,7 @@ local serverReachable = false -- apakah server Node.js reachable
423
533
  local RECONNECT_DELAY_INITIAL = 1.5
424
534
  local RECONNECT_DELAY_MAX = 1.5
425
535
  local reconnectDelay = RECONNECT_DELAY_INITIAL
536
+ local currentLoopId = nil
426
537
 
427
538
  -- ── Skills (ModuleScript) ─────────────────────────────────────────────────────
428
539
  local skills = {}
@@ -502,21 +613,41 @@ LogService.MessageOut:Connect(addSystemLog)
502
613
 
503
614
  -- ── Helper: Resolve path ke Instance ─────────────────────────────────────────
504
615
  local function getInstanceFromPath(pathString)
616
+ if type(pathString) ~= "string" or pathString == "" then return nil end
505
617
  local parts = string.split(pathString, ".")
618
+ if #parts == 0 then return nil end
506
619
  local current = game
507
620
  if parts[1] == "game" then table.remove(parts, 1) end
508
621
  pcall(function()
509
622
  local svc = game:GetService(parts[1])
510
623
  if svc then current = svc; table.remove(parts, 1) end
511
624
  end)
512
- for _, part in ipairs(parts) do
625
+ local i = 1
626
+ while i <= #parts do
627
+ local part = parts[i]
513
628
  local found = current:FindFirstChild(part)
629
+
630
+ -- Jika tidak ketemu, coba gabungkan dengan part berikutnya (untuk nama yang mengandung titik)
631
+ local combinedPart = part
632
+ local lookAhead = i + 1
633
+ while not found and lookAhead <= #parts do
634
+ combinedPart = combinedPart .. "." .. parts[lookAhead]
635
+ found = current:FindFirstChild(combinedPart)
636
+ if found then
637
+ i = lookAhead
638
+ end
639
+ lookAhead = lookAhead + 1
640
+ end
641
+
514
642
  if not found then return nil end
515
643
  current = found
644
+ i = i + 1
516
645
  end
517
646
  return current
518
647
  end
519
648
 
649
+ ctx.getInstanceFromPath = getInstanceFromPath
650
+
520
651
  -- ── Proses Task dari Server ───────────────────────────────────────────────────
521
652
  local function processTask(taskData)
522
653
  if not taskData or not taskData.id or not taskData.command then return nil end
@@ -527,7 +658,11 @@ local function processTask(taskData)
527
658
 
528
659
  local targetInstance = getInstanceFromPath(taskData.target)
529
660
  if not targetInstance then
530
- return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
661
+ if taskData.target == "" or taskData.target == "nil" then
662
+ targetInstance = game
663
+ else
664
+ return { id = taskData.id, status = "error", error = "Target path tidak ditemukan: " .. tostring(taskData.target) }
665
+ end
531
666
  end
532
667
 
533
668
  local skillFunc = skills[taskData.command]
@@ -535,8 +670,16 @@ local function processTask(taskData)
535
670
  return { id = taskData.id, status = "error", error = "Command tidak dikenali: " .. taskData.command }
536
671
  end
537
672
 
538
- local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
539
- result.id = taskData.id
673
+ local ok, result = pcall(function()
674
+ return skillFunc(targetInstance, taskData.data, ctx, taskData.target)
675
+ end)
676
+
677
+ if not ok then
678
+ result = { status = "error", error = "Internal Plugin Error: " .. tostring(result) }
679
+ end
680
+
681
+ result = result or { status = "success", result = "Eksekusi selesai (Tidak ada return value)" }
682
+ result.id = taskData.id
540
683
 
541
684
  if ui then
542
685
  local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
@@ -568,24 +711,11 @@ local function sendResponse(taskId, resultData)
568
711
  end)
569
712
  end
570
713
 
571
- -- ── Update UI berdasarkan state ───────────────────────────────────────────────
572
- local function updateUIStatus()
573
- if not ui then return end
574
- if not serverReachable then
575
- ui:setStatus("waiting")
576
- elseif mcpAgentActive then
577
- ui:setStatus("connected")
578
- else
579
- -- Server reachable tapi tidak ada AI agent
580
- -- Tampilkan sebagai connected (karena server sse bridge aktif)
581
- ui:setStatus("server_connected")
582
- end
583
- end
584
-
585
714
  -- ── Proses satu event dari SSE stream ────────────────────────────────────────
586
715
  local function handleSSELine(line)
587
716
  -- Format SSE: "data: {...json...}"
588
717
  local payload = line
718
+ if line:sub(1, 6) == "event:" then return end
589
719
  if line:sub(1, 5) == "data:" then
590
720
  payload = line:sub(6):gsub("^%s+", ""):gsub("%s+$", "")
591
721
  end
@@ -607,8 +737,12 @@ local function handleSSELine(line)
607
737
  local changed = (mcpAgentActive ~= newActive)
608
738
  mcpAgentActive = newActive
609
739
  if ui then
610
- -- Jaga status tetap Connected selama bridge server hidup
611
- ui:setStatus("connected")
740
+ if mcpAgentActive then
741
+ ui:setStatus("connected")
742
+ else
743
+ ui:setStatus("server_connected")
744
+ end
745
+
612
746
  if newActive and changed then
613
747
  ui:addLog("[System] ✅ IDE Agent (Antigravity) aktif.", Color3.fromRGB(100, 255, 100))
614
748
  elseif not newActive and changed then
@@ -653,13 +787,16 @@ function startConnectionLoop()
653
787
  isManualDisconnect = false
654
788
  reconnectDelay = RECONNECT_DELAY_INITIAL
655
789
 
790
+ local loopId = HttpService:GenerateGUID(false)
791
+ currentLoopId = loopId
792
+
656
793
  if ui then
657
794
  ui:addLog("[System] NVStudio MCP v2.1.8 — Menghubungkan ke server...", Color3.fromRGB(200, 200, 200))
658
795
  end
659
796
 
660
797
  local consecutiveErrors = 0
661
798
 
662
- while isRunning and not isManualDisconnect do
799
+ while isRunning and not isManualDisconnect and currentLoopId == loopId do
663
800
  -- Stream / polling snapshot dari server
664
801
  local streamOk, streamRaw = pcall(function()
665
802
  return HttpService:GetAsync(
@@ -675,7 +812,7 @@ function startConnectionLoop()
675
812
  if not serverReachable then
676
813
  serverReachable = true
677
814
  if ui then
678
- ui:setStatus("connected")
815
+ ui:setStatus(mcpAgentActive and "connected" or "server_connected")
679
816
  ui:addLog("[System] ✅ Server MCP terhubung & siap.", Color3.fromRGB(100, 255, 150))
680
817
  end
681
818
  end
@@ -765,6 +902,10 @@ return function(targetInstance, data, ctx, targetPath)
765
902
  local replacementContent = payload.replacement_content
766
903
  local allowMultiple = payload.allow_multiple or false
767
904
 
905
+ if targetContent == "" then
906
+ return { status = "error", error = "target_content tidak boleh berupa string kosong." }
907
+ end
908
+
768
909
  -- Escape magic characters for string.gsub if needed, or use string.find manually.
769
910
  -- string.gsub doesn't do plain text replacement directly without escaping.
770
911
  -- We'll write a plain text replace function.
@@ -786,16 +927,28 @@ return function(targetInstance, data, ctx, targetPath)
786
927
  return res, count
787
928
  end
788
929
 
789
- local _, occurrences = plainReplace(currentSource, targetContent, replacementContent)
930
+ local function countOccurrences(str, pattern)
931
+ local count = 0
932
+ local startIdx = 1
933
+ while true do
934
+ local findStart, findEnd = string.find(str, pattern, startIdx, true)
935
+ if not findStart then break end
936
+ count = count + 1
937
+ startIdx = findEnd + 1
938
+ if count > 1 and not allowMultiple then break end
939
+ end
940
+ return count
941
+ end
942
+
943
+ local occurrences = countOccurrences(currentSource, targetContent)
790
944
 
791
945
  if occurrences == 0 then
792
946
  return { status = "error", error = "target_content tidak ditemukan di dalam script." }
793
947
  elseif occurrences > 1 and not allowMultiple then
794
- return { status = "error", error = "target_content ditemukan " .. occurrences .. " kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
948
+ return { status = "error", error = "target_content ditemukan lebih dari 1 kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
795
949
  end
796
950
 
797
951
  newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
798
- affectedLines = affectedLines -- not exact line count, but occurrences count
799
952
 
800
953
  -- Mode 2: Line Range Replace
801
954
  elseif payload.start_line and payload.end_line and payload.replacement_content then
@@ -825,7 +978,11 @@ return function(targetInstance, data, ctx, targetPath)
825
978
  end
826
979
 
827
980
  -- Simpan backup snapshot sebelum mengaplikasikan patch
828
- ctx.scriptHistory[targetPath] = currentSource
981
+ if type(ctx.scriptHistory[targetPath]) ~= "table" then
982
+ ctx.scriptHistory[targetPath] = {}
983
+ end
984
+ table.insert(ctx.scriptHistory[targetPath], currentSource)
985
+ if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
829
986
 
830
987
  -- Terapkan perubahan
831
988
  targetInstance.Source = newSource
@@ -851,10 +1008,16 @@ end
851
1008
  <BinaryString name="Tags"></BinaryString>
852
1009
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
853
1010
  if targetInstance:IsA("LuaSourceContainer") then
854
- local prevSource = ctx.scriptHistory[targetPath]
855
- if prevSource then
1011
+ local historyStack = ctx.scriptHistory[targetPath]
1012
+ if historyStack and type(historyStack) == "table" and #historyStack > 0 then
1013
+ local prevSource = table.remove(historyStack)
856
1014
  targetInstance.Source = prevSource
857
- return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
1015
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya. Sisa backup: " .. #historyStack }
1016
+ elseif type(historyStack) == "string" then
1017
+ -- Kompatibilitas mundur dengan data lama (sebelum FEAT-04)
1018
+ targetInstance.Source = historyStack
1019
+ ctx.scriptHistory[targetPath] = nil
1020
+ return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback (versi tunggal/legacy)." }
858
1021
  else
859
1022
  return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
860
1023
  end
@@ -876,10 +1039,10 @@ end
876
1039
  <int64 name="SourceAssetId">-1</int64>
877
1040
  <BinaryString name="Tags"></BinaryString>
878
1041
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
879
- local query = tostring(data)
880
- if not query or query == "" then
1042
+ if not data or data == "" or data == "nil" then
881
1043
  return { status = "error", error = "Query pencarian tidak boleh kosong." }
882
1044
  end
1045
+ local query = tostring(data)
883
1046
 
884
1047
  local matches = {}
885
1048
  local matchCount = 0
@@ -890,7 +1053,9 @@ end
890
1053
  game:GetService("ServerScriptService"),
891
1054
  game:GetService("ReplicatedStorage"),
892
1055
  game:GetService("StarterPlayer"),
893
- game:GetService("StarterGui")
1056
+ game:GetService("StarterGui"),
1057
+ game:GetService("ServerStorage"),
1058
+ game:GetService("Lighting")
894
1059
  }
895
1060
 
896
1061
  local function searchInScript(scriptObj)
@@ -920,19 +1085,32 @@ end
920
1085
  return false
921
1086
  end
922
1087
 
923
- for _, service in ipairs(searchableServices) do
924
- pcall(function()
925
- local descendants = service:GetDescendants()
1088
+ if targetInstance and targetInstance ~= game then
1089
+ local descendants = {}
1090
+ pcall(function() descendants = targetInstance:GetDescendants() end)
1091
+ for _, desc in ipairs(descendants) do
1092
+ if matchCount >= MAX_MATCHES then break end
1093
+ if desc:IsA("LuaSourceContainer") then
1094
+ if searchInScript(desc) then
1095
+ break
1096
+ end
1097
+ end
1098
+ end
1099
+ else
1100
+ for _, service in ipairs(searchableServices) do
1101
+ local descendants = {}
1102
+ pcall(function() descendants = service:GetDescendants() end)
926
1103
  for _, desc in ipairs(descendants) do
1104
+ if matchCount >= MAX_MATCHES then break end
927
1105
  if desc:IsA("LuaSourceContainer") then
928
1106
  if searchInScript(desc) then
929
1107
  break
930
1108
  end
931
1109
  end
932
1110
  end
933
- end)
934
- if matchCount >= MAX_MATCHES then
935
- break
1111
+ if matchCount >= MAX_MATCHES then
1112
+ break
1113
+ end
936
1114
  end
937
1115
  end
938
1116
 
@@ -952,16 +1130,35 @@ end
952
1130
  <int64 name="SourceAssetId">-1</int64>
953
1131
  <BinaryString name="Tags"></BinaryString>
954
1132
  <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
1133
+ if not data or data == "" or data == "nil" then
1134
+ return { status = "error", error = "Query pencarian tidak boleh kosong." }
1135
+ end
955
1136
  local searchName = tostring(data)
956
1137
  local results = {}
957
1138
 
958
1139
  local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
1140
+ local lowerSearch = string.lower(searchName)
959
1141
 
960
- for _, service in ipairs(searchableServices) do
961
- for _, desc in ipairs(service:GetDescendants()) do
962
- if desc.Name == searchName then
963
- table.insert(results, desc:GetFullName())
964
- end
1142
+ local nodesChecked = 0
1143
+ local function search(inst)
1144
+ nodesChecked = nodesChecked + 1
1145
+ if nodesChecked % 1000 == 0 then task.wait() end
1146
+ if string.find(inst.Name:lower(), searchName:lower(), 1, true) then
1147
+ table.insert(results, inst:GetFullName() .. " (" .. inst.ClassName .. ")")
1148
+ end
1149
+ if #results >= 50 then return end
1150
+ for _, child in ipairs(inst:GetChildren()) do
1151
+ search(child)
1152
+ if #results >= 50 then break end
1153
+ end
1154
+ end
1155
+
1156
+ if targetInstance and targetInstance ~= game then
1157
+ search(targetInstance)
1158
+ else
1159
+ for _, service in ipairs(searchableServices) do
1160
+ search(service)
1161
+ if #results >= 50 then break end
965
1162
  end
966
1163
  end
967
1164
 
@@ -971,6 +1168,92 @@ end
971
1168
  return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
972
1169
  end
973
1170
  end
1171
+ ]]></ProtectedString>
1172
+ </Properties>
1173
+ </Item>
1174
+ <Item class="ModuleScript" referent="RBX_set_properties">
1175
+ <Properties>
1176
+ <BinaryString name="AttributesSerialize"></BinaryString>
1177
+ <SecurityCapabilities name="Capabilities">0</SecurityCapabilities>
1178
+ <bool name="DefinesCapabilities">false</bool>
1179
+ <bool name="Disabled">false</bool>
1180
+ <Content name="LinkedSource"><null></null></Content>
1181
+ <string name="Name">set_properties</string>
1182
+ <int64 name="SourceAssetId">-1</int64>
1183
+ <BinaryString name="Tags"></BinaryString>
1184
+ <ProtectedString name="Source"><![CDATA[return function(targetInstance, data, ctx, targetPath)
1185
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
1186
+ if not success or type(config) ~= "table" then
1187
+ return { status = "error", error = "Format data JSON tidak valid untuk set_properties." }
1188
+ end
1189
+
1190
+ local applied = {}
1191
+ local errors = {}
1192
+
1193
+ for prop, val in pairs(config) do
1194
+ local ok, err = pcall(function()
1195
+ local currentVal = targetInstance[prop]
1196
+ if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
1197
+ targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
1198
+ elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
1199
+ local r, g, b = val[1], val[2], val[3]
1200
+ if r > 1 or g > 1 or b > 1 then
1201
+ targetInstance[prop] = Color3.fromRGB(r, g, b)
1202
+ else
1203
+ targetInstance[prop] = Color3.new(r, g, b)
1204
+ end
1205
+ elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
1206
+ targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
1207
+ elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
1208
+ targetInstance[prop] = CFrame.new(unpack(val))
1209
+ elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
1210
+ local enumType = tostring(currentVal.EnumType)
1211
+ local successEnum, enumValue = pcall(function() return Enum[enumType][val] end)
1212
+ if successEnum and enumValue then
1213
+ targetInstance[prop] = enumValue
1214
+ else
1215
+ error("Nilai Enum '" .. val .. "' tidak valid untuk EnumType '" .. enumType .. "'")
1216
+ end
1217
+ elseif prop == "Parent" or prop == "PrimaryPart" or string.match(prop, ".*Value$") then
1218
+ -- Evaluasi properti relasional
1219
+ if typeof(currentVal) == "Instance" or typeof(targetInstance[prop]) == "Instance" or val == nil then
1220
+ if type(val) == "string" and ctx.getInstanceFromPath then
1221
+ local resolved = ctx.getInstanceFromPath(val)
1222
+ if resolved then
1223
+ targetInstance[prop] = resolved
1224
+ else
1225
+ error("Instance target tidak ditemukan pada path: " .. val)
1226
+ end
1227
+ else
1228
+ targetInstance[prop] = val
1229
+ end
1230
+ else
1231
+ targetInstance[prop] = val
1232
+ end
1233
+ else
1234
+ targetInstance[prop] = val
1235
+ end
1236
+ end)
1237
+
1238
+ if ok then
1239
+ table.insert(applied, prop)
1240
+ else
1241
+ table.insert(errors, prop .. ": " .. tostring(err))
1242
+ end
1243
+ end
1244
+
1245
+ if #errors > 0 then
1246
+ local successMsg = ""
1247
+ if #applied > 0 then
1248
+ successMsg = "Berhasil mengubah: " .. table.concat(applied, ", ") .. ".\n"
1249
+ end
1250
+ return {
1251
+ status = #applied > 0 and "success" or "error",
1252
+ result = successMsg .. "Namun beberapa properti gagal diubah:\n" .. table.concat(errors, "\n")
1253
+ }
1254
+ end
1255
+ return { status = "success", result = "Berhasil mengubah properti: " .. table.concat(applied, ", ") }
1256
+ end
974
1257
  ]]></ProtectedString>
975
1258
  </Properties>
976
1259
  </Item>
@@ -1081,7 +1364,7 @@ function UIManager.init(pluginInstance, studioId)
1081
1364
 
1082
1365
  -- Version
1083
1366
  local versionLabel = Instance.new("TextLabel")
1084
- versionLabel.Text = "v2.1.8"
1367
+ versionLabel.Text = "v2.2.0"
1085
1368
  versionLabel.Font = Enum.Font.Gotham
1086
1369
  versionLabel.TextSize = 11
1087
1370
  versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
@@ -1405,9 +1688,9 @@ function UIManager:setStatus(stateStr)
1405
1688
  -- Server reachable, tampilkan Connected meskipun agent (IDE) belum konek via stdio
1406
1689
  self.connectBtn.Text = " Disconnect"
1407
1690
  self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
1408
- self.statusLabel.Text = "● Connected"
1409
- self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
1410
- self.termStroke.Color = Color3.fromRGB(50, 120, 60)
1691
+ self.statusLabel.Text = "● IDE Disconnected"
1692
+ self.statusLabel.TextColor3 = Color3.fromRGB(255, 180, 50)
1693
+ self.termStroke.Color = Color3.fromRGB(120, 80, 20)
1411
1694
 
1412
1695
  elseif stateStr == "connected" then
1413
1696
  self.connectBtn.Text = " Disconnect"
@@ -1458,7 +1741,11 @@ return UIManager
1458
1741
  -- hanya akan membuat AI gagal terus menerus untuk menulis script.
1459
1742
  -- Error syntax akan ditangkap secara native oleh Roblox Output.
1460
1743
 
1461
- ctx.scriptHistory[targetPath] = targetInstance.Source
1744
+ if type(ctx.scriptHistory[targetPath]) ~= "table" then
1745
+ ctx.scriptHistory[targetPath] = {}
1746
+ end
1747
+ table.insert(ctx.scriptHistory[targetPath], targetInstance.Source)
1748
+ if #ctx.scriptHistory[targetPath] > 15 then table.remove(ctx.scriptHistory[targetPath], 1) end
1462
1749
  targetInstance.Source = newCode
1463
1750
  return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
1464
1751
  else