@adiholick/nvstudio-mcp 1.0.3 → 1.1.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.
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
38
38
  tools: [
39
39
  {
40
40
  name: "call_mcp_tool",
41
- description: "ALAT MUTLAK UNTUK ROBLOX. DILARANG KERAS menggunakan PowerShell atau skrip bypass (fetch_mcp_source.js). WAJIB memanggil 'get_children' terlebih dahulu sebelum 'get_script_source' untuk memverifikasi tipe instance agar tidak terjadi error teks kosong.",
41
+ description: "ALAT MUTLAK UNTUK ROBLOX. DILARANG KERAS menggunakan PowerShell atau skrip bypass (fetch_mcp_source.js). WAJIB memanggil 'get_children' terlebih dahulu sebelum 'get_script_source'. Command: get_children, get_script_source, update_script_source, rollback_script, get_logs, create_instance, search_instance, generate_terrain, insert_asset, delete_instance, get_properties, script_grep, execute_luau, search_asset.",
42
42
  inputSchema: {
43
43
  type: "object",
44
44
  properties: {
@@ -95,7 +95,41 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
95
95
  };
96
96
  }
97
97
  }
98
- // 7. Teruskan ke antrean dan kembalikan hasilnya sebagai string JSON
98
+ // 7. Intercept command yang dieksekusi di server lokal (Node.js) alih-alih di Studio
99
+ if (command === "search_asset" && data) {
100
+ try {
101
+ const queryParams = typeof data === "string" ? JSON.parse(data) : data;
102
+ const keyword = encodeURIComponent(queryParams.keyword || "");
103
+ let assetType = "10"; // Default: Model
104
+ if (queryParams.category === "Audio") {
105
+ assetType = "9"; // Audio
106
+ }
107
+ else if (queryParams.category === "Decal" || queryParams.category === "Image") {
108
+ assetType = "13"; // Decal
109
+ }
110
+ const url = `https://catalog.roblox.com/v1/search/items?category=All&keyword=${keyword}&limit=10&itemTypes=Asset&assetTypes=${assetType}`;
111
+ const res = await fetch(url);
112
+ const jsonRes = await res.json();
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: JSON.stringify({
118
+ status: "success",
119
+ result: jsonRes.data || jsonRes
120
+ }, null, 2)
121
+ }
122
+ ]
123
+ };
124
+ }
125
+ catch (err) {
126
+ return {
127
+ content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Gagal mencari aset: " + err.message }) }],
128
+ isError: true
129
+ };
130
+ }
131
+ }
132
+ // 8. Teruskan ke antrean dan kembalikan hasilnya sebagai string JSON
99
133
  try {
100
134
  if (command === "update_script_source" && data) {
101
135
  backupScript(target, String(data));
@@ -130,22 +164,24 @@ async function main() {
130
164
  const transport = new stdio_js_1.StdioServerTransport();
131
165
  await server.connect(transport);
132
166
  console.error("[nvstudio-mcp] MCP Server aktif melalui StdioServerTransport.");
167
+ let isShuttingDown = false;
133
168
  const shutdown = () => {
169
+ if (isShuttingDown)
170
+ return;
171
+ isShuttingDown = true;
134
172
  console.error("[nvstudio-mcp] Koneksi ditutup oleh IDE. Mematikan server...");
135
173
  process.exit(0);
136
174
  };
137
- // Tangani penutupan koneksi
175
+ // Tangani penutupan koneksi stdin (ketika IDE menutup pipe)
138
176
  process.stdin.on('close', shutdown);
139
177
  process.stdin.on('end', shutdown);
140
- // Fallback: Cek apakah parent process (IDE) masih hidup secara berkala
178
+ process.stdin.on('error', shutdown);
179
+ // Fallback khusus Windows: cek apakah stdin masih readable secara berkala
180
+ // process.kill(ppid, 0) tidak reliable di Windows, jadi kita pakai cara ini
141
181
  setInterval(() => {
142
- try {
143
- // process.kill dengan signal 0 hanya mengecek keberadaan proses tanpa membunuhnya
144
- process.kill(process.ppid, 0);
145
- }
146
- catch (e) {
147
- console.error("[nvstudio-mcp] Parent process (IDE) sudah mati. Mematikan server untuk menghindari zombie connection...");
148
- process.exit(0);
182
+ if (!process.stdin.readable) {
183
+ console.error("[nvstudio-mcp] stdin tidak readable lagi. IDE kemungkinan sudah ditutup.");
184
+ shutdown();
149
185
  }
150
186
  }, 3000);
151
187
  }
@@ -22,17 +22,58 @@ function addLog(io, type, message) {
22
22
  type,
23
23
  message,
24
24
  };
25
- activityLogs.unshift(log); // newest first
25
+ activityLogs.unshift(log);
26
26
  if (activityLogs.length > 200)
27
27
  activityLogs.pop();
28
28
  io.emit('log', log);
29
29
  io.emit('stats', { totalTasks, successCount, errorCount, sessionStart });
30
30
  }
31
+ /**
32
+ * Membunuh proses zombie yang masih menyandera port tertentu.
33
+ * Ini menyelesaikan masalah EADDRINUSE yang terjadi setelah restart IDE.
34
+ */
35
+ function killZombieOnPort(port) {
36
+ try {
37
+ if (process.platform === 'win32') {
38
+ const output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf8', timeout: 5000 });
39
+ const lines = output.trim().split('\n');
40
+ for (const line of lines) {
41
+ const parts = line.trim().split(/\s+/);
42
+ const pid = parts[parts.length - 1];
43
+ if (pid && pid !== '0' && pid !== String(process.pid)) {
44
+ try {
45
+ (0, child_process_1.execSync)(`taskkill /F /PID ${pid}`, { encoding: 'utf8', timeout: 5000 });
46
+ console.error(`[Bridge] Zombie process (PID ${pid}) pada port ${port} berhasil dihentikan.`);
47
+ }
48
+ catch { /* ignore if already dead */ }
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ else {
54
+ // macOS / Linux
55
+ const output = (0, child_process_1.execSync)(`lsof -ti :${port}`, { encoding: 'utf8', timeout: 5000 });
56
+ const pids = output.trim().split('\n');
57
+ for (const pid of pids) {
58
+ if (pid && pid !== String(process.pid)) {
59
+ try {
60
+ (0, child_process_1.execSync)(`kill -9 ${pid}`);
61
+ }
62
+ catch { /* ignore */ }
63
+ }
64
+ }
65
+ return true;
66
+ }
67
+ }
68
+ catch {
69
+ // Tidak ada proses yang menyandera port — ini normal
70
+ return false;
71
+ }
72
+ }
31
73
  function startBridgeServer(port = 3055) {
32
74
  const app = (0, express_1.default)();
33
75
  const httpServer = (0, http_1.createServer)(app);
34
76
  const io = new socket_io_1.Server(httpServer, { cors: { origin: "*" } });
35
- // FIX: Menaikkan limit ke 50mb agar file .Source (Lua) yang besar tidak melempar error 413 Payload Too Large
36
77
  app.use(express_1.default.json({ limit: '50mb' }));
37
78
  app.use(express_1.default.static(path_1.default.join(__dirname, '../public')));
38
79
  let dashboardOpened = false;
@@ -89,7 +130,25 @@ function startBridgeServer(port = 3055) {
89
130
  (0, task_queue_1.resolvePendingTask)(id, status, result, error);
90
131
  res.json({ message: "Response diterima" });
91
132
  });
92
- httpServer.listen(port, () => {
93
- console.error(`[Bridge] Dashboard visual & server aktif di http://localhost:${port}`);
94
- });
133
+ // ── ANTI-ZOMBIE: Coba listen, jika gagal bunuh zombie lalu retry ──
134
+ function tryListen(retriesLeft = 2) {
135
+ const server = httpServer.listen(port, () => {
136
+ console.error(`[Bridge] Dashboard visual & server aktif di http://localhost:${port}`);
137
+ });
138
+ server.on('error', (err) => {
139
+ if (err.code === 'EADDRINUSE' && retriesLeft > 0) {
140
+ console.error(`[Bridge] Port ${port} sedang digunakan oleh proses zombie. Mencoba membunuh...`);
141
+ killZombieOnPort(port);
142
+ setTimeout(() => {
143
+ console.error(`[Bridge] Mencoba listen ulang pada port ${port}... (sisa ${retriesLeft - 1} percobaan)`);
144
+ tryListen(retriesLeft - 1);
145
+ }, 1500);
146
+ }
147
+ else {
148
+ console.error(`[Bridge] FATAL: Gagal mendengarkan port ${port}:`, err.message);
149
+ process.exit(1);
150
+ }
151
+ });
152
+ }
153
+ tryListen();
95
154
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@adiholick/nvstudio-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Local Bridge Server MCP for Roblox Studio",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
7
7
  "files": [
8
8
  "dist",
9
- "public"
9
+ "public",
10
+ "studio-plugin"
10
11
  ],
11
12
  "scripts": {
12
13
  "build": "tsc",
@@ -0,0 +1,291 @@
1
+ local HttpService = game:GetService("HttpService")
2
+ local LogService = game:GetService("LogService")
3
+
4
+ local TASK_URL = "http://localhost:3055/api/tasks"
5
+ local RESPONSE_URL = "http://localhost:3055/api/response"
6
+
7
+ -- Generate Unique Studio ID (menghindari bentrok jika 2 Studio terbuka)
8
+ local STUDIO_ID = HttpService:GenerateGUID(false)
9
+
10
+ -- Import UI Manager (Procedural Dashboard)
11
+ local uiModule = script.Parent:FindFirstChild("ui_manager")
12
+ local ui = nil
13
+
14
+ -- Membangun Tombol Toolbar (Toggle)
15
+ local toolbar = plugin:CreateToolbar("NVStudio MCP")
16
+ local toggleBtn = toolbar:CreateButton("Dashboard", "Buka Dashboard NVStudio MCP", "rbxassetid://82696893597722")
17
+
18
+ -- Global State & Context AI
19
+ local ctx = {
20
+ scriptHistory = {},
21
+ recentLogs = {},
22
+ HttpService = HttpService,
23
+ studioId = STUDIO_ID
24
+ }
25
+ local MAX_LOGS = 50
26
+ local isConnected = false
27
+ local autoConnect = plugin:GetSetting("AutoConnect") or false
28
+ local isPolling = false
29
+ local isClickDebounce = false -- Mencegah rapid double-click
30
+
31
+ -- Deklarasi fungsi polling di awal agar bisa dipanggil oleh UI
32
+ local startPolling
33
+ local stopPolling
34
+
35
+ -- Inisialisasi UI jika modul ditemukan
36
+ if uiModule then
37
+ local UIManager = require(uiModule)
38
+ ui = UIManager.init(plugin, STUDIO_ID)
39
+
40
+ -- Sync Initial State ke UI
41
+ ui:setAutoConnectState(autoConnect)
42
+ ui:setStatus("disconnected")
43
+
44
+ -- Event Toggle GUI Panel
45
+ toggleBtn.Click:Connect(function()
46
+ ui:toggleVisibility()
47
+ end)
48
+
49
+ -- Event Tombol Connect (dengan debounce untuk mencegah double-click)
50
+ ui.connectBtn.MouseButton1Click:Connect(function()
51
+ if isClickDebounce then return end
52
+ isClickDebounce = true
53
+
54
+ if not isConnected then
55
+ -- CONNECT (manual) — reset manual disconnect flag agar auto-detect bisa bekerja
56
+ isManualDisconnect = false
57
+ isConnected = true
58
+ ui:setStatus("waiting")
59
+ ui:addLog("[System] Koneksi manual dimulai. Mencari AI Server (Waiting)...", Color3.fromRGB(255, 220, 100))
60
+ startPolling()
61
+ else
62
+ -- DISCONNECT — hentikan polling dan tandai manual disconnect
63
+ stopPolling()
64
+ ui:setStatus("disconnected")
65
+ ui:addLog("[System] Koneksi dihentikan oleh pengguna.", Color3.fromRGB(255, 100, 100))
66
+ end
67
+
68
+ task.delay(0.3, function()
69
+ isClickDebounce = false
70
+ end)
71
+ end)
72
+
73
+ -- Event Custom Checkbox (Auto-Connect)
74
+ ui.autoConnectBtn.MouseButton1Click:Connect(function()
75
+ autoConnect = not autoConnect
76
+ plugin:SetSetting("AutoConnect", autoConnect)
77
+ ui:setAutoConnectState(autoConnect)
78
+ ui:addLog("[System] Preferensi Auto-Connect disimpan: " .. tostring(autoConnect), Color3.fromRGB(200, 200, 255))
79
+ end)
80
+ else
81
+ -- Fallback aman jika pengguna lupa meng-copy ui_manager.lua
82
+ toggleBtn.Click:Connect(function()
83
+ print("[NVStudio MCP] ERROR: Modul 'ui_manager' tidak ditemukan di dalam Folder. GUI Gagal dimuat!")
84
+ end)
85
+ end
86
+
87
+ -- Handler Output Studio (Untuk AI dan GUI)
88
+ local function addSystemLog(msg, msgType)
89
+ local color = Color3.fromRGB(200, 200, 200)
90
+ local prefix = "[INFO] "
91
+ if msgType == Enum.MessageType.MessageError then
92
+ prefix = "[ERROR] "
93
+ color = Color3.fromRGB(255, 100, 100)
94
+ elseif msgType == Enum.MessageType.MessageWarning then
95
+ prefix = "[WARN] "
96
+ color = Color3.fromRGB(255, 200, 100)
97
+ end
98
+
99
+ local fullMsg = prefix .. msg
100
+
101
+ -- 1. Simpan di memori untuk disedot AI
102
+ table.insert(ctx.recentLogs, fullMsg)
103
+ if #ctx.recentLogs > MAX_LOGS then
104
+ table.remove(ctx.recentLogs, 1)
105
+ end
106
+
107
+ -- 2. Cetak ke Terminal GUI Plugin
108
+ if ui then
109
+ ui:addLog(fullMsg, color)
110
+ end
111
+ end
112
+
113
+ -- Ikat listener output
114
+ LogService.MessageOut:Connect(addSystemLog)
115
+
116
+ -- Memuat semua module skills secara dinamis
117
+ local skills = {}
118
+ for _, module in ipairs(script.Parent:GetChildren()) do
119
+ if module:IsA("ModuleScript") and module.Name ~= "ui_manager" then
120
+ skills[module.Name] = require(module)
121
+ end
122
+ end
123
+
124
+ -- Fungsi Helper Pathing
125
+ local function getInstanceFromPath(pathString)
126
+ local parts = string.split(pathString, ".")
127
+ local current = game
128
+
129
+ if parts[1] == "game" then table.remove(parts, 1) end
130
+
131
+ for _, part in ipairs(parts) do
132
+ local found = current:FindFirstChild(part)
133
+ if not found then return nil end
134
+ current = found
135
+ end
136
+ return current
137
+ end
138
+
139
+ -- Memproses Task Eksekusi dari AI
140
+ local function processTask(taskData)
141
+ if not taskData or not taskData.id or not taskData.command then return nil end
142
+
143
+ if ui then
144
+ ui:addLog("[AI Command] Menerima instruksi: " .. taskData.command, Color3.fromRGB(150, 200, 255))
145
+ end
146
+
147
+ local targetInstance = getInstanceFromPath(taskData.target)
148
+ if not targetInstance then
149
+ return {
150
+ id = taskData.id,
151
+ status = "error",
152
+ error = "Target path tidak ditemukan: " .. taskData.target
153
+ }
154
+ end
155
+
156
+ local skillFunc = skills[taskData.command]
157
+ if not skillFunc then
158
+ return {
159
+ id = taskData.id,
160
+ status = "error",
161
+ error = "Command tidak dikenali oleh Studio: " .. taskData.command
162
+ }
163
+ end
164
+
165
+ -- Eksekusi skill AI
166
+ local result = skillFunc(targetInstance, taskData.data, ctx, taskData.target)
167
+ result.id = taskData.id
168
+
169
+ -- Cetak hasil eksekusi ke Terminal GUI
170
+ if ui then
171
+ local color = result.status == "success" and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(255, 100, 100)
172
+ ui:addLog("[AI Result] " .. tostring(result.result or result.error), color)
173
+ end
174
+
175
+ return result
176
+ end
177
+
178
+ -- Menghentikan polling dengan bersih (dipanggil oleh tombol Disconnect)
179
+ local isManualDisconnect = false -- Mencegah auto-reconnect saat user sengaja disconnect
180
+
181
+ function stopPolling()
182
+ isConnected = false
183
+ isManualDisconnect = true
184
+ -- isPolling akan menjadi false secara otomatis saat loop keluar
185
+ end
186
+
187
+ -- Background Polling Loop (Dapat dihentikan oleh user)
188
+ function startPolling()
189
+ if isPolling then return end
190
+ isPolling = true
191
+
192
+ task.spawn(function()
193
+ while isConnected do
194
+ local success, response = pcall(function()
195
+ -- Tambahkan cache-buster agar Studio tidak me-cache response HTTP
196
+ local cacheBuster = "?t=" .. tostring(os.clock())
197
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
198
+ end)
199
+
200
+ -- PENTING: Cek ulang isConnected SETELAH HTTP request selesai.
201
+ -- Jika user menekan Disconnect saat request sedang berjalan,
202
+ -- kita TIDAK boleh menimpa status UI "disconnected" yang sudah di-set.
203
+ if not isConnected then
204
+ break
205
+ end
206
+
207
+ if success and response then
208
+ -- JIKA PING SUKSES, Node.js menyala (IDE / Agen Sedang Terbuka!)
209
+ if ui then ui:setStatus("connected") end
210
+
211
+ local decodeSuccess, taskData = pcall(function()
212
+ return HttpService:JSONDecode(response)
213
+ end)
214
+
215
+ if decodeSuccess and taskData and taskData.id then
216
+ local resultData = processTask(taskData)
217
+ if resultData then
218
+ pcall(function()
219
+ local jsonResult = HttpService:JSONEncode(resultData)
220
+ HttpService:PostAsync(RESPONSE_URL, jsonResult, Enum.HttpContentType.ApplicationJson)
221
+ end)
222
+ end
223
+ end
224
+ else
225
+ -- Fetch gagal (Node.js server mati karena IDE/Antigravity/Cursor ditutup)
226
+ if ui then ui:setStatus("waiting") end
227
+ end
228
+
229
+ -- Cek lagi sebelum sleep (agar disconnect lebih responsif)
230
+ if not isConnected then
231
+ break
232
+ end
233
+ task.wait(1)
234
+ end
235
+ isPolling = false
236
+ end)
237
+ end
238
+
239
+ -- ══════════════════════════════════════════════════════════════════════
240
+ -- AUTO-DETECT: Background heartbeat yang SELALU berjalan.
241
+ -- Mendeteksi apakah server MCP aktif setiap 2 detik.
242
+ -- Jika terdeteksi aktif → otomatis connect (tanpa perlu tekan tombol).
243
+ -- Jika terdeteksi mati → otomatis update status.
244
+ -- ══════════════════════════════════════════════════════════════════════
245
+ task.spawn(function()
246
+ task.wait(1) -- Tunggu sebentar agar UI siap
247
+
248
+ if ui then
249
+ ui:addLog("[System] Plugin termuat. Auto-detect server MCP aktif...", Color3.fromRGB(200, 200, 200))
250
+ end
251
+
252
+ while true do
253
+ -- Skip heartbeat jika polling loop sudah aktif (ia sudah menangani semuanya)
254
+ if not isPolling then
255
+ local success, errorMsg = pcall(function()
256
+ local cacheBuster = "?t=" .. tostring(os.clock())
257
+ return HttpService:GetAsync(TASK_URL .. cacheBuster)
258
+ end)
259
+
260
+ if success then
261
+ -- Server terdeteksi aktif!
262
+ if not isConnected and not isManualDisconnect then
263
+ isConnected = true
264
+ if ui then
265
+ ui:setStatus("connected")
266
+ ui:addLog("[System] Server MCP terdeteksi! Auto-connected.", Color3.fromRGB(100, 255, 100))
267
+ end
268
+ startPolling()
269
+ end
270
+ else
271
+ -- Server tidak aktif
272
+ if isConnected then
273
+ -- Server baru saja mati (sebelumnya connected) — polling loop juga akan exit
274
+ isConnected = false
275
+ if ui then
276
+ ui:setStatus("waiting")
277
+ ui:addLog("[System] Server MCP terputus. Menunggu reconnect...", Color3.fromRGB(255, 200, 100))
278
+ end
279
+ elseif not isManualDisconnect then
280
+ if ui then
281
+ ui:setStatus("waiting")
282
+ -- HANYA print error jika belum terkoneksi, untuk debugging
283
+ ui:addLog("[Debug] Deteksi gagal: " .. tostring(errorMsg), Color3.fromRGB(150, 150, 150))
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ task.wait(2)
290
+ end
291
+ end)
@@ -0,0 +1,24 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
3
+ if not success or not config.ClassName then
4
+ return { status = "error", error = "Format data JSON tidak valid atau ClassName hilang." }
5
+ end
6
+
7
+ local newSuccess, newInst = pcall(function()
8
+ local inst = Instance.new(config.ClassName)
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
+ return inst
17
+ end)
18
+
19
+ if newSuccess then
20
+ return { status = "success", result = "Instance '"..config.ClassName.."' berhasil dibuat di " .. targetPath }
21
+ else
22
+ return { status = "error", error = "Gagal membuat Instance. Pastikan ClassName valid." }
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ -- Guardrail: Cegah penghapusan Service utama
3
+ local protectedServices = {
4
+ "Workspace", "Players", "Lighting", "MaterialService", "NetworkClient",
5
+ "ReplicatedFirst", "ReplicatedStorage", "ServerScriptService", "ServerStorage",
6
+ "StarterGui", "StarterPack", "StarterPlayer", "Teams", "SoundService", "Chat",
7
+ "TextChatService", "VoiceChatService"
8
+ }
9
+
10
+ if targetInstance.Parent == game or targetInstance.Parent == nil then
11
+ for _, serviceName in ipairs(protectedServices) do
12
+ if targetInstance.Name == serviceName or targetInstance.ClassName == serviceName then
13
+ return { status = "error", error = "GUARDRAIL BLOCKED: Anda tidak diizinkan untuk menghapus layanan tingkat atas (" .. targetInstance.Name .. ")." }
14
+ end
15
+ end
16
+ end
17
+
18
+ local name = targetInstance.Name
19
+ local className = targetInstance.ClassName
20
+ local parentName = targetInstance.Parent and targetInstance.Parent.Name or "nil"
21
+
22
+ local success, err = pcall(function()
23
+ targetInstance:Destroy()
24
+ end)
25
+
26
+ if success then
27
+ return { status = "success", result = "Instance '" .. name .. "' (" .. className .. ") berhasil dihapus dari " .. parentName .. "." }
28
+ else
29
+ return { status = "error", error = "Gagal menghapus instance: " .. tostring(err) }
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local code = data
3
+ if not code or code == "" then
4
+ return { status = "error", error = "Kode yang dieksekusi tidak boleh kosong." }
5
+ end
6
+
7
+ local HttpService = game:GetService("HttpService")
8
+
9
+ -- Bungkus code ke dalam fungsi return
10
+ local wrappedCode = "return function()\n" .. code .. "\nend"
11
+
12
+ local moduleScript = Instance.new("ModuleScript")
13
+ moduleScript.Name = "AI_Execute_Temp"
14
+ moduleScript.Source = wrappedCode
15
+
16
+ -- Taruh di ReplicatedStorage sementara agar bisa di-require
17
+ moduleScript.Parent = game:GetService("ReplicatedStorage")
18
+
19
+ local resultData = nil
20
+ local execError = nil
21
+ local isFinished = false
22
+
23
+ -- Eksekusi asinkron dengan batas waktu (timeout) 5 detik
24
+ task.spawn(function()
25
+ local reqSuccess, func = pcall(function() return require(moduleScript) end)
26
+ if reqSuccess and type(func) == "function" then
27
+ local execSuccess, res = pcall(func)
28
+ if execSuccess then
29
+ resultData = res
30
+ else
31
+ execError = "Runtime Error: " .. tostring(res)
32
+ end
33
+ else
34
+ execError = "Syntax/Require Error: " .. tostring(func)
35
+ end
36
+ isFinished = true
37
+ end)
38
+
39
+ -- Tunggu hingga 5 detik
40
+ local timeWaited = 0
41
+ while not isFinished and timeWaited < 5 do
42
+ timeWaited = timeWaited + task.wait()
43
+ end
44
+
45
+ -- Pembersihan (Wajib dihancurkan)
46
+ pcall(function() moduleScript:Destroy() end)
47
+
48
+ if not isFinished then
49
+ return { status = "error", error = "Timeout 5 detik terlampaui. Kemungkinan ada infinite loop tanpa task.wait()." }
50
+ end
51
+
52
+ if execError then
53
+ return { status = "error", error = execError }
54
+ end
55
+
56
+ -- Cegah return nil error
57
+ if resultData == nil then
58
+ return { status = "success", result = "Eksekusi berhasil (Tidak ada nilai kembalian)." }
59
+ end
60
+
61
+ -- JSON Encode untuk mencegah raw Instance return crash
62
+ local serializeSuccess, jsonRes = pcall(function()
63
+ return HttpService:JSONEncode(resultData)
64
+ end)
65
+
66
+ if serializeSuccess then
67
+ return { status = "success", result = HttpService:JSONDecode(jsonRes) } -- decode kembali agar format aslinya diteruskan oleh router
68
+ else
69
+ return { status = "error", error = "Gagal menserialisasi hasil (Dilarang mengembalikan raw Roblox Instance): " .. tostring(jsonRes) }
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local success, config = pcall(function() return ctx.HttpService:JSONDecode(data) end)
3
+ if not success or not config.Size or not config.Position or not config.Material then
4
+ return { status = "error", error = "Format JSON tidak valid. Membutuhkan Size, Position, dan Material." }
5
+ end
6
+
7
+ local fillSuccess, err = pcall(function()
8
+ local terrain = game.Workspace.Terrain
9
+ local materialEnum = Enum.Material[config.Material] or Enum.Material.Grass
10
+
11
+ local size = Vector3.new(config.Size[1], config.Size[2], config.Size[3])
12
+ local position = Vector3.new(config.Position[1], config.Position[2], config.Position[3])
13
+
14
+ local region = Region3.new(position - (size/2), position + (size/2))
15
+ terrain:FillRegion(region:ExpandToGrid(4), 4, materialEnum)
16
+ end)
17
+
18
+ if fillSuccess then
19
+ return { status = "success", result = "Terrain (" .. config.Material .. ") berhasil di-generate." }
20
+ else
21
+ return { status = "error", error = "Gagal men-generate Terrain. Pastikan tipe data array angka valid: " .. tostring(err) }
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ local children = {}
3
+ for _, child in ipairs(targetInstance:GetChildren()) do
4
+ table.insert(children, child.Name .. " (" .. child.ClassName .. ")")
5
+ end
6
+ return { status = "success", result = children }
7
+ end
@@ -0,0 +1,7 @@
1
+ return function(targetInstance, data, ctx, targetPath)
2
+ if #ctx.recentLogs == 0 then
3
+ return { status = "success", result = {"Console kosong. Tidak ada error atau log terbaru."} }
4
+ else
5
+ return { status = "success", result = ctx.recentLogs }
6
+ end
7
+ end