@adiholick/nvstudio-mcp 2.2.3 → 2.3.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/README.md +43 -47
- package/agent-plugin/mcp_config.json +8 -0
- package/agent-plugin/plugin.json +6 -0
- package/agent-plugin/rules/AGENTS.md +87 -0
- package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
- package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
- package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +69 -0
- package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
- package/dist/build-plugin.js +55 -0
- package/dist/index.js +216 -3
- package/dist/luau-validator.js +99 -34
- package/dist/roblox-bridge.js +44 -4
- package/dist/roblox-docs.js +264 -0
- package/package.json +5 -3
- package/public/index.html +619 -108
- package/studio-plugin/Main.server.lua +1 -1
- package/studio-plugin/batch_create_instances.lua +128 -0
- package/studio-plugin/batch_set_properties.lua +69 -0
- package/studio-plugin/insert_asset.lua +85 -12
- package/studio-plugin/nvstudio_mcp.rbxmx +885 -14
- package/studio-plugin/preview_asset.lua +99 -0
- package/studio-plugin/scan_security.lua +81 -0
- package/studio-plugin/security_scanner.lua +346 -0
- package/studio-plugin/set_properties.lua +10 -0
- package/studio-plugin/ui_manager.lua +1 -1
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const task_queue_1 = require("./task-queue");
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const luau_validator_1 = require("./luau-validator");
|
|
15
|
+
const roblox_docs_1 = require("./roblox-docs");
|
|
15
16
|
const HISTORY_DIR = path_1.default.join(__dirname, '..', '.history');
|
|
16
17
|
if (!fs_1.default.existsSync(HISTORY_DIR)) {
|
|
17
18
|
fs_1.default.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
@@ -377,9 +378,64 @@ const TOOLS = [
|
|
|
377
378
|
required: ["material", "position", "size"],
|
|
378
379
|
},
|
|
379
380
|
},
|
|
381
|
+
{
|
|
382
|
+
name: "preview_asset",
|
|
383
|
+
description: "Pratinjau hierarki dan pemindaian keamanan aset Creator Store secara terisolasi sebelum dimasukkan ke dalam game. Mendeteksi skrip backdoor, require eksternal numerik, obfuscation, webhook discord, dan virus script tanpa risiko mengeksekusi kode berbahaya.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
asset_id: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Roblox Asset ID angka (atau rbxassetid://) dari Creator Store.",
|
|
390
|
+
},
|
|
391
|
+
deep_scan: {
|
|
392
|
+
type: "boolean",
|
|
393
|
+
description: "Jika true (default), memindai dan menganalisis source code seluruh skrip di dalam model.",
|
|
394
|
+
},
|
|
395
|
+
placeId: {
|
|
396
|
+
type: "number",
|
|
397
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
398
|
+
},
|
|
399
|
+
studioId: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Target Studio session ID.",
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
required: ["asset_id"],
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: "scan_security",
|
|
409
|
+
description: "Audit keamanan instance atau model yang sudah ada di dalam game (Workspace, ServerScriptService, dll.) untuk mendeteksi skrip backdoor, virus, webhook discord, dan kode berbahaya.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
target: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Path instance atau folder yang ingin diaudit (default: 'Workspace').",
|
|
416
|
+
},
|
|
417
|
+
deep_scan: {
|
|
418
|
+
type: "boolean",
|
|
419
|
+
description: "Jika true (default), memindai isi source code skrip.",
|
|
420
|
+
},
|
|
421
|
+
clean_threats: {
|
|
422
|
+
type: "boolean",
|
|
423
|
+
description: "Jika true, secara otomatis menghapus skrip berbahaya yang terdeteksi (default: false).",
|
|
424
|
+
},
|
|
425
|
+
placeId: {
|
|
426
|
+
type: "number",
|
|
427
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
428
|
+
},
|
|
429
|
+
studioId: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "Target Studio session ID.",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
380
436
|
{
|
|
381
437
|
name: "insert_asset",
|
|
382
|
-
description: "Muat dan masukkan aset Roblox (Model/Package) berdasarkan Asset ID ke dalam Workspace atau target tertentu.",
|
|
438
|
+
description: "Muat dan masukkan aset Roblox (Model/Package) berdasarkan Asset ID ke dalam Workspace atau target tertentu dengan proteksi sanitasi skrip otomatis.",
|
|
383
439
|
inputSchema: {
|
|
384
440
|
type: "object",
|
|
385
441
|
properties: {
|
|
@@ -391,6 +447,14 @@ const TOOLS = [
|
|
|
391
447
|
type: "string",
|
|
392
448
|
description: "Parent tujuan tempat menempatkan model (default: 'Workspace').",
|
|
393
449
|
},
|
|
450
|
+
strip_scripts: {
|
|
451
|
+
type: "boolean",
|
|
452
|
+
description: "Jika true (default: true), secara otomatis menghapus semua skrip dari model sebelum dimasukkan ke game demi keamanan mutlak dari backdoor.",
|
|
453
|
+
},
|
|
454
|
+
scan_security: {
|
|
455
|
+
type: "boolean",
|
|
456
|
+
description: "Jika true (default: true), memindai potensi ancaman keamanan sebelum insert.",
|
|
457
|
+
},
|
|
394
458
|
placeId: {
|
|
395
459
|
type: "number",
|
|
396
460
|
description: "Target Place ID untuk multi-Studio routing.",
|
|
@@ -475,7 +539,7 @@ const TOOLS = [
|
|
|
475
539
|
},
|
|
476
540
|
{
|
|
477
541
|
name: "call_mcp_tool",
|
|
478
|
-
description: "Legacy tool dispatcher untuk kompatibilitas mundur. Mendukung semua command: execute_luau, get_children, get_full_tree, get_script_source, update_script_source, patch_script_source, rollback_script, get_properties, set_properties, create_instance, delete_instance, search_instance, generate_terrain, insert_asset, search_asset, script_grep, get_logs.",
|
|
542
|
+
description: "Legacy tool dispatcher untuk kompatibilitas mundur. Mendukung semua command: execute_luau, get_children, get_full_tree, get_script_source, update_script_source, patch_script_source, rollback_script, get_properties, set_properties, create_instance, delete_instance, search_instance, generate_terrain, insert_asset, preview_asset, scan_security, batch_create_instances, batch_set_properties, search_asset, script_grep, get_roblox_docs, get_logs.",
|
|
479
543
|
inputSchema: {
|
|
480
544
|
type: "object",
|
|
481
545
|
properties: {
|
|
@@ -503,6 +567,80 @@ const TOOLS = [
|
|
|
503
567
|
required: ["command"],
|
|
504
568
|
},
|
|
505
569
|
},
|
|
570
|
+
{
|
|
571
|
+
name: "get_roblox_docs",
|
|
572
|
+
description: "Ambil dokumentasi resmi Roblox Engine API langsung dari Creator Docs (Classes, Services, Data types, Enums, dan Libraries). Mengembalikan format Markdown lengkap beserta signature fungsi, deskripsi parameter, properti, event, dan contoh kode untuk mencegah halusinasi kode Luau.",
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: "object",
|
|
575
|
+
properties: {
|
|
576
|
+
query: {
|
|
577
|
+
type: "string",
|
|
578
|
+
description: "Nama Class, Service, Data type, Enum, atau Library Roblox (contoh: 'TweenService', 'CanvasGroup', 'Vector3', 'EasingStyle', 'task').",
|
|
579
|
+
},
|
|
580
|
+
member: {
|
|
581
|
+
type: "string",
|
|
582
|
+
description: "Opsional: Nama method, property, event, atau enum item spesifik untuk menghemat token (contoh: 'Create', 'GroupTransparency', 'Completed').",
|
|
583
|
+
},
|
|
584
|
+
category: {
|
|
585
|
+
type: "string",
|
|
586
|
+
enum: ["class", "datatype", "enum", "library", "auto"],
|
|
587
|
+
description: "Opsional: Kategori dokumentasi (default: 'auto').",
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
required: ["query"],
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "batch_create_instances",
|
|
595
|
+
description: "Pembangun instance massal dan deklaratif dalam satu kali pemanggilan. Mendukung mode hierarki bersarang (tree) atau daftar array datar (instances) lengkap dengan penyetelan properti langsung (UDim, Vector2, UDim2, Color3, Enum, dll.) untuk menghemat token dan latensi jaringan.",
|
|
596
|
+
inputSchema: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
target: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "Path root tujuan tempat menaruh instance (contoh: 'StarterGui.MyScreen', 'Workspace.Map').",
|
|
602
|
+
},
|
|
603
|
+
tree: {
|
|
604
|
+
type: "object",
|
|
605
|
+
description: "Objek hierarki bersarang { className, name, properties, children: [...] } untuk membangun struktur pohon secara rekursif.",
|
|
606
|
+
},
|
|
607
|
+
instances: {
|
|
608
|
+
type: "array",
|
|
609
|
+
description: "Daftar array datar [{ className, name, parent, properties }] jika tidak menggunakan mode tree.",
|
|
610
|
+
},
|
|
611
|
+
placeId: {
|
|
612
|
+
type: "number",
|
|
613
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
614
|
+
},
|
|
615
|
+
studioId: {
|
|
616
|
+
type: "string",
|
|
617
|
+
description: "Target Studio session ID.",
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: "batch_set_properties",
|
|
624
|
+
description: "Ubah properti pada banyak instance sekaligus dalam satu kali request atomik.",
|
|
625
|
+
inputSchema: {
|
|
626
|
+
type: "object",
|
|
627
|
+
properties: {
|
|
628
|
+
updates: {
|
|
629
|
+
type: "array",
|
|
630
|
+
description: "Array objek pembaruan: [{ target: 'Path.To.Instance', properties: { PropName: value, ... } }].",
|
|
631
|
+
},
|
|
632
|
+
placeId: {
|
|
633
|
+
type: "number",
|
|
634
|
+
description: "Target Place ID untuk multi-Studio routing.",
|
|
635
|
+
},
|
|
636
|
+
studioId: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Target Studio session ID.",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
required: ["updates"],
|
|
642
|
+
},
|
|
643
|
+
},
|
|
506
644
|
];
|
|
507
645
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
508
646
|
return { tools: TOOLS };
|
|
@@ -752,11 +890,41 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
752
890
|
});
|
|
753
891
|
break;
|
|
754
892
|
}
|
|
893
|
+
case "preview_asset": {
|
|
894
|
+
const assetId = String(args.asset_id ?? "");
|
|
895
|
+
if (!assetId)
|
|
896
|
+
throw new Error("Parameter 'asset_id' wajib diisi.");
|
|
897
|
+
const payload = JSON.stringify({
|
|
898
|
+
asset_id: assetId,
|
|
899
|
+
deep_scan: args.deep_scan !== false,
|
|
900
|
+
});
|
|
901
|
+
result = await dispatchTaskToStudio("preview_asset", "", payload, {
|
|
902
|
+
placeId: args.placeId,
|
|
903
|
+
studioId: args.studioId,
|
|
904
|
+
});
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
case "scan_security": {
|
|
908
|
+
const payload = JSON.stringify({
|
|
909
|
+
deep_scan: args.deep_scan !== false,
|
|
910
|
+
clean_threats: args.clean_threats === true,
|
|
911
|
+
});
|
|
912
|
+
result = await dispatchTaskToStudio("scan_security", args.target || "Workspace", payload, {
|
|
913
|
+
placeId: args.placeId,
|
|
914
|
+
studioId: args.studioId,
|
|
915
|
+
});
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
755
918
|
case "insert_asset": {
|
|
756
919
|
const assetId = String(args.asset_id ?? "");
|
|
757
920
|
if (!assetId)
|
|
758
921
|
throw new Error("Parameter 'asset_id' wajib diisi.");
|
|
759
|
-
|
|
922
|
+
const payload = JSON.stringify({
|
|
923
|
+
asset_id: assetId,
|
|
924
|
+
strip_scripts: args.strip_scripts !== false,
|
|
925
|
+
scan_security: args.scan_security !== false,
|
|
926
|
+
});
|
|
927
|
+
result = await dispatchTaskToStudio("insert_asset", args.target || "Workspace", payload, {
|
|
760
928
|
placeId: args.placeId,
|
|
761
929
|
studioId: args.studioId,
|
|
762
930
|
});
|
|
@@ -802,6 +970,39 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
802
970
|
}
|
|
803
971
|
break;
|
|
804
972
|
}
|
|
973
|
+
case "get_roblox_docs": {
|
|
974
|
+
const query = String(args.query ?? "").trim();
|
|
975
|
+
if (!query)
|
|
976
|
+
throw new Error("Parameter 'query' wajib diisi.");
|
|
977
|
+
const member = args.member ? String(args.member).trim() : undefined;
|
|
978
|
+
const category = args.category ? String(args.category).trim() : "auto";
|
|
979
|
+
result = await (0, roblox_docs_1.getRobloxDocs)(query, member, category);
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
case "batch_create_instances": {
|
|
983
|
+
const payload = JSON.stringify({
|
|
984
|
+
target: args.target,
|
|
985
|
+
tree: args.tree,
|
|
986
|
+
instances: args.instances,
|
|
987
|
+
});
|
|
988
|
+
result = await dispatchTaskToStudio("batch_create_instances", args.target || "Workspace", payload, {
|
|
989
|
+
placeId: args.placeId,
|
|
990
|
+
studioId: args.studioId,
|
|
991
|
+
});
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
case "batch_set_properties": {
|
|
995
|
+
if (!args.updates || !Array.isArray(args.updates))
|
|
996
|
+
throw new Error("Parameter 'updates' wajib berupa array.");
|
|
997
|
+
const payload = JSON.stringify({
|
|
998
|
+
updates: args.updates,
|
|
999
|
+
});
|
|
1000
|
+
result = await dispatchTaskToStudio("batch_set_properties", "", payload, {
|
|
1001
|
+
placeId: args.placeId,
|
|
1002
|
+
studioId: args.studioId,
|
|
1003
|
+
});
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
805
1006
|
case "call_mcp_tool": {
|
|
806
1007
|
const { command, target, data, placeId, studioId } = args;
|
|
807
1008
|
if (!command)
|
|
@@ -815,6 +1016,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
|
815
1016
|
if (target)
|
|
816
1017
|
backupScript(target, String(data));
|
|
817
1018
|
}
|
|
1019
|
+
// Intercept get_roblox_docs di Node.js
|
|
1020
|
+
if (command === "get_roblox_docs") {
|
|
1021
|
+
const queryParams = typeof data === "string" ? (function () { try {
|
|
1022
|
+
return JSON.parse(data);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
return { query: data };
|
|
1026
|
+
} })() : (data || {});
|
|
1027
|
+
const q = String(queryParams.query || target || "").trim();
|
|
1028
|
+
result = await (0, roblox_docs_1.getRobloxDocs)(q, queryParams.member, queryParams.category);
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
818
1031
|
// Intercept search_asset di Node.js
|
|
819
1032
|
if (command === "search_asset" && data) {
|
|
820
1033
|
const queryParams = typeof data === "string" ? JSON.parse(data) : data;
|
package/dist/luau-validator.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateLuauSyntax = validateLuauSyntax;
|
|
4
|
+
/**
|
|
5
|
+
* Heuristic pre-commit syntax validator for Luau.
|
|
6
|
+
* Validates bracket parity, string literals, multiline comments,
|
|
7
|
+
* and block keywords (function, do, statement if ... then, repeat ... until, end).
|
|
8
|
+
*/
|
|
4
9
|
function validateLuauSyntax(source) {
|
|
5
10
|
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
|
-
// Bracket stacks
|
|
10
11
|
let parenDepth = 0; // ()
|
|
11
12
|
let braceDepth = 0; // {}
|
|
12
13
|
let bracketDepth = 0; // []
|
|
14
|
+
let blockDepth = 0; // function, do, if ... then -> end
|
|
15
|
+
let repeatDepth = 0; // repeat -> until
|
|
13
16
|
let inString = false;
|
|
14
17
|
let stringChar = '';
|
|
15
18
|
let inMultilineString = false;
|
|
@@ -18,10 +21,11 @@ function validateLuauSyntax(source) {
|
|
|
18
21
|
for (let i = 0; i < lines.length; i++) {
|
|
19
22
|
const line = lines[i];
|
|
20
23
|
let j = 0;
|
|
24
|
+
const lineTokens = [];
|
|
21
25
|
while (j < line.length) {
|
|
22
26
|
const char = line[j];
|
|
23
27
|
const nextChar = line[j + 1] || '';
|
|
24
|
-
//
|
|
28
|
+
// Multiline comment start: --[[ or --[===[
|
|
25
29
|
if (!inString && !inMultilineString && !inMultilineComment) {
|
|
26
30
|
if (char === '-' && nextChar === '-') {
|
|
27
31
|
const nextNextChar = line[j + 2] || '';
|
|
@@ -39,7 +43,7 @@ function validateLuauSyntax(source) {
|
|
|
39
43
|
continue;
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
|
-
// Single
|
|
46
|
+
// Single-line comment, ignore rest of line
|
|
43
47
|
break;
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -60,7 +64,7 @@ function validateLuauSyntax(source) {
|
|
|
60
64
|
j++;
|
|
61
65
|
continue;
|
|
62
66
|
}
|
|
63
|
-
//
|
|
67
|
+
// String literals
|
|
64
68
|
if (!inString && !inMultilineString) {
|
|
65
69
|
if (char === '"' || char === "'") {
|
|
66
70
|
inString = true;
|
|
@@ -81,10 +85,6 @@ function validateLuauSyntax(source) {
|
|
|
81
85
|
j = k + 1;
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
84
|
-
// If it ended at end of line, we can't reliably detect it. Let's just avoid bracket counting
|
|
85
|
-
if (line[k] === undefined) {
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
if (inString) {
|
|
@@ -115,36 +115,97 @@ function validateLuauSyntax(source) {
|
|
|
115
115
|
j++;
|
|
116
116
|
continue;
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
// Outside strings & comments: collect bracket depth & tokens
|
|
119
|
+
if (char === '(') {
|
|
120
|
+
parenDepth++;
|
|
121
|
+
lineTokens.push('(');
|
|
122
|
+
}
|
|
123
|
+
else if (char === ')') {
|
|
124
|
+
parenDepth--;
|
|
125
|
+
lineTokens.push(')');
|
|
126
|
+
if (parenDepth < 0)
|
|
127
|
+
return { valid: false, error: 'Kelebihan tutup kurung ")"', line: i + 1 };
|
|
128
|
+
}
|
|
129
|
+
else if (char === '{') {
|
|
130
|
+
braceDepth++;
|
|
131
|
+
lineTokens.push('{');
|
|
132
|
+
}
|
|
133
|
+
else if (char === '}') {
|
|
134
|
+
braceDepth--;
|
|
135
|
+
lineTokens.push('}');
|
|
136
|
+
if (braceDepth < 0)
|
|
137
|
+
return { valid: false, error: 'Kelebihan tutup kurawal "}"', line: i + 1 };
|
|
138
|
+
}
|
|
139
|
+
else if (char === '[') {
|
|
140
|
+
bracketDepth++;
|
|
141
|
+
lineTokens.push('[');
|
|
142
|
+
}
|
|
143
|
+
else if (char === ']') {
|
|
144
|
+
bracketDepth--;
|
|
145
|
+
lineTokens.push(']');
|
|
146
|
+
if (bracketDepth < 0)
|
|
147
|
+
return { valid: false, error: 'Kelebihan tutup siku "]"', line: i + 1 };
|
|
148
|
+
}
|
|
149
|
+
else if (char === '=') {
|
|
150
|
+
lineTokens.push('=');
|
|
151
|
+
}
|
|
152
|
+
else if (char === ',') {
|
|
153
|
+
lineTokens.push(',');
|
|
154
|
+
}
|
|
155
|
+
else if (/[a-zA-Z_]/.test(char)) {
|
|
156
|
+
let start = j;
|
|
157
|
+
while (j < line.length && /[a-zA-Z0-9_]/.test(line[j])) {
|
|
158
|
+
j++;
|
|
141
159
|
}
|
|
160
|
+
lineTokens.push(line.substring(start, j));
|
|
161
|
+
continue;
|
|
142
162
|
}
|
|
143
163
|
j++;
|
|
144
164
|
}
|
|
145
165
|
if (inString) {
|
|
146
166
|
return { valid: false, error: 'String tidak ditutup (unclosed string literal)', line: i + 1 };
|
|
147
167
|
}
|
|
168
|
+
// Process line tokens for block balance
|
|
169
|
+
let statementIfCount = 0;
|
|
170
|
+
for (let t = 0; t < lineTokens.length; t++) {
|
|
171
|
+
const tok = lineTokens[t];
|
|
172
|
+
const prevTok = t > 0 ? lineTokens[t - 1] : null;
|
|
173
|
+
if (tok === 'function') {
|
|
174
|
+
blockDepth++;
|
|
175
|
+
}
|
|
176
|
+
else if (tok === 'do') {
|
|
177
|
+
blockDepth++;
|
|
178
|
+
}
|
|
179
|
+
else if (tok === 'if') {
|
|
180
|
+
// Luau expression if is preceded by '=', 'return', '(', ',', '{', '[', 'and', 'or', 'not'
|
|
181
|
+
const isExpr = prevTok && ['=', 'return', '(', ',', '{', '[', 'and', 'or', 'not'].includes(prevTok);
|
|
182
|
+
if (!isExpr) {
|
|
183
|
+
statementIfCount++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else if (tok === 'then') {
|
|
187
|
+
const isElseIf = lineTokens.includes('elseif');
|
|
188
|
+
if (!isElseIf) {
|
|
189
|
+
if (statementIfCount > 0) {
|
|
190
|
+
blockDepth++;
|
|
191
|
+
statementIfCount--;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (tok === 'repeat') {
|
|
196
|
+
repeatDepth++;
|
|
197
|
+
}
|
|
198
|
+
else if (tok === 'until') {
|
|
199
|
+
repeatDepth--;
|
|
200
|
+
if (repeatDepth < 0)
|
|
201
|
+
return { valid: false, error: 'Kelebihan kata kunci "until"', line: i + 1 };
|
|
202
|
+
}
|
|
203
|
+
else if (tok === 'end') {
|
|
204
|
+
blockDepth--;
|
|
205
|
+
if (blockDepth < 0)
|
|
206
|
+
return { valid: false, error: 'Kelebihan kata kunci "end"', line: i + 1 };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
148
209
|
}
|
|
149
210
|
if (inMultilineString)
|
|
150
211
|
return { valid: false, error: 'Multiline string tidak ditutup (unclosed [[ ]])' };
|
|
@@ -156,5 +217,9 @@ function validateLuauSyntax(source) {
|
|
|
156
217
|
return { valid: false, error: 'Kekurangan tutup kurawal "}"' };
|
|
157
218
|
if (bracketDepth > 0)
|
|
158
219
|
return { valid: false, error: 'Kekurangan tutup siku "]"' };
|
|
220
|
+
if (repeatDepth > 0)
|
|
221
|
+
return { valid: false, error: 'Kekurangan kata kunci "until"' };
|
|
222
|
+
if (blockDepth > 0)
|
|
223
|
+
return { valid: false, error: 'Kekurangan kata kunci "end" (blok tidak ditutup)' };
|
|
159
224
|
return { valid: true };
|
|
160
225
|
}
|
package/dist/roblox-bridge.js
CHANGED
|
@@ -18,6 +18,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
18
|
const child_process_1 = require("child_process");
|
|
19
19
|
const task_queue_1 = require("./task-queue");
|
|
20
20
|
const ws_gateway_1 = require("./ws-gateway");
|
|
21
|
+
const roblox_docs_1 = require("./roblox-docs");
|
|
21
22
|
const { version: SERVER_VERSION } = require('../package.json');
|
|
22
23
|
exports.isBridgeHosting = false;
|
|
23
24
|
let resolveBridgeReady;
|
|
@@ -349,7 +350,17 @@ function addLog(io, type, message) {
|
|
|
349
350
|
activityLogs.pop();
|
|
350
351
|
if (io.engine && io.engine.clientsCount > 0) {
|
|
351
352
|
io.emit('log', log);
|
|
352
|
-
io.emit('stats', {
|
|
353
|
+
io.emit('stats', {
|
|
354
|
+
totalTasks,
|
|
355
|
+
successCount,
|
|
356
|
+
errorCount,
|
|
357
|
+
sessionStart,
|
|
358
|
+
studioConnected: isStudioConnected(),
|
|
359
|
+
studioCount: getConnectedStudioCount(),
|
|
360
|
+
wsStudios: (0, ws_gateway_1.getWsPeerCount)(),
|
|
361
|
+
connectedStudios: getConnectedStudios(),
|
|
362
|
+
version: SERVER_VERSION
|
|
363
|
+
});
|
|
353
364
|
}
|
|
354
365
|
}
|
|
355
366
|
/**
|
|
@@ -422,7 +433,12 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
422
433
|
onStudioStatusChange: () => {
|
|
423
434
|
const connected = isStudioConnected();
|
|
424
435
|
const studioCount = getConnectedStudioCount();
|
|
425
|
-
io.emit('studio-status', {
|
|
436
|
+
io.emit('studio-status', {
|
|
437
|
+
connected,
|
|
438
|
+
studioCount,
|
|
439
|
+
connectedStudios: getConnectedStudios(),
|
|
440
|
+
version: SERVER_VERSION
|
|
441
|
+
});
|
|
426
442
|
broadcastStatus();
|
|
427
443
|
if (!connected && (0, task_queue_1.hasPendingTasks)()) {
|
|
428
444
|
checkPendingTasksOnStudioDisconnect();
|
|
@@ -464,10 +480,28 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
464
480
|
studioConnected: isStudioConnected(),
|
|
465
481
|
studioCount: getConnectedStudioCount(),
|
|
466
482
|
wsStudios: (0, ws_gateway_1.getWsPeerCount)(),
|
|
467
|
-
port
|
|
483
|
+
port,
|
|
484
|
+
version: SERVER_VERSION,
|
|
485
|
+
connectedStudios: getConnectedStudios(),
|
|
468
486
|
},
|
|
469
487
|
});
|
|
470
488
|
});
|
|
489
|
+
// ── /api/docs (Endpoint pencarian dokumentasi engine untuk web dashboard) ──
|
|
490
|
+
app.get('/api/docs', async (req, res) => {
|
|
491
|
+
try {
|
|
492
|
+
const query = String(req.query.query || '').trim();
|
|
493
|
+
const member = req.query.member ? String(req.query.member).trim() : undefined;
|
|
494
|
+
const category = req.query.category ? String(req.query.category).trim() : 'auto';
|
|
495
|
+
if (!query) {
|
|
496
|
+
return res.status(400).json({ error: "Parameter 'query' wajib diisi." });
|
|
497
|
+
}
|
|
498
|
+
const result = await (0, roblox_docs_1.getRobloxDocs)(query, member, category);
|
|
499
|
+
res.json({ success: true, result });
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
res.status(500).json({ error: err.message });
|
|
503
|
+
}
|
|
504
|
+
});
|
|
471
505
|
// ── /api/stats ─────────────────────────────────────────────────────────────
|
|
472
506
|
app.get('/api/stats', (req, res) => {
|
|
473
507
|
const activeStudios = getConnectedStudioCount();
|
|
@@ -514,7 +548,13 @@ function startBridgeServer(port, isDaemon = false) {
|
|
|
514
548
|
// Daftarkan studio jika belum ada
|
|
515
549
|
if (!studioEventQueues.has(studioId)) {
|
|
516
550
|
studioEventQueues.set(studioId, []);
|
|
517
|
-
io.emit('studio-status', {
|
|
551
|
+
io.emit('studio-status', {
|
|
552
|
+
connected: true,
|
|
553
|
+
studioCount: Math.max(studioEventQueues.size, 1),
|
|
554
|
+
studioId,
|
|
555
|
+
connectedStudios: getConnectedStudios(),
|
|
556
|
+
version: SERVER_VERSION
|
|
557
|
+
});
|
|
518
558
|
addLog(io, 'system', `Studio terdaftar. (ID: ${studioId.substring(0, 8)}…, Place: ${placeId ?? 'Local'}, Total: ${studioEventQueues.size})`);
|
|
519
559
|
if (!isDaemon && !dashboardOpened) {
|
|
520
560
|
dashboardOpened = true;
|