@adiholick/nvstudio-mcp 2.1.7 → 2.2.0

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.
@@ -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 _, occurrences = plainReplace(currentSource, targetContent, replacementContent)
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 " .. occurrences .. " kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
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] = currentSource
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 prevSource = ctx.scriptHistory[targetPath]
4
- if prevSource then
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
- local query = tostring(data)
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
- for _, service in ipairs(searchableServices) do
47
- pcall(function()
48
- local descendants = service:GetDescendants()
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
- end)
57
- if matchCount >= MAX_MATCHES then
58
- break
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
- for _, service in ipairs(searchableServices) do
8
- for _, desc in ipairs(service:GetDescendants()) do
9
- if desc.Name == searchName then
10
- table.insert(results, desc:GetFullName())
11
- end
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.1.7"
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 = "● Connected"
423
- self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
424
- self.termStroke.Color = Color3.fromRGB(50, 120, 60)
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] = targetInstance.Source
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