@adiholick/nvstudio-mcp 1.0.4 → 2.0.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/build-plugin.js +115 -0
- package/dist/index.js +50 -11
- package/dist/luau-validator.js +191 -0
- package/dist/roblox-bridge.js +27 -1
- package/dist/task-queue.js +4 -1
- package/package.json +5 -3
- package/studio-plugin/Main.server.lua +302 -0
- package/studio-plugin/create_instance.lua +24 -0
- package/studio-plugin/delete_instance.lua +31 -0
- package/studio-plugin/execute_luau.lua +71 -0
- package/studio-plugin/generate_terrain.lua +23 -0
- package/studio-plugin/get_children.lua +7 -0
- package/studio-plugin/get_logs.lua +7 -0
- package/studio-plugin/get_properties.lua +52 -0
- package/studio-plugin/get_script_source.lua +7 -0
- package/studio-plugin/insert_asset.lua +18 -0
- package/studio-plugin/nvstudio_mcp.rbxmx +1420 -0
- package/studio-plugin/patch_script_source.lua +97 -0
- package/studio-plugin/rollback_script.lua +13 -0
- package/studio-plugin/script_grep.lua +63 -0
- package/studio-plugin/search_instance.lua +20 -0
- package/studio-plugin/ui_manager.lua +443 -0
- package/studio-plugin/update_script_source.lua +16 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const PLUGIN_DIR = path_1.default.join(process.cwd(), 'studio-plugin');
|
|
10
|
+
const OUTPUT_FILE = path_1.default.join(PLUGIN_DIR, 'nvstudio_mcp.rbxmx');
|
|
11
|
+
// Helper to escape XML special characters
|
|
12
|
+
function escapeXml(unsafe) {
|
|
13
|
+
return unsafe.replace(/[<>&'"]/g, function (c) {
|
|
14
|
+
switch (c) {
|
|
15
|
+
case '<': return '<';
|
|
16
|
+
case '>': return '>';
|
|
17
|
+
case '&': return '&';
|
|
18
|
+
case '\'': return ''';
|
|
19
|
+
case '"': return '"';
|
|
20
|
+
default: return c;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function buildRbxmx() {
|
|
25
|
+
console.log('[Build] Memulai kompilasi nvstudio_mcp.rbxmx...');
|
|
26
|
+
if (!fs_1.default.existsSync(PLUGIN_DIR)) {
|
|
27
|
+
console.error(`[Build] Folder ${PLUGIN_DIR} tidak ditemukan.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const files = fs_1.default.readdirSync(PLUGIN_DIR).filter(file => file.endsWith('.lua'));
|
|
31
|
+
// RBXMX Header
|
|
32
|
+
let xml = `<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">\n`;
|
|
33
|
+
xml += `\t<External>null</External>\n\t<External>nil</External>\n`;
|
|
34
|
+
// Main Folder
|
|
35
|
+
xml += `\t<Item class="Folder" referent="RBX0497167F99814CA6A2096BB729707905">\n`;
|
|
36
|
+
xml += `\t\t<Properties>\n`;
|
|
37
|
+
xml += `\t\t\t<BinaryString name="AttributesSerialize"></BinaryString>\n`;
|
|
38
|
+
xml += `\t\t\t<SecurityCapabilities name="Capabilities">0</SecurityCapabilities>\n`;
|
|
39
|
+
xml += `\t\t\t<bool name="DefinesCapabilities">false</bool>\n`;
|
|
40
|
+
xml += `\t\t\t<string name="Name">nvstudio_mcp</string>\n`;
|
|
41
|
+
xml += `\t\t\t<int64 name="SourceAssetId">-1</int64>\n`;
|
|
42
|
+
xml += `\t\t\t<SharedString name="Tags">yuZpQdnvvUBOTYh1jqZ2cA==</SharedString>\n`;
|
|
43
|
+
xml += `\t\t</Properties>\n`;
|
|
44
|
+
// Process files
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const filePath = path_1.default.join(PLUGIN_DIR, file);
|
|
47
|
+
const source = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
48
|
+
const name = file.replace('.lua', '');
|
|
49
|
+
// Main.server.lua is a Script, others are ModuleScripts
|
|
50
|
+
const isMain = name === 'Main.server';
|
|
51
|
+
const className = isMain ? 'Script' : 'ModuleScript';
|
|
52
|
+
// Use a simple hash-like referent
|
|
53
|
+
const referent = `RBX_${name.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
54
|
+
xml += `\t\t<Item class="${className}" referent="${referent}">\n`;
|
|
55
|
+
xml += `\t\t\t<Properties>\n`;
|
|
56
|
+
xml += `\t\t\t\t<BinaryString name="AttributesSerialize"></BinaryString>\n`;
|
|
57
|
+
xml += `\t\t\t\t<SecurityCapabilities name="Capabilities">0</SecurityCapabilities>\n`;
|
|
58
|
+
xml += `\t\t\t\t<bool name="DefinesCapabilities">false</bool>\n`;
|
|
59
|
+
xml += `\t\t\t\t<bool name="Disabled">false</bool>\n`;
|
|
60
|
+
xml += `\t\t\t\t<Content name="LinkedSource"><null></null></Content>\n`;
|
|
61
|
+
xml += `\t\t\t\t<string name="Name">${name}</string>\n`;
|
|
62
|
+
xml += `\t\t\t\t<int64 name="SourceAssetId">-1</int64>\n`;
|
|
63
|
+
xml += `\t\t\t\t<SharedString name="Tags"></SharedString>\n`;
|
|
64
|
+
if (isMain) {
|
|
65
|
+
xml += `\t\t\t\t<token name="RunContext">0</token>\n`;
|
|
66
|
+
}
|
|
67
|
+
// Wrap source in CDATA to avoid XML escaping issues for the code itself
|
|
68
|
+
// Ensure no ]]> appears inside the CDATA
|
|
69
|
+
const safeSource = source.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
70
|
+
xml += `\t\t\t\t<ProtectedString name="Source"><![CDATA[${safeSource}]]></ProtectedString>\n`;
|
|
71
|
+
xml += `\t\t\t</Properties>\n`;
|
|
72
|
+
xml += `\t\t</Item>\n`;
|
|
73
|
+
}
|
|
74
|
+
// Close Folder
|
|
75
|
+
xml += `\t</Item>\n`;
|
|
76
|
+
// Shared Strings definition (required by Roblox XML format if Tags is used)
|
|
77
|
+
xml += `\t<SharedStrings>\n`;
|
|
78
|
+
xml += `\t\t<SharedString md5="yuZpQdnvvUBOTYh1jqZ2cA=="></SharedString>\n`;
|
|
79
|
+
xml += `\t</SharedStrings>\n`;
|
|
80
|
+
// Close Roblox
|
|
81
|
+
xml += `</roblox>\n`;
|
|
82
|
+
fs_1.default.writeFileSync(OUTPUT_FILE, xml, 'utf-8');
|
|
83
|
+
console.log(`[Build] Berhasil membuat ${OUTPUT_FILE}`);
|
|
84
|
+
copyToRobloxPlugins();
|
|
85
|
+
}
|
|
86
|
+
function copyToRobloxPlugins() {
|
|
87
|
+
let pluginsDir = '';
|
|
88
|
+
if (process.platform === 'win32') {
|
|
89
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
90
|
+
if (localAppData) {
|
|
91
|
+
pluginsDir = path_1.default.join(localAppData, 'Roblox', 'Plugins');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (process.platform === 'darwin') {
|
|
95
|
+
pluginsDir = path_1.default.join(os_1.default.homedir(), 'Documents', 'Roblox', 'Plugins');
|
|
96
|
+
}
|
|
97
|
+
if (pluginsDir) {
|
|
98
|
+
if (!fs_1.default.existsSync(pluginsDir)) {
|
|
99
|
+
console.log(`[Build] Folder plugin Roblox tidak ditemukan di ${pluginsDir}. Melewati penyalinan otomatis.`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const targetFile = path_1.default.join(pluginsDir, 'nvstudio_mcp.rbxmx');
|
|
103
|
+
try {
|
|
104
|
+
fs_1.default.copyFileSync(OUTPUT_FILE, targetFile);
|
|
105
|
+
console.log(`[Build] ✅ Berhasil menyalin nvstudio_mcp.rbxmx ke ${targetFile}`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`[Build] Gagal menyalin ke ${targetFile}:`, err.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`[Build] Platform ${process.platform} tidak didukung untuk auto-copy plugin.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
buildRbxmx();
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const roblox_bridge_1 = require("./roblox-bridge");
|
|
|
11
11
|
const task_queue_1 = require("./task-queue");
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const luau_validator_1 = require("./luau-validator");
|
|
14
15
|
const HISTORY_DIR = path_1.default.join(process.cwd(), '.history');
|
|
15
16
|
if (!fs_1.default.existsSync(HISTORY_DIR)) {
|
|
16
17
|
fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
@@ -26,7 +27,7 @@ function backupScript(target, code) {
|
|
|
26
27
|
// 2. Inisialisasi server MCP
|
|
27
28
|
const server = new index_js_1.Server({
|
|
28
29
|
name: "nvstudio-mcp",
|
|
29
|
-
version: "
|
|
30
|
+
version: "2.0.0",
|
|
30
31
|
}, {
|
|
31
32
|
capabilities: {
|
|
32
33
|
tools: {},
|
|
@@ -38,21 +39,21 @@ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
|
38
39
|
tools: [
|
|
39
40
|
{
|
|
40
41
|
name: "call_mcp_tool",
|
|
41
|
-
description: "ALAT MUTLAK UNTUK ROBLOX.
|
|
42
|
+
description: "ALAT MUTLAK UNTUK ROBLOX. Command: patch_script_source, 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
43
|
inputSchema: {
|
|
43
44
|
type: "object",
|
|
44
45
|
properties: {
|
|
45
46
|
command: {
|
|
46
47
|
type: "string",
|
|
47
|
-
description: "Perintah yang
|
|
48
|
+
description: "Perintah yang dieksekusi (contoh: 'patch_script_source', 'get_script_source', 'update_script_source').",
|
|
48
49
|
},
|
|
49
50
|
target: {
|
|
50
51
|
type: "string",
|
|
51
|
-
description: "Path absolut dari objek target di Roblox Studio (contoh: 'Workspace.Map.Script'
|
|
52
|
+
description: "Path absolut dari objek target di Roblox Studio (contoh: 'Workspace.Map.Script').",
|
|
52
53
|
},
|
|
53
54
|
data: {
|
|
54
55
|
type: "string",
|
|
55
|
-
description: "Data tambahan
|
|
56
|
+
description: "Data tambahan (misal payload JSON untuk patch_script_source, atau source code penuh untuk update_script_source).",
|
|
56
57
|
},
|
|
57
58
|
},
|
|
58
59
|
required: ["command", "target"],
|
|
@@ -82,20 +83,58 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
82
83
|
isError: true,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
|
-
//
|
|
86
|
-
if (command === "
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
86
|
+
// Pre-commit validation for full source replacement
|
|
87
|
+
if (command === "update_script_source" && data) {
|
|
88
|
+
const validation = (0, luau_validator_1.validateLuauSyntax)(String(data));
|
|
89
|
+
if (!validation.valid) {
|
|
89
90
|
return {
|
|
90
91
|
content: [{
|
|
91
92
|
type: "text",
|
|
92
|
-
text:
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
status: "error",
|
|
95
|
+
error: `Syntax error terdeteksi (Pre-commit check Node.js): ${validation.error} pada baris ${validation.line || 'unknown'}`
|
|
96
|
+
})
|
|
93
97
|
}],
|
|
94
98
|
isError: true
|
|
95
99
|
};
|
|
96
100
|
}
|
|
101
|
+
backupScript(target, String(data));
|
|
97
102
|
}
|
|
98
|
-
//
|
|
103
|
+
// Intercept command yang dieksekusi di server lokal (Node.js) alih-alih di Studio
|
|
104
|
+
if (command === "search_asset" && data) {
|
|
105
|
+
try {
|
|
106
|
+
const queryParams = typeof data === "string" ? JSON.parse(data) : data;
|
|
107
|
+
const keyword = encodeURIComponent(queryParams.keyword || "");
|
|
108
|
+
let assetType = "10"; // Default: Model
|
|
109
|
+
if (queryParams.category === "Audio") {
|
|
110
|
+
assetType = "9"; // Audio
|
|
111
|
+
}
|
|
112
|
+
else if (queryParams.category === "Decal" || queryParams.category === "Image") {
|
|
113
|
+
assetType = "13"; // Decal
|
|
114
|
+
}
|
|
115
|
+
const url = `https://catalog.roblox.com/v1/search/items?category=All&keyword=${keyword}&limit=10&itemTypes=Asset&assetTypes=${assetType}`;
|
|
116
|
+
const res = await fetch(url);
|
|
117
|
+
const jsonRes = await res.json();
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "text",
|
|
122
|
+
text: JSON.stringify({
|
|
123
|
+
status: "success",
|
|
124
|
+
result: jsonRes.data || jsonRes
|
|
125
|
+
}, null, 2)
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: JSON.stringify({ status: "error", error: "Gagal mencari aset: " + err.message }) }],
|
|
133
|
+
isError: true
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 8. Teruskan ke antrean dan kembalikan hasilnya sebagai string JSON
|
|
99
138
|
try {
|
|
100
139
|
if (command === "update_script_source" && data) {
|
|
101
140
|
backupScript(target, String(data));
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateLuauSyntax = validateLuauSyntax;
|
|
4
|
+
function validateLuauSyntax(source) {
|
|
5
|
+
const lines = source.split('\n');
|
|
6
|
+
// Stack for blocks: function, do, if, for, while, repeat
|
|
7
|
+
// We'll keep it simple: just count block openers vs block closers (end, until)
|
|
8
|
+
// Note: This is a heuristic pre-commit check. It doesn't replace a full AST parser,
|
|
9
|
+
// but catches 90% of AI hallucination syntax errors (missing 'end', etc.)
|
|
10
|
+
let blockDepth = 0;
|
|
11
|
+
let repeatDepth = 0;
|
|
12
|
+
// Bracket stacks
|
|
13
|
+
let parenDepth = 0; // ()
|
|
14
|
+
let braceDepth = 0; // {}
|
|
15
|
+
let bracketDepth = 0; // []
|
|
16
|
+
let inString = false;
|
|
17
|
+
let stringChar = '';
|
|
18
|
+
let inMultilineString = false;
|
|
19
|
+
let multilineEqCount = 0;
|
|
20
|
+
let inMultilineComment = false;
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
let j = 0;
|
|
24
|
+
while (j < line.length) {
|
|
25
|
+
const char = line[j];
|
|
26
|
+
const nextChar = line[j + 1] || '';
|
|
27
|
+
// Handle multiline comments
|
|
28
|
+
if (!inString && !inMultilineString && !inMultilineComment) {
|
|
29
|
+
if (char === '-' && nextChar === '-') {
|
|
30
|
+
const nextNextChar = line[j + 2] || '';
|
|
31
|
+
if (nextNextChar === '[') {
|
|
32
|
+
let k = j + 3;
|
|
33
|
+
let eq = 0;
|
|
34
|
+
while (line[k] === '=') {
|
|
35
|
+
eq++;
|
|
36
|
+
k++;
|
|
37
|
+
}
|
|
38
|
+
if (line[k] === '[') {
|
|
39
|
+
inMultilineComment = true;
|
|
40
|
+
multilineEqCount = eq;
|
|
41
|
+
j = k + 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Single line comment, skip rest of line
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (inMultilineComment) {
|
|
50
|
+
if (char === ']') {
|
|
51
|
+
let k = j + 1;
|
|
52
|
+
let eq = 0;
|
|
53
|
+
while (line[k] === '=') {
|
|
54
|
+
eq++;
|
|
55
|
+
k++;
|
|
56
|
+
}
|
|
57
|
+
if (line[k] === ']' && eq === multilineEqCount) {
|
|
58
|
+
inMultilineComment = false;
|
|
59
|
+
j = k + 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
j++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Handle strings
|
|
67
|
+
if (!inString && !inMultilineString) {
|
|
68
|
+
if (char === '"' || char === "'") {
|
|
69
|
+
inString = true;
|
|
70
|
+
stringChar = char;
|
|
71
|
+
j++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (char === '[') {
|
|
75
|
+
let k = j + 1;
|
|
76
|
+
let eq = 0;
|
|
77
|
+
while (line[k] === '=') {
|
|
78
|
+
eq++;
|
|
79
|
+
k++;
|
|
80
|
+
}
|
|
81
|
+
if (line[k] === '[') {
|
|
82
|
+
inMultilineString = true;
|
|
83
|
+
multilineEqCount = eq;
|
|
84
|
+
j = k + 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (inString) {
|
|
90
|
+
if (char === '\\' && (nextChar === '"' || nextChar === "'" || nextChar === '\\')) {
|
|
91
|
+
j += 2;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === stringChar) {
|
|
95
|
+
inString = false;
|
|
96
|
+
}
|
|
97
|
+
j++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (inMultilineString) {
|
|
101
|
+
if (char === ']') {
|
|
102
|
+
let k = j + 1;
|
|
103
|
+
let eq = 0;
|
|
104
|
+
while (line[k] === '=') {
|
|
105
|
+
eq++;
|
|
106
|
+
k++;
|
|
107
|
+
}
|
|
108
|
+
if (line[k] === ']' && eq === multilineEqCount) {
|
|
109
|
+
inMultilineString = false;
|
|
110
|
+
j = k + 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
j++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// At this point, we are outside strings and comments.
|
|
118
|
+
// Check brackets
|
|
119
|
+
if (char === '(')
|
|
120
|
+
parenDepth++;
|
|
121
|
+
else if (char === ')') {
|
|
122
|
+
parenDepth--;
|
|
123
|
+
if (parenDepth < 0)
|
|
124
|
+
return { valid: false, error: 'Kelebihan tutup kurung ")"', line: i + 1 };
|
|
125
|
+
}
|
|
126
|
+
else if (char === '{')
|
|
127
|
+
braceDepth++;
|
|
128
|
+
else if (char === '}') {
|
|
129
|
+
braceDepth--;
|
|
130
|
+
if (braceDepth < 0)
|
|
131
|
+
return { valid: false, error: 'Kelebihan tutup kurawal "}"', line: i + 1 };
|
|
132
|
+
}
|
|
133
|
+
else if (char === '[')
|
|
134
|
+
bracketDepth++;
|
|
135
|
+
else if (char === ']') {
|
|
136
|
+
bracketDepth--;
|
|
137
|
+
if (bracketDepth < 0)
|
|
138
|
+
return { valid: false, error: 'Kelebihan tutup siku "]"', line: i + 1 };
|
|
139
|
+
}
|
|
140
|
+
j++;
|
|
141
|
+
}
|
|
142
|
+
if (inString) {
|
|
143
|
+
return { valid: false, error: 'String tidak ditutup (unclosed string literal)', line: i + 1 };
|
|
144
|
+
}
|
|
145
|
+
// Tokenize line to check blocks (very simplified heuristic)
|
|
146
|
+
// We only check for keywords bounded by word boundaries
|
|
147
|
+
if (!inMultilineString && !inMultilineComment) {
|
|
148
|
+
// Remove string literals and comments for naive keyword counting
|
|
149
|
+
let cleanLine = line.replace(/(--.*)/, ''); // remove single line comments
|
|
150
|
+
cleanLine = cleanLine.replace(/(["'])(?:(?=(\\?))\2.)*?\1/g, ''); // remove inline strings
|
|
151
|
+
// Avoid false positives like `local func = "do"` by looking at word boundaries
|
|
152
|
+
// This is a naive regex matching.
|
|
153
|
+
// In Luau: 'do', 'if', 'function', 'for', 'while' need an 'end'.
|
|
154
|
+
// 'repeat' needs 'until'.
|
|
155
|
+
const words = cleanLine.split(/[^a-zA-Z0-9_]+/).filter(w => w.length > 0);
|
|
156
|
+
for (let w = 0; w < words.length; w++) {
|
|
157
|
+
const word = words[w];
|
|
158
|
+
if (word === 'if' || word === 'function' || word === 'for' || word === 'while' || word === 'do') {
|
|
159
|
+
// 'elseif' does not create a new block.
|
|
160
|
+
// 'if' does.
|
|
161
|
+
blockDepth++;
|
|
162
|
+
}
|
|
163
|
+
else if (word === 'end') {
|
|
164
|
+
blockDepth--;
|
|
165
|
+
}
|
|
166
|
+
else if (word === 'repeat') {
|
|
167
|
+
repeatDepth++;
|
|
168
|
+
}
|
|
169
|
+
else if (word === 'until') {
|
|
170
|
+
repeatDepth--;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (inMultilineString)
|
|
176
|
+
return { valid: false, error: 'Multiline string tidak ditutup (unclosed [[ ]])' };
|
|
177
|
+
if (inMultilineComment)
|
|
178
|
+
return { valid: false, error: 'Multiline comment tidak ditutup (unclosed --[[ ]])' };
|
|
179
|
+
if (parenDepth > 0)
|
|
180
|
+
return { valid: false, error: 'Kekurangan tutup kurung ")"' };
|
|
181
|
+
if (braceDepth > 0)
|
|
182
|
+
return { valid: false, error: 'Kekurangan tutup kurawal "}"' };
|
|
183
|
+
if (bracketDepth > 0)
|
|
184
|
+
return { valid: false, error: 'Kekurangan tutup siku "]"' };
|
|
185
|
+
// We can't strictly enforce blockDepth == 0 because our heuristic is very naive
|
|
186
|
+
// and might be thrown off by complex inline patterns, but it catches massive missing 'end's.
|
|
187
|
+
// So we'll return a warning instead of blocking if blockDepth != 0,
|
|
188
|
+
// or we can just let it pass to Studio which will output the real error.
|
|
189
|
+
// For now, let's just use it as a basic check.
|
|
190
|
+
return { valid: true };
|
|
191
|
+
}
|
package/dist/roblox-bridge.js
CHANGED
|
@@ -89,6 +89,16 @@ function startBridgeServer(port = 3055) {
|
|
|
89
89
|
app.get('/api/stats', (req, res) => {
|
|
90
90
|
res.json({ totalTasks, successCount, errorCount, sessionStart, studioConnected, port });
|
|
91
91
|
});
|
|
92
|
+
const waitingPollers = [];
|
|
93
|
+
task_queue_1.taskEmitter.on('new_task', () => {
|
|
94
|
+
if (task_queue_1.taskQueue.length > 0 && waitingPollers.length > 0) {
|
|
95
|
+
const res = waitingPollers.shift();
|
|
96
|
+
const task = task_queue_1.taskQueue.shift();
|
|
97
|
+
totalTasks++;
|
|
98
|
+
addLog(io, 'task', `Mengirim perintah '${task?.command}' ke Studio${task?.target ? ` → ${task.target}` : ''}.`);
|
|
99
|
+
res?.json(task);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
92
102
|
// Endpoint Studio Polling
|
|
93
103
|
app.get('/api/tasks', (req, res) => {
|
|
94
104
|
if (!studioConnected) {
|
|
@@ -113,7 +123,23 @@ function startBridgeServer(port = 3055) {
|
|
|
113
123
|
res.json(task);
|
|
114
124
|
}
|
|
115
125
|
else {
|
|
116
|
-
|
|
126
|
+
// Long-polling: tunggu maksimal 20 detik
|
|
127
|
+
const timeoutId = setTimeout(() => {
|
|
128
|
+
const index = waitingPollers.indexOf(res);
|
|
129
|
+
if (index !== -1) {
|
|
130
|
+
waitingPollers.splice(index, 1);
|
|
131
|
+
res.json({ id: null });
|
|
132
|
+
}
|
|
133
|
+
}, 20000);
|
|
134
|
+
waitingPollers.push(res);
|
|
135
|
+
// Bersihkan jika terputus prematur
|
|
136
|
+
req.on('close', () => {
|
|
137
|
+
clearTimeout(timeoutId);
|
|
138
|
+
const index = waitingPollers.indexOf(res);
|
|
139
|
+
if (index !== -1) {
|
|
140
|
+
waitingPollers.splice(index, 1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
117
143
|
}
|
|
118
144
|
});
|
|
119
145
|
// Endpoint Studio Response
|
package/dist/task-queue.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pendingTasks = exports.taskQueue = void 0;
|
|
3
|
+
exports.pendingTasks = exports.taskEmitter = exports.taskQueue = void 0;
|
|
4
4
|
exports.addTaskToQueue = addTaskToQueue;
|
|
5
5
|
exports.resolvePendingTask = resolvePendingTask;
|
|
6
6
|
const uuid_1 = require("uuid");
|
|
7
|
+
const events_1 = require("events");
|
|
7
8
|
// 1. Array untuk menampung antrean tugas (task queue)
|
|
8
9
|
exports.taskQueue = [];
|
|
10
|
+
exports.taskEmitter = new events_1.EventEmitter();
|
|
9
11
|
// 2. Map untuk menyimpan Promise dari tugas yang sedang menunggu (pending tasks)
|
|
10
12
|
exports.pendingTasks = new Map();
|
|
11
13
|
/**
|
|
@@ -24,6 +26,7 @@ function addTaskToQueue(command, target, data) {
|
|
|
24
26
|
}, 30000);
|
|
25
27
|
exports.pendingTasks.set(id, { resolve, reject, timer });
|
|
26
28
|
exports.taskQueue.push(task);
|
|
29
|
+
exports.taskEmitter.emit('new_task');
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
32
|
/**
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adiholick/nvstudio-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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
|
-
"build": "tsc",
|
|
13
|
+
"build": "tsc && npm run build:plugin",
|
|
14
|
+
"build:plugin": "node --experimental-strip-types src/build-plugin.ts",
|
|
13
15
|
"start": "node dist/index.js",
|
|
14
16
|
"dev": "ts-node src/index.ts"
|
|
15
17
|
},
|